Skip to content

Commit 94cd27b

Browse files
authored
Update V1.8.3
1 parent 075f905 commit 94cd27b

File tree

10 files changed

+467
-64
lines changed

10 files changed

+467
-64
lines changed

AppSettings.h

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#pragma once
66
#include <JuceHeader.h>
77
#include <unordered_map>
8+
#include <unordered_set>
89
#include <string>
910

1011
//==============================================================================
@@ -589,6 +590,50 @@ class TrackMap
589590

590591
uint64_t getGeneration() const { return generation; }
591592

593+
//------------------------------------------------------------------
594+
// rekordbox XML import -- parse <DJ_PLAYLISTS> into TrackMapEntry list.
595+
// Returns entries with artist, title, durationSec populated.
596+
// Offsets and triggers are left at defaults (user configures later).
597+
// Artwork and waveform will be fetched from the CDJ on first play.
598+
//------------------------------------------------------------------
599+
static std::vector<TrackMapEntry> parseRekordboxXml(const juce::File& file)
600+
{
601+
std::vector<TrackMapEntry> result;
602+
auto xml = juce::XmlDocument::parse(file);
603+
if (!xml || xml->getTagName() != "DJ_PLAYLISTS") return result;
604+
605+
auto* collection = xml->getChildByName("COLLECTION");
606+
if (!collection) return result;
607+
608+
// Deduplicate by key (same artist+title can appear from multiple
609+
// locations, e.g. local library + USB export in the same XML).
610+
std::unordered_set<std::string> seen;
611+
612+
for (auto* track = collection->getChildByName("TRACK");
613+
track != nullptr;
614+
track = track->getNextElementWithTagName("TRACK"))
615+
{
616+
juce::String title = track->getStringAttribute("Name").trim();
617+
juce::String artist = track->getStringAttribute("Artist").trim();
618+
int duration = track->getIntAttribute("TotalTime", 0);
619+
620+
if (title.isEmpty()) continue; // hasValidKey requires title
621+
622+
TrackMapEntry e;
623+
e.title = title;
624+
e.artist = artist;
625+
e.durationSec = duration;
626+
627+
auto k = e.key();
628+
if (seen.count(k)) continue;
629+
seen.insert(k);
630+
631+
result.push_back(std::move(e));
632+
}
633+
634+
return result;
635+
}
636+
592637
private:
593638
std::unordered_map<std::string, TrackMapEntry> entries;
594639
uint64_t generation = 0;

ArtnetOutput.h

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ class ArtnetOutput : public juce::HighResolutionTimer
9999
isRunningFlag.store(true, std::memory_order_relaxed);
100100
paused.store(false, std::memory_order_relaxed);
101101
sendErrors.store(0, std::memory_order_relaxed);
102+
artnetSeeded = false;
102103
updateTimerRate();
103104
return true;
104105
}
@@ -155,6 +156,7 @@ class ArtnetOutput : public juce::HighResolutionTimer
155156
}
156157
else if (isRunningFlag.load(std::memory_order_relaxed))
157158
{
159+
artnetSeeded = false;
158160
lastFrameSendTime.store(juce::Time::getMillisecondCounterHiRes(), std::memory_order_relaxed);
159161
updateTimerRate();
160162
}
@@ -171,6 +173,7 @@ class ArtnetOutput : public juce::HighResolutionTimer
171173
|| paused.load(std::memory_order_relaxed)
172174
|| socket == nullptr)
173175
return;
176+
artnetSeeded = false;
174177
FrameRate fps = currentFps.load(std::memory_order_relaxed);
175178
sendArtTimeCode(fps);
176179
}
@@ -273,11 +276,41 @@ class ArtnetOutput : public juce::HighResolutionTimer
273276

274277
void sendArtTimeCode(FrameRate fps)
275278
{
276-
Timecode tc;
279+
Timecode pending;
277280
{
278281
const juce::SpinLock::ScopedLockType lock(tcLock);
279-
tc = timecodeToSend;
282+
pending = timecodeToSend;
283+
}
284+
285+
// Auto-increment: advance by 1 frame per send. Compare with
286+
// pendingTimecode and only resync on diff > 1 (seek/jump).
287+
// Prevents 1-frame backward jitter from interpolation overshoot.
288+
// Same architectural pattern as the LTC and MTC encoders.
289+
Timecode tc;
290+
if (!artnetSeeded)
291+
{
292+
tc = pending;
293+
artnetSeeded = true;
294+
}
295+
else
296+
{
297+
tc = incrementFrame(encoderTc, fps);
298+
299+
int maxFrames = frameRateToInt(fps);
300+
auto toTotal = [maxFrames](const Timecode& t) -> int64_t {
301+
return (int64_t)t.hours * 3600 * maxFrames
302+
+ (int64_t)t.minutes * 60 * maxFrames
303+
+ (int64_t)t.seconds * maxFrames
304+
+ (int64_t)t.frames;
305+
};
306+
int64_t dayFrames = (int64_t)24 * 3600 * maxFrames;
307+
int64_t rawDiff = toTotal(pending) - toTotal(tc);
308+
int64_t diff = ((rawDiff % dayFrames) + dayFrames) % dayFrames;
309+
if (diff > dayFrames / 2) diff = dayFrames - diff;
310+
if (diff > 1)
311+
tc = pending;
280312
}
313+
encoderTc = tc;
281314

282315
// Validate ranges -- don't send corrupt data to the network
283316
int maxFrames = frameRateToInt(fps);
@@ -335,6 +368,8 @@ class ArtnetOutput : public juce::HighResolutionTimer
335368

336369
juce::SpinLock tcLock;
337370
Timecode timecodeToSend; // Written by UI thread under tcLock, read by timer thread under tcLock
371+
Timecode encoderTc; // Auto-increment: last sent timecode (timer thread only)
372+
bool artnetSeeded = false; // Auto-increment: false until first frame seeds encoderTc
338373
std::atomic<FrameRate> currentFps { FrameRate::FPS_25 };
339374
std::atomic<double> lastFrameSendTime { 0.0 };
340375
std::atomic<uint32_t> sendErrors { 0 };

DbServerClient.h

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ class DbServerClient : private juce::Thread
495495
static constexpr int kPortDiscoveryPort = 12523;
496496
static constexpr int kDefaultDbPort = 1051;
497497
static constexpr int kConnectTimeoutMs = 3000;
498-
static constexpr int kReadTimeoutMs = 3000;
498+
static constexpr int kReadTimeoutMs = 1000; // CDJ responds in <10ms; 3000 was wasteful
499499
static constexpr int kMaxCacheEntries = 256;
500500
static constexpr int kMaxArtCacheEntries = 64;
501501
static constexpr int kMaxConnections = 6;
@@ -555,6 +555,7 @@ class DbServerClient : private juce::Thread
555555
std::unique_ptr<juce::StreamingSocket> socket;
556556
int dbPort = 0;
557557
bool contextSetUp = false;
558+
uint8_t contextPlayer = 0; // player number used in setupQueryContext
558559
uint32_t txId = 0;
559560
double lastFailTime = 0.0;
560561

@@ -568,6 +569,7 @@ class DbServerClient : private juce::Thread
568569
socket = nullptr;
569570
}
570571
contextSetUp = false;
572+
contextPlayer = 0;
571573
txId = 0;
572574
dbPort = 0;
573575
playerIP.clear();
@@ -892,7 +894,22 @@ class DbServerClient : private juce::Thread
892894
for (auto& conn : connections)
893895
{
894896
if (conn.playerIP == playerIP && conn.isConnected())
897+
{
898+
// If the query identity changed (e.g. NXS2 network topology
899+
// change caused suggestDbPlayerNumber to pick a different
900+
// player), the existing context is stale -- close and reconnect
901+
// so setupQueryContext uses the new identity.
902+
if (conn.contextPlayer != 0 && conn.contextPlayer != ourPlayer)
903+
{
904+
DBG("DbServerClient: context player mismatch ("
905+
+ juce::String(conn.contextPlayer) + " vs "
906+
+ juce::String(ourPlayer) + ") -- reconnecting to " + playerIP);
907+
conn.close();
908+
conn.lastFailTime = 0.0; // intentional close, not a failure
909+
break; // fall through to new connection below
910+
}
895911
return &conn;
912+
}
896913
}
897914

898915
// Find empty slot or recycle oldest
@@ -1060,6 +1077,7 @@ class DbServerClient : private juce::Thread
10601077
DBG("DbServerClient: query context established, CDJ reports player="
10611078
+ juce::String(resp.numArgs[1]));
10621079
conn.contextSetUp = true;
1080+
conn.contextPlayer = ourPlayer;
10631081
conn.txId = 1;
10641082
return true;
10651083
}

Main.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class SuperTimecodeConverterApplication : public juce::JUCEApplication
1111
SuperTimecodeConverterApplication() {}
1212

1313
const juce::String getApplicationName() override { return "Super Timecode Converter"; }
14-
const juce::String getApplicationVersion() override { return "1.8.2"; }
14+
const juce::String getApplicationVersion() override { return "1.8.3"; }
1515
bool moreThanOneInstanceAllowed() override { return false; }
1616

1717
void initialise(const juce::String&) override

MtcOutput.h

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)