Skip to content

Commit 83594ac

Browse files
authored
Update V1.7.1
1 parent 315eb95 commit 83594ac

9 files changed

Lines changed: 344 additions & 204 deletions

File tree

AppSettings.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,7 @@ struct EngineSettings
632632
int mtcOutputOffset = 0;
633633
int artnetOutputOffset = 0;
634634
int ltcOutputOffset = 0;
635+
int tcnetOutputOffsetMs = 0; // TCNet offset in milliseconds, -1000 to +1000
635636

636637
// Track change triggers -- destinations
637638
bool triggerMidiEnabled = false;
@@ -699,6 +700,7 @@ struct EngineSettings
699700
obj->setProperty("mtcOutputOffset", mtcOutputOffset);
700701
obj->setProperty("artnetOutputOffset", artnetOutputOffset);
701702
obj->setProperty("ltcOutputOffset", ltcOutputOffset);
703+
obj->setProperty("tcnetOutputOffsetMs", tcnetOutputOffsetMs);
702704

703705
// Track change triggers
704706
obj->setProperty("triggerMidiEnabled", triggerMidiEnabled);
@@ -797,6 +799,7 @@ struct EngineSettings
797799
mtcOutputOffset = clampOffset(getInt("mtcOutputOffset", 0));
798800
artnetOutputOffset = clampOffset(getInt("artnetOutputOffset", 0));
799801
ltcOutputOffset = clampOffset(getInt("ltcOutputOffset", 0));
802+
tcnetOutputOffsetMs = juce::jlimit(-1000, 1000, getInt("tcnetOutputOffsetMs", 0));
800803

801804
// Track change triggers
802805
triggerMidiEnabled = getBool("triggerMidiEnabled", false);
@@ -1041,6 +1044,7 @@ struct AppSettings
10411044
es.mtcOutputOffset = clampOffset(getInt("mtcOutputOffset", 0));
10421045
es.artnetOutputOffset = clampOffset(getInt("artnetOutputOffset", 0));
10431046
es.ltcOutputOffset = clampOffset(getInt("ltcOutputOffset", 0));
1047+
es.tcnetOutputOffsetMs = juce::jlimit(-1000, 1000, getInt("tcnetOutputOffsetMs", 0));
10441048

10451049
engines.clear();
10461050
engines.push_back(es);

DbServerClient.h

Lines changed: 77 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@ struct TrackMetadata
5353
std::vector<uint8_t> waveformData;
5454
int waveformEntryCount = 0;
5555
int waveformBytesPerEntry = 0; // 3 = ThreeBand, 6 = ColorNxs2
56+
bool waveformQueried = false; // true once waveform fetch has been attempted
5657

5758
bool isValid() const { return trackId != 0 && title.isNotEmpty(); }
5859
bool hasWaveform() const { return waveformEntryCount > 0 && !waveformData.empty(); }
60+
bool isFullyCached() const { return isValid() && waveformQueried; }
5961
bool isThreeBandWaveform() const { return waveformBytesPerEntry == 3; }
6062
};
6163

@@ -129,13 +131,16 @@ class DbServerClient : private juce::Thread
129131
+ " ourPlayer=" + juce::String(ourPlayer)
130132
+ " model=" + playerModel);
131133

132-
// Check cache first -- avoid unnecessary requests
134+
// Check cache first -- avoid unnecessary requests.
135+
// Use isFullyCached() so that entries with metadata but no waveform
136+
// (partial cache from an early query before CDJ was fully ready)
137+
// are re-requested to fetch the missing waveform data.
133138
uint64_t cacheKey = makeCacheKey(playerIP, trackId);
134139
{
135140
const juce::SpinLock::ScopedLockType lock(cacheLock);
136141
auto it = metadataCache.find(cacheKey);
137-
if (it != metadataCache.end() && it->second.isValid())
138-
return; // already cached
142+
if (it != metadataCache.end() && it->second.isFullyCached())
143+
return; // fully cached (metadata + waveform attempted)
139144
}
140145

141146
// Enqueue request (producer lock: any thread may call this)
@@ -1337,54 +1342,90 @@ class DbServerClient : private juce::Thread
13371342

13381343
if (req.trackId != 0)
13391344
{
1340-
// Metadata request
1341-
auto meta = queryTrackMetadata(*conn, req.slot, req.trackType,
1342-
req.trackId, req.ourPlayer);
1343-
if (meta.isValid())
1345+
// Check if metadata text is already cached (partial cache --
1346+
// metadata succeeded on a previous request but waveform or
1347+
// artwork may still be missing).
1348+
uint64_t cacheKey = makeCacheKey(req.playerIP, req.trackId);
1349+
bool metaAlreadyCached = false;
1350+
TrackMetadata meta;
13441351
{
1345-
cacheMetadata(req.playerIP, meta);
1346-
DBG("DbServerClient: cached metadata for track "
1347-
+ juce::String(req.trackId) + " -- \""
1348-
+ meta.artist + " - " + meta.title + "\"");
1352+
const juce::SpinLock::ScopedLockType lock(cacheLock);
1353+
auto it = metadataCache.find(cacheKey);
1354+
if (it != metadataCache.end() && it->second.isValid())
1355+
{
1356+
metaAlreadyCached = true;
1357+
meta = it->second;
1358+
}
1359+
}
1360+
1361+
if (!metaAlreadyCached)
1362+
{
1363+
// First time: query metadata from CDJ dbserver
1364+
meta = queryTrackMetadata(*conn, req.slot, req.trackType,
1365+
req.trackId, req.ourPlayer);
1366+
if (meta.isValid())
1367+
{
1368+
cacheMetadata(req.playerIP, meta);
1369+
DBG("DbServerClient: cached metadata for track "
1370+
+ juce::String(req.trackId) + " -- \""
1371+
+ meta.artist + " - " + meta.title + "\"");
1372+
}
1373+
else
1374+
{
1375+
errorCount.fetch_add(1, std::memory_order_relaxed);
1376+
DBG("DbServerClient: metadata query failed for trackId="
1377+
+ juce::String(req.trackId));
1378+
if (!conn->isConnected())
1379+
conn->close();
1380+
return;
1381+
}
1382+
}
13491383

1350-
// Request artwork if available
1351-
if (req.wantArt && meta.artworkId != 0)
1384+
// Artwork: fetch if not yet cached
1385+
if (req.wantArt && meta.artworkId != 0)
1386+
{
1387+
bool artCached;
1388+
{
1389+
const juce::SpinLock::ScopedLockType lock(artCacheLock);
1390+
artCached = artworkCache.count(meta.artworkId) > 0;
1391+
}
1392+
if (!artCached)
13521393
{
13531394
auto img = queryArtwork(*conn, req.slot, req.trackType,
13541395
meta.artworkId, req.ourPlayer);
13551396
if (img.isValid())
13561397
cacheArtwork(meta.artworkId, img);
13571398
}
1399+
}
13581400

1359-
// Request preview waveform (PWV6 for CDJ-3000, PWV4 for NXS2)
1360-
if (req.wantWaveform)
1401+
// Waveform: fetch if not yet cached
1402+
if (req.wantWaveform && !meta.hasWaveform())
1403+
{
1404+
auto wfResult = queryPreviewWaveform(
1405+
*conn, req.slot, req.trackType, req.trackId,
1406+
req.ourPlayer, req.playerModel);
1407+
if (wfResult.entryCount > 0)
13611408
{
1362-
auto wfResult = queryPreviewWaveform(
1363-
*conn, req.slot, req.trackType, req.trackId,
1364-
req.ourPlayer, req.playerModel);
1365-
if (wfResult.entryCount > 0)
1409+
const juce::SpinLock::ScopedLockType lock(cacheLock);
1410+
auto it = metadataCache.find(cacheKey);
1411+
if (it != metadataCache.end())
13661412
{
1367-
uint64_t key = makeCacheKey(req.playerIP, req.trackId);
1368-
const juce::SpinLock::ScopedLockType lock(cacheLock);
1369-
auto it = metadataCache.find(key);
1370-
if (it != metadataCache.end())
1371-
{
1372-
it->second.waveformData = std::move(wfResult.data);
1373-
it->second.waveformEntryCount = wfResult.entryCount;
1374-
it->second.waveformBytesPerEntry =
1375-
(wfResult.format == WaveformFormat::ThreeBand) ? 3 : 6;
1376-
}
1413+
it->second.waveformData = std::move(wfResult.data);
1414+
it->second.waveformEntryCount = wfResult.entryCount;
1415+
it->second.waveformBytesPerEntry =
1416+
(wfResult.format == WaveformFormat::ThreeBand) ? 3 : 6;
13771417
}
13781418
}
13791419
}
1380-
else
1420+
1421+
// Mark waveform as attempted so isFullyCached() returns true
1422+
// and we don't re-request indefinitely for tracks without
1423+
// waveform data (e.g. non-rekordbox media).
13811424
{
1382-
errorCount.fetch_add(1, std::memory_order_relaxed);
1383-
DBG("DbServerClient: metadata query failed for trackId="
1384-
+ juce::String(req.trackId));
1385-
// Connection may be stale -- close so next request reconnects
1386-
if (!conn->isConnected())
1387-
conn->close();
1425+
const juce::SpinLock::ScopedLockType lock(cacheLock);
1426+
auto it = metadataCache.find(cacheKey);
1427+
if (it != metadataCache.end())
1428+
it->second.waveformQueried = true;
13881429
}
13891430
}
13901431
else if (req.artworkId != 0)

LinkBridge.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,16 @@ class LinkBridge
8888
{
8989
link.enable(enabled);
9090
enabledFlag.store(enabled, std::memory_order_relaxed);
91-
if (!enabled)
91+
if (enabled)
92+
{
93+
// Force the next setTempo() to commit immediately, even if the
94+
// BPM happens to match what we last sent. Without this, re-enabling
95+
// Link after a disable leaves the session at 120 BPM (SDK default)
96+
// because lastCommittedBpm still holds the old value and the
97+
// hysteresis check suppresses the "identical" commit.
98+
lastCommittedBpm = 0.0;
99+
}
100+
else
92101
{
93102
peerCount.store(0, std::memory_order_relaxed);
94103
lastCommittedBpm = 0.0;

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.7.0"; }
14+
const juce::String getApplicationVersion() override { return "1.7.1"; }
1515
bool moreThanOneInstanceAllowed() override { return false; }
1616

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

MainComponent.cpp

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,24 @@ MainComponent::MainComponent()
999999
saveSettings();
10001000
};
10011001

1002+
rightContent.addAndMakeVisible(sldTcnetOffset);
1003+
sldTcnetOffset.setSliderStyle(juce::Slider::LinearHorizontal);
1004+
sldTcnetOffset.setTextBoxStyle(juce::Slider::TextBoxRight, false, 52, 20);
1005+
sldTcnetOffset.setRange(-1000.0, 1000.0, 1.0);
1006+
sldTcnetOffset.setValue(0.0, juce::dontSendNotification);
1007+
sldTcnetOffset.setTextValueSuffix(" ms");
1008+
sldTcnetOffset.setDoubleClickReturnValue(true, 0.0);
1009+
sldTcnetOffset.setColour(juce::Slider::backgroundColourId, juce::Colour(0xFF1A1D23));
1010+
sldTcnetOffset.setColour(juce::Slider::trackColourId, juce::Colour(0xFF37474F));
1011+
sldTcnetOffset.setColour(juce::Slider::thumbColourId, juce::Colour(0xFF78909C));
1012+
sldTcnetOffset.setColour(juce::Slider::textBoxTextColourId, textBright);
1013+
sldTcnetOffset.setColour(juce::Slider::textBoxBackgroundColourId, juce::Colour(0xFF1A1D23));
1014+
sldTcnetOffset.setColour(juce::Slider::textBoxOutlineColourId, borderCol);
1015+
rightContent.addAndMakeVisible(lblTcnetOffset);
1016+
lblTcnetOffset.setText("TCNET OFFSET:", juce::dontSendNotification);
1017+
styleLabel(lblTcnetOffset);
1018+
sldTcnetOffset.onValueChange = [this] { if (!syncing && !isShowLockedRevert()) { currentEngine().setTcnetOutputOffsetMs((int)sldTcnetOffset.getValue()); saveSettings(); } };
1019+
10021020
addRightLabelAndCombo(lblAudioOutputTypeFilter, cmbAudioOutputTypeFilter, "AUDIO DRIVER:");
10031021
cmbAudioOutputTypeFilter.onChange = [this]
10041022
{
@@ -1507,6 +1525,7 @@ void MainComponent::syncUIFromEngine()
15071525
sldMtcOffset.setValue(eng.getMtcOutputOffset(), juce::dontSendNotification);
15081526
sldArtnetOffset.setValue(eng.getArtnetOutputOffset(), juce::dontSendNotification);
15091527
sldLtcOffset.setValue(eng.getLtcOutputOffset(), juce::dontSendNotification);
1528+
sldTcnetOffset.setValue(eng.getTcnetOutputOffsetMs(), juce::dontSendNotification);
15101529

15111530
// Gains
15121531
sldLtcInputGain.setValue(eng.getLtcInput().getInputGain() * 100.0f, juce::dontSendNotification);
@@ -3126,6 +3145,7 @@ void MainComponent::loadAndApplyNonAudioSettings()
31263145
eng.setMtcOutputOffset(es.mtcOutputOffset);
31273146
eng.setArtnetOutputOffset(es.artnetOutputOffset);
31283147
eng.setLtcOutputOffset(es.ltcOutputOffset);
3148+
eng.setTcnetOutputOffsetMs(es.tcnetOutputOffsetMs);
31293149

31303150
eng.getLtcInput().setInputGain((float)es.ltcInputGain / 100.0f);
31313151
eng.getLtcInput().setPassthruGain((float)es.thruInputGain / 100.0f);
@@ -3399,6 +3419,7 @@ void MainComponent::flushSettings()
33993419
es.mtcOutputOffset = eng.getMtcOutputOffset();
34003420
es.artnetOutputOffset = eng.getArtnetOutputOffset();
34013421
es.ltcOutputOffset = eng.getLtcOutputOffset();
3422+
es.tcnetOutputOffsetMs = eng.getTcnetOutputOffsetMs();
34023423

34033424
es.ltcInputGain = (int)(eng.getLtcInput().getInputGain() * 100.0f);
34043425
es.thruInputGain = (int)(eng.getLtcInput().getPassthruGain() * 100.0f);
@@ -3888,6 +3909,7 @@ void MainComponent::updateDeviceSelectorVisibility()
38883909
bool showTcnetConfig = eng.isOutputTcnetEnabled();
38893910
cmbTcnetInterface.setVisible(showTcnetConfig); lblTcnetInterface.setVisible(showTcnetConfig);
38903911
cmbTcnetLayer.setVisible(showTcnetConfig); lblTcnetLayer.setVisible(showTcnetConfig);
3912+
sldTcnetOffset.setVisible(showTcnetConfig); lblTcnetOffset.setVisible(showTcnetConfig);
38913913
if (showTcnetConfig) repopulateTcnetLayerCombo();
38923914

38933915
bool anyDevice = (input != SrcType::SystemTime) || eng.isOutputMtcEnabled() || eng.isOutputArtnetEnabled() || eng.isOutputLtcEnabled() || (eng.isPrimary() && eng.isOutputThruEnabled());
@@ -4576,6 +4598,7 @@ void MainComponent::resized()
45764598
btnTcnetOut.setBounds(row); rp.removeFromTop(2);
45774599
if (cmbTcnetInterface.isVisible()) layCombo(lblTcnetInterface, cmbTcnetInterface, rp);
45784600
if (cmbTcnetLayer.isVisible()) layCombo(lblTcnetLayer, cmbTcnetLayer, rp);
4601+
if (sldTcnetOffset.isVisible()) laySlider(lblTcnetOffset, sldTcnetOffset, rp);
45794602
rp.removeFromTop(2);
45804603
}
45814604

@@ -4743,7 +4766,8 @@ void MainComponent::timerCallback()
47434766
eng.isSourceActive(),
47444767
onAirFader,
47454768
beatInBar,
4746-
bpm100);
4769+
bpm100,
4770+
eng.getTcnetOutputOffsetMs());
47474771

47484772
// Feed track metadata for Resolume unicast.
47494773
// DJ sources: real artist + title from CDJ/Denon.
@@ -4964,7 +4988,18 @@ void MainComponent::timerCallback()
49644988
}
49654989

49664990
// Phase 3: update color waveform from DbServerClient cache
4991+
//
4992+
// IMPORTANT: Use the SOURCE player's IP (the CDJ that owns the media),
4993+
// not the deck's own IP. When CDJ 2 loads a track from CDJ 1's USB
4994+
// via Link export, the cache entry is keyed by CDJ 1's IP (set by
4995+
// TimecodeEngine::requestDbMetadata). Looking up with CDJ 2's IP
4996+
// would miss the cache entry and the waveform would never load.
49674997
uint32_t wfTrackId = trackInfo.trackId;
4998+
uint8_t wfSrcPlayer = sharedProDJLinkInput.getLoadedPlayer(pdlPlayer);
4999+
if (wfSrcPlayer == 0) wfSrcPlayer = (uint8_t)pdlPlayer;
5000+
juce::String srcIP = sharedProDJLinkInput.getPlayerIP((int)wfSrcPlayer);
5001+
if (srcIP.isEmpty()) srcIP = sharedProDJLinkInput.getPlayerIP(pdlPlayer);
5002+
49685003
if (wfTrackId != 0 && wfTrackId != displayedWaveformTrackId)
49695004
{
49705005
// Track changed -- clear old waveform immediately (avoids stale cursor)
@@ -4975,8 +5010,7 @@ void MainComponent::timerCallback()
49755010
displayedWaveformTrackId = wfTrackId;
49765011

49775012
// Try to populate from cache (may not have waveform yet)
4978-
juce::String pdlIP = sharedProDJLinkInput.getPlayerIP(pdlPlayer);
4979-
auto meta = sharedDbClient.getCachedMetadata(pdlIP, wfTrackId);
5013+
auto meta = sharedDbClient.getCachedMetadata(srcIP, wfTrackId);
49805014
if (meta.hasWaveform())
49815015
{
49825016
waveformDisplay.setColorWaveformData(meta.waveformData,
@@ -4986,8 +5020,7 @@ void MainComponent::timerCallback()
49865020
else if (wfTrackId != 0 && !waveformDisplay.hasWaveformData())
49875021
{
49885022
// Waveform not yet loaded -- retry from cache (async: arrives after metadata)
4989-
juce::String pdlIP = sharedProDJLinkInput.getPlayerIP(pdlPlayer);
4990-
auto meta = sharedDbClient.getCachedMetadata(pdlIP, wfTrackId);
5023+
auto meta = sharedDbClient.getCachedMetadata(srcIP, wfTrackId);
49915024
if (meta.hasWaveform())
49925025
{
49935026
waveformDisplay.setColorWaveformData(meta.waveformData,

MainComponent.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ class MainComponent : public juce::Component,
332332
juce::ToggleButton btnTcnetOut { "TCNET OUT" };
333333
juce::ComboBox cmbTcnetInterface; juce::Label lblTcnetInterface;
334334
juce::ComboBox cmbTcnetLayer; juce::Label lblTcnetLayer;
335+
GainSlider sldTcnetOffset; juce::Label lblTcnetOffset;
335336
int showLockFlashCountdown = 0; // ticks remaining for flash feedback
336337

337338
/// Returns true if Show Lock is active and the action should be blocked.

0 commit comments

Comments
 (0)