@@ -56,6 +56,7 @@ class MtcOutput : public juce::HighResolutionTimer
5656 isRunningFlag.store (true , std::memory_order_relaxed);
5757 paused.store (false , std::memory_order_relaxed);
5858 currentQFIndex.store (0 , std::memory_order_relaxed);
59+ mtcSeeded = false ;
5960
6061 // Full Frame is sent after the first setTimecode() call populates
6162 // pendingTimecode -- avoids transmitting a misleading 00:00:00.00
@@ -123,6 +124,7 @@ class MtcOutput : public juce::HighResolutionTimer
123124 stopTimer ();
124125 currentQFIndex.store (0 , std::memory_order_relaxed);
125126 paused.store (false , std::memory_order_relaxed);
127+ mtcSeeded = false ;
126128
127129 // Re-sync receivers after pause with a Full Frame message
128130 sendFullFrame ();
@@ -148,6 +150,7 @@ class MtcOutput : public juce::HighResolutionTimer
148150 return ;
149151 // Reset QF cycle to start fresh from the new position
150152 currentQFIndex.store (0 , std::memory_order_relaxed);
153+ mtcSeeded = false ;
151154 sendFullFrame ();
152155 }
153156
@@ -216,13 +219,46 @@ class MtcOutput : public juce::HighResolutionTimer
216219 double lastSend = lastQfSendTime.load (std::memory_order_relaxed);
217220 while ((now - lastSend) >= qfInterval && sent < 2 )
218221 {
219- // At QF index 0, snapshot the timecode for this entire 8-QF cycle
220- // This guarantees all 8 QFs describe the SAME timecode - no jumps
222+ // At QF index 0, determine the timecode for this entire 8-QF cycle.
223+ // Auto-increment: advance by 2 frames (one QF cycle = 2 frame durations).
224+ // Compare with pendingTimecode and only resync on diff > 2 (seek/jump).
225+ // This prevents 1-frame backward jitter from interpolation overshoot
226+ // causing a full QF cycle of wrong data → MTC receiver flicker.
227+ // Same architectural pattern as the LTC encoder's auto-increment.
221228 int qfIdx = currentQFIndex.load (std::memory_order_relaxed);
222229 if (qfIdx == 0 )
223230 {
224- const juce::SpinLock::ScopedLockType lock (tcLock);
225- cycleTimecode = pendingTimecode;
231+ Timecode pending;
232+ {
233+ const juce::SpinLock::ScopedLockType lock (tcLock);
234+ pending = pendingTimecode;
235+ }
236+
237+ if (!mtcSeeded)
238+ {
239+ cycleTimecode = pending;
240+ mtcSeeded = true ;
241+ }
242+ else
243+ {
244+ // Auto-increment by 2 frames (1 QF cycle = 2 frame durations)
245+ cycleTimecode = incrementFrame (incrementFrame (cycleTimecode, fps), fps);
246+
247+ // Resync if pending differs by more than 2 frames (seek/jump)
248+ int maxFrames = frameRateToInt (fps);
249+ auto toTotal = [maxFrames](const Timecode& t) -> int64_t {
250+ return (int64_t )t.hours * 3600 * maxFrames
251+ + (int64_t )t.minutes * 60 * maxFrames
252+ + (int64_t )t.seconds * maxFrames
253+ + (int64_t )t.frames ;
254+ };
255+ int64_t dayFrames = (int64_t )24 * 3600 * maxFrames;
256+ int64_t rawDiff = toTotal (pending) - toTotal (cycleTimecode);
257+ int64_t diff = ((rawDiff % dayFrames) + dayFrames) % dayFrames;
258+ if (diff > dayFrames / 2 ) diff = dayFrames - diff;
259+ if (diff > 2 )
260+ cycleTimecode = pending;
261+ }
226262 }
227263
228264 sendQuarterFrame (qfIdx, fps);
@@ -289,6 +325,7 @@ class MtcOutput : public juce::HighResolutionTimer
289325 juce::SpinLock tcLock;
290326 Timecode pendingTimecode; // Written by UI thread, read under tcLock
291327 Timecode cycleTimecode; // Timer-thread-only: snapshot taken at QF index 0, read through QF 1-7
328+ bool mtcSeeded = false ; // Auto-increment: false until first QF0 seeds cycleTimecode
292329 std::atomic<FrameRate> currentFps { FrameRate::FPS_25 };
293330 // currentQFIndex is primarily accessed from the timer thread, but reset from
294331 // the UI thread in start()/setPaused() after stopTimer(). JUCE guarantees
0 commit comments