Skip to content

Commit c91b649

Browse files
authored
Update V1.8.2
1 parent ebfbb87 commit c91b649

8 files changed

Lines changed: 154 additions & 29 deletions

File tree

AppSettings.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,37 @@ class TrackMap
440440
return entries.count(TrackMapEntry::makeKey(artist, title, dur)) > 0;
441441
}
442442

443+
/// Find by artist+title, ignoring duration. Used as a last-resort fallback
444+
/// when the caller's duration doesn't match the entry's saved duration.
445+
const TrackMapEntry* findIgnoringDuration(const juce::String& artist,
446+
const juce::String& title) const
447+
{
448+
auto base = TrackMapEntry::makeKey(artist, title, 0); // key without duration
449+
// Exact match (entry saved without duration)
450+
auto it = entries.find(base);
451+
if (it != entries.end()) return &it->second;
452+
// Prefix match (entry saved with some duration: "base|NNN")
453+
auto prefix = base + "|";
454+
for (auto& [k, v] : entries)
455+
if (k.size() > prefix.size() && k.substr(0, prefix.size()) == prefix)
456+
return &v;
457+
return nullptr;
458+
}
459+
460+
/// Mutable version of findIgnoringDuration
461+
TrackMapEntry* findIgnoringDuration(const juce::String& artist,
462+
const juce::String& title)
463+
{
464+
auto base = TrackMapEntry::makeKey(artist, title, 0);
465+
auto it = entries.find(base);
466+
if (it != entries.end()) return &it->second;
467+
auto prefix = base + "|";
468+
for (auto& [k, v] : entries)
469+
if (k.size() > prefix.size() && k.substr(0, prefix.size()) == prefix)
470+
return &v;
471+
return nullptr;
472+
}
473+
443474
//------------------------------------------------------------------
444475
// Mutation
445476
//------------------------------------------------------------------

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

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

MainComponent.cpp

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6042,6 +6042,10 @@ void MainComponent::updateBpmMultButtonStates()
60426042
{
60436043
auto* entry = settings.trackMap.find(trackInfo.artist, trackInfo.title,
60446044
trackInfo.durationSec);
6045+
if (!entry && trackInfo.durationSec > 0)
6046+
entry = settings.trackMap.find(trackInfo.artist, trackInfo.title, 0);
6047+
if (!entry)
6048+
entry = settings.trackMap.findIgnoringDuration(trackInfo.artist, trackInfo.title);
60456049
if (entry != nullptr) map = entry->bpmMultiplier;
60466050
}
60476051

@@ -6088,6 +6092,10 @@ void MainComponent::saveBpmMultToTrackMap(int clickedMult)
60886092
if (info.title.isEmpty()) return;
60896093

60906094
auto* entry = settings.trackMap.find(info.artist, info.title, info.durationSec);
6095+
if (!entry && info.durationSec > 0)
6096+
entry = settings.trackMap.find(info.artist, info.title, 0);
6097+
if (!entry)
6098+
entry = settings.trackMap.findIgnoringDuration(info.artist, info.title);
60916099
int currentMapValue = (entry != nullptr) ? entry->bpmMultiplier : 0;
60926100

60936101
// Double-click on 1x: clear saved value. Otherwise: save (no toggle).
@@ -6111,6 +6119,37 @@ void MainComponent::saveBpmMultToTrackMap(int clickedMult)
61116119
newEntry.title = info.title;
61126120
newEntry.durationSec = info.durationSec;
61136121
newEntry.bpmMultiplier = newValue;
6122+
6123+
// Auto-populate cue points from rekordbox if available
6124+
if (info.trackId != 0)
6125+
{
6126+
auto meta = sharedDbClient.getCachedMetadataByTrackId(info.trackId);
6127+
if (meta.isValid() && !meta.cueList.empty())
6128+
{
6129+
for (auto& rc : meta.cueList)
6130+
{
6131+
if (rc.positionMs == 0) continue;
6132+
CuePoint cp;
6133+
cp.positionMs = rc.positionMs;
6134+
auto letter = rc.hotCueLetter();
6135+
if (letter.isNotEmpty())
6136+
cp.name = letter;
6137+
if (rc.comment.isNotEmpty())
6138+
cp.name += cp.name.isNotEmpty()
6139+
? " " + rc.comment : rc.comment;
6140+
if (cp.name.isEmpty())
6141+
{
6142+
if (rc.type == TrackMetadata::RekordboxCue::MemoryPoint)
6143+
cp.name = "MEM";
6144+
else if (rc.type == TrackMetadata::RekordboxCue::Loop)
6145+
cp.name = "LOOP";
6146+
}
6147+
newEntry.cuePoints.push_back(std::move(cp));
6148+
}
6149+
newEntry.sortCuePoints();
6150+
}
6151+
}
6152+
61146153
settings.trackMap.addOrUpdate(newEntry);
61156154
}
61166155
else

ProDJLinkView.h

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,10 @@ class ProDJLinkViewComponent : public juce::Component,
308308

309309
int dur = (int)ds.trackLenSec;
310310
auto* entry = trackMap.find(ds.artist, ds.title, dur);
311+
if (!entry && dur > 0)
312+
entry = trackMap.find(ds.artist, ds.title, 0);
313+
if (!entry)
314+
entry = trackMap.findIgnoringDuration(ds.artist, ds.title);
311315
int currentMapValue = (entry != nullptr) ? entry->bpmMultiplier : 0;
312316

313317
// Double-click on 1x: clear saved value. Otherwise: save (no toggle).
@@ -330,6 +334,37 @@ class ProDJLinkViewComponent : public juce::Component,
330334
newEntry.title = ds.title;
331335
newEntry.durationSec = dur;
332336
newEntry.bpmMultiplier = newValue;
337+
338+
// Auto-populate cue points from rekordbox if available
339+
if (ds.trackId != 0)
340+
{
341+
auto meta = dbClient.getCachedMetadataByTrackId(ds.trackId);
342+
if (meta.isValid() && !meta.cueList.empty())
343+
{
344+
for (auto& rc : meta.cueList)
345+
{
346+
if (rc.positionMs == 0) continue;
347+
CuePoint cp;
348+
cp.positionMs = rc.positionMs;
349+
auto letter = rc.hotCueLetter();
350+
if (letter.isNotEmpty())
351+
cp.name = letter;
352+
if (rc.comment.isNotEmpty())
353+
cp.name += cp.name.isNotEmpty()
354+
? " " + rc.comment : rc.comment;
355+
if (cp.name.isEmpty())
356+
{
357+
if (rc.type == TrackMetadata::RekordboxCue::MemoryPoint)
358+
cp.name = "MEM";
359+
else if (rc.type == TrackMetadata::RekordboxCue::Loop)
360+
cp.name = "LOOP";
361+
}
362+
newEntry.cuePoints.push_back(std::move(cp));
363+
}
364+
newEntry.sortCuePoints();
365+
}
366+
}
367+
333368
trackMap.addOrUpdate(newEntry);
334369
}
335370
else
@@ -459,10 +494,13 @@ class ProDJLinkViewComponent : public juce::Component,
459494
ds.trackMapped = false;
460495
ds.offset = "00:00:00:00";
461496
ds.offsetTimecode = {};
462-
if (ds.title.isNotEmpty()
463-
&& !ds.title.startsWith("Track #"))
497+
if (ds.title.isNotEmpty())
464498
{
465499
tmEntry = trackMap.find(ds.artist, ds.title, (int)ds.trackLenSec);
500+
if (!tmEntry && ds.trackLenSec > 0)
501+
tmEntry = trackMap.find(ds.artist, ds.title, 0);
502+
if (!tmEntry)
503+
tmEntry = trackMap.findIgnoringDuration(ds.artist, ds.title);
466504
if (tmEntry != nullptr)
467505
{
468506
ds.trackMapped = true;
@@ -568,8 +606,8 @@ class ProDJLinkViewComponent : public juce::Component,
568606
if (meta.isValid())
569607
{
570608
ds.lastMetaVersion = meta.cacheVersion;
571-
ds.artist = meta.artist;
572-
ds.title = meta.title;
609+
if (meta.artist.isNotEmpty()) ds.artist = meta.artist;
610+
if (meta.title.isNotEmpty()) ds.title = meta.title;
573611
ds.key = meta.key;
574612
ds.artworkId = meta.artworkId;
575613
ds.cueCount = (int)meta.cueList.size();
@@ -638,8 +676,8 @@ class ProDJLinkViewComponent : public juce::Component,
638676
if (metaById.isValid())
639677
{
640678
ds.lastMetaVersion = metaById.cacheVersion;
641-
ds.artist = metaById.artist;
642-
ds.title = metaById.title;
679+
if (metaById.artist.isNotEmpty()) ds.artist = metaById.artist;
680+
if (metaById.title.isNotEmpty()) ds.title = metaById.title;
643681
ds.key = metaById.key;
644682
ds.artworkId = metaById.artworkId;
645683
ds.cueCount = (int)metaById.cueList.size();
@@ -962,6 +1000,10 @@ class ProDJLinkViewComponent : public juce::Component,
9621000
{
9631001
int dur = (int)ds.trackLenSec;
9641002
auto* mutableEntry = trackMap.find(ds.artist, ds.title, dur);
1003+
if (!mutableEntry && dur > 0)
1004+
mutableEntry = trackMap.find(ds.artist, ds.title, 0);
1005+
if (!mutableEntry)
1006+
mutableEntry = trackMap.findIgnoringDuration(ds.artist, ds.title);
9651007
if (mutableEntry != nullptr && mutableEntry->cuePoints.empty())
9661008
{
9671009
for (auto& rc : meta.cueList)
@@ -993,6 +1035,10 @@ class ProDJLinkViewComponent : public juce::Component,
9931035
// Cues not yet from rekordbox -- fallback to TrackMap
9941036
int dur = (int)ds.trackLenSec;
9951037
auto* tmFallback = trackMap.find(ds.artist, ds.title, dur);
1038+
if (!tmFallback && dur > 0)
1039+
tmFallback = trackMap.find(ds.artist, ds.title, 0);
1040+
if (!tmFallback)
1041+
tmFallback = trackMap.findIgnoringDuration(ds.artist, ds.title);
9961042
if (tmFallback && !tmFallback->cuePoints.empty())
9971043
ds.detailWaveform.setCuePoints(tmFallback->cuePoints);
9981044
}

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ STC connects to Denon Engine OS hardware via the StageLinQ protocol, receiving d
129129

130130
### Track Map
131131

132-
Map tracks by **artist + title + duration** to timecode offsets and show control triggers. When a mapped track is loaded on a CDJ or Denon deck, STC automatically applies the timecode offset and fires the configured triggers. Tracks are identified universally regardless of which USB/SD they are loaded from. The duration acts as a fingerprint to distinguish different versions of the same track (e.g. radio edit vs extended mix).
132+
Map tracks by **title** (and optionally artist and duration) to timecode offsets and show control triggers. When a mapped track is loaded on a CDJ or Denon deck, STC automatically applies the timecode offset and fires the configured triggers. Tracks are identified universally regardless of which USB/SD they are loaded from. Artist is optional -- tracks without artist metadata (sound effects, jingles, DJ tools) work fully with TrackMap. The duration acts as a fingerprint to distinguish different versions of the same track (e.g. radio edit vs extended mix).
133133

134134
- Per-track timecode offset (HH:MM:SS:FF)
135135
- Per-track BPM multiplier (/4, /2, 1x, x2, x4) -- applied to MIDI Clock, Ableton Link, and OSC BPM forward
@@ -159,7 +159,7 @@ Per-track timed triggers that fire at specific playhead positions during playbac
159159
- **Live editing:** cue points added or modified while a track is playing take effect immediately without reloading the track.
160160
- **Waveform and artwork cache:** waveform preview data and album artwork are saved to disk the first time a track is seen. The cue editor shows both even when the CDJ is not connected, enabling offline cue programming.
161161
- **Works with both Pioneer and Denon** hardware via Pro DJ Link and StageLinQ.
162-
- **Auto-populate from rekordbox:** when a track with a TrackMap entry loads on a CDJ, STC automatically imports the DJ's rekordbox hot cues, memory points, and loops as cue points -- with their letters and comments as labels. Only applies if the entry has no manually-configured cue points. Blocked during Show Lock.
162+
- **Auto-populate from rekordbox:** when a track with a TrackMap entry loads on a CDJ, STC automatically imports the DJ's rekordbox hot cues, memory points, and loops as cue points -- with their letters and comments as labels. Also applies when creating a new entry via BPM multiplier double-click. Only applies if the entry has no manually-configured cue points. Blocked during Show Lock.
163163
- Cue points are stored in the Track Map (trackmap.json) alongside the track's offset and track-change triggers.
164164
- Maximizable window with persisted position across sessions.
165165
- Blocked during Show Lock to prevent accidental changes during a live show.
@@ -185,7 +185,7 @@ Table editor with per-parameter enable/disable, editable addresses and CC/Note/D
185185

186186
### PDL View
187187

188-
External window showing the full Pro DJ Link network state at 60Hz:
188+
External window showing the full Pro DJ Link network state at 60Hz. The layout uses priority-based sizing: info text stays readable at any window size, bottom chrome (map/engine/BPM mult rows) hides progressively on small decks, and the detail waveform collapses first when space is tight.
189189

190190
- 4-deck display (2x2 grid or 4x1 horizontal): artwork, track info, BPM (with multiplied value when active), key, cue count, play state, pitch, engine assignments
191191
- **Preview waveform** with playhead cursor, rekordbox cue markers (colored by DJ assignment), minute markers, beat grid (downbeat lines), and stored loop overlays

StageLinQView.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,10 @@ class StageLinQViewComponent : public juce::Component,
240240
if (ds.title.isNotEmpty())
241241
{
242242
tmEntry = trackMap.find(ds.artist, ds.title, (int)ds.trackLenSec);
243+
if (!tmEntry && ds.trackLenSec > 0)
244+
tmEntry = trackMap.find(ds.artist, ds.title, 0);
245+
if (!tmEntry)
246+
tmEntry = trackMap.findIgnoringDuration(ds.artist, ds.title);
243247
if (tmEntry != nullptr)
244248
{
245249
ds.trackMapped = true;

TimecodeEngine.h

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -555,8 +555,8 @@ class TimecodeEngine
555555
auto meta = dbClient->getCachedMetadataLightById(id);
556556
if (meta.isValid())
557557
{
558-
cachedTrackArtist = meta.artist;
559-
cachedTrackTitle = meta.title;
558+
if (meta.artist.isNotEmpty()) cachedTrackArtist = meta.artist;
559+
if (meta.title.isNotEmpty()) cachedTrackTitle = meta.title;
560560
if (meta.durationSeconds > 0)
561561
{
562562
cachedTrackDurationSec = meta.durationSeconds;
@@ -630,8 +630,8 @@ class TimecodeEngine
630630
// Ensure artist/title are from dbClient if available
631631
if (info.artist.isEmpty() || info.title.startsWith("Track #"))
632632
{
633-
info.artist = meta.artist;
634-
info.title = meta.title;
633+
if (meta.artist.isNotEmpty()) info.artist = meta.artist;
634+
if (meta.title.isNotEmpty()) info.title = meta.title;
635635
}
636636
}
637637
}
@@ -1013,9 +1013,9 @@ class TimecodeEngine
10131013
&& cachedTrackTitle.startsWith("Track #"))
10141014
{
10151015
auto meta = dbClient->getCachedMetadataLightById(cachedTrackId);
1016-
if (meta.isValid())
1016+
if (meta.isValid() && meta.title.isNotEmpty())
10171017
{
1018-
cachedTrackArtist = meta.artist;
1018+
if (meta.artist.isNotEmpty()) cachedTrackArtist = meta.artist;
10191019
cachedTrackTitle = meta.title;
10201020
if (meta.durationSeconds > 0)
10211021
{
@@ -1123,8 +1123,7 @@ class TimecodeEngine
11231123
}
11241124

11251125
if (pdlHasTC && trackMapEnabled
1126-
&& cachedTrackArtist.isNotEmpty() && cachedTrackTitle.isNotEmpty()
1127-
&& !cachedTrackTitle.startsWith("Track #"))
1126+
&& cachedTrackTitle.isNotEmpty())
11281127
{
11291128
if (trackMapped)
11301129
inputStatusText += " | MAP: " + cachedTrackTitle;
@@ -1384,7 +1383,7 @@ class TimecodeEngine
13841383
}
13851384

13861385
if (slqHasTC && trackMapEnabled
1387-
&& cachedTrackArtist.isNotEmpty() && cachedTrackTitle.isNotEmpty())
1386+
&& cachedTrackTitle.isNotEmpty())
13881387
{
13891388
if (trackMapped)
13901389
inputStatusText += " | MAP: " + cachedTrackTitle;
@@ -2336,8 +2335,7 @@ class TimecodeEngine
23362335
/// Returns the matched entry pointer (or nullptr if not found/malformed).
23372336
const TrackMapEntry* lookupTrackInMap()
23382337
{
2339-
if (!trackMapPtr || cachedTrackTitle.isEmpty()
2340-
|| cachedTrackTitle.startsWith("Track #"))
2338+
if (!trackMapPtr || cachedTrackTitle.isEmpty())
23412339
{
23422340
trackMapped = false;
23432341
return nullptr;
@@ -2353,6 +2351,14 @@ class TimecodeEngine
23532351
// don't lose the TrackMap match (and its armed cue points).
23542352
if (!entry && cachedTrackDurationSec > 0)
23552353
entry = trackMapPtr->find(cachedTrackArtist, cachedTrackTitle, 0);
2354+
2355+
// Last resort: ignore duration entirely. Catches the case where the
2356+
// entry was saved with a duration that differs from the engine's cached
2357+
// duration (e.g. PDL View saved with CDJ-reported duration while the
2358+
// engine's cachedTrackDurationSec was still 0 or stale from an earlier
2359+
// enrichment pass).
2360+
if (!entry)
2361+
entry = trackMapPtr->findIgnoringDuration(cachedTrackArtist, cachedTrackTitle);
23562362
if (entry)
23572363
{
23582364
int h, m, s, f;

0 commit comments

Comments
 (0)