Skip to content

Commit a278ad2

Browse files
authored
Update V1.5.3
1 parent d603d5f commit a278ad2

14 files changed

Lines changed: 570 additions & 316 deletions

ArtnetOutput.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ class ArtnetOutput : public juce::HighResolutionTimer
338338
std::atomic<FrameRate> currentFps { FrameRate::FPS_25 };
339339
std::atomic<double> lastFrameSendTime { 0.0 };
340340
std::atomic<uint32_t> sendErrors { 0 };
341-
uint8_t dmxSequence = 0; // incrementing 1-255 for OpDmx sequencing
341+
uint8_t dmxSequence = 0; // incrementing 1-255 for OpDmx sequencing (message-thread-only currently, but atomic-safe for future use)
342342

343343
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ArtnetOutput)
344344
};

DbServerClient.h

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,45 @@ class DbServerClient : private juce::Thread
219219
return {};
220220
}
221221

222+
/// Lightweight metadata lookup -- returns text fields and IDs only,
223+
/// skipping the waveform vector copy. Use when you only need artist/title/
224+
/// key/BPM/artworkId (e.g. from getActiveTrackInfo at 60Hz).
225+
struct MetadataLight
226+
{
227+
uint32_t trackId = 0;
228+
juce::String title, artist, album, genre, key;
229+
int bpmTimes100 = 0;
230+
uint32_t artworkId = 0;
231+
int durationSeconds = 0;
232+
bool valid = false;
233+
bool isValid() const { return valid; }
234+
};
235+
236+
MetadataLight getCachedMetadataLightById(uint32_t trackId) const
237+
{
238+
if (trackId == 0) return {};
239+
const juce::SpinLock::ScopedLockType lock(cacheLock);
240+
for (auto& [key, meta] : metadataCache)
241+
{
242+
if (meta.trackId == trackId && meta.isValid())
243+
{
244+
MetadataLight m;
245+
m.trackId = meta.trackId;
246+
m.title = meta.title;
247+
m.artist = meta.artist;
248+
m.album = meta.album;
249+
m.genre = meta.genre;
250+
m.key = meta.key;
251+
m.bpmTimes100 = meta.bpmTimes100;
252+
m.artworkId = meta.artworkId;
253+
m.durationSeconds = meta.durationSeconds;
254+
m.valid = true;
255+
return m;
256+
}
257+
}
258+
return {};
259+
}
260+
222261
//==========================================================================
223262
// Retrieve cached artwork (returns null Image if not cached)
224263
//==========================================================================

LtcOutput.h

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ class LtcOutput : private juce::AudioIODeviceCallback
110110
/// Call when resuming from pause to avoid continuing a stale half-encoded frame.
111111
void reseed()
112112
{
113-
needNewFrame = true;
114-
encoderSeeded = false;
113+
needNewFrame.store(true, std::memory_order_relaxed);
114+
encoderSeeded.store(false, std::memory_order_relaxed);
115115
}
116116

117117
void setPaused(bool p)
@@ -148,30 +148,34 @@ class LtcOutput : private juce::AudioIODeviceCallback
148148
std::atomic<float> peakLevel { 0.0f };
149149
std::atomic<double> pitchMultiplier { 1.0 };
150150

151-
// LTC encoder state -- audio-callback-thread-only (no synchronisation needed)
151+
// LTC encoder state -- mostly audio-callback-thread-only.
152+
// EXCEPTION: needNewFrame and encoderSeeded are also written by reseed()
153+
// from the message thread, so they must be atomic to avoid data races
154+
// (especially on ARM / Apple Silicon where non-atomic cross-thread writes
155+
// can produce torn reads).
152156
static constexpr int LTC_FRAME_BITS = 80;
153157
uint8_t frameBits[LTC_FRAME_BITS] = {};
154158
int currentBitIndex = 0;
155159
int halfCellIndex = 0;
156160
double samplePositionInHalfBit = 0.0;
157161
double samplesPerHalfBit = 0.0;
158162
float currentLevel = 1.0f;
159-
bool needNewFrame = true;
163+
std::atomic<bool> needNewFrame { true };
160164
static constexpr float baseAmplitude = 0.8f;
161165

162166
// Auto-increment state: the encoder maintains its own running timecode
163167
// to avoid repeating frames when the UI thread lags behind the audio clock
164168
Timecode encoderTc;
165-
bool encoderSeeded = false;
169+
std::atomic<bool> encoderSeeded { false };
166170

167171
void resetEncoder()
168172
{
169173
currentBitIndex = 0;
170174
halfCellIndex = 0;
171175
samplePositionInHalfBit = 0.0;
172176
currentLevel = 1.0f;
173-
needNewFrame = true;
174-
encoderSeeded = false;
177+
needNewFrame.store(true, std::memory_order_relaxed);
178+
encoderSeeded.store(false, std::memory_order_relaxed);
175179
encoderTc = Timecode();
176180
updateSamplesPerBit();
177181
}
@@ -190,10 +194,10 @@ class LtcOutput : private juce::AudioIODeviceCallback
190194
FrameRate fps = pendingFps.load(std::memory_order_relaxed);
191195
Timecode pendingTc = unpackTimecode(packedPendingTc.load(std::memory_order_relaxed));
192196

193-
if (!encoderSeeded)
197+
if (!encoderSeeded.load(std::memory_order_relaxed))
194198
{
195199
encoderTc = pendingTc;
196-
encoderSeeded = true;
200+
encoderSeeded.store(true, std::memory_order_relaxed);
197201
}
198202
else
199203
{
@@ -320,14 +324,14 @@ class LtcOutput : private juce::AudioIODeviceCallback
320324
float peak = 0.0f;
321325
for (int i = 0; i < numSamples; ++i)
322326
{
323-
if (needNewFrame)
327+
if (needNewFrame.load(std::memory_order_relaxed))
324328
{
325329
updateSamplesPerBit();
326330
packFrame();
327331
currentBitIndex = 0;
328332
halfCellIndex = 0;
329333
samplePositionInHalfBit = 0.0;
330-
needNewFrame = false;
334+
needNewFrame.store(false, std::memory_order_relaxed);
331335
// Do NOT invert currentLevel here -- the mandatory start-of-bit
332336
// transition for bit 0 was already applied when the previous
333337
// frame's last bit completed (halfCellIndex 1 -> 0 branch).
@@ -362,7 +366,7 @@ class LtcOutput : private juce::AudioIODeviceCallback
362366
currentLevel = -currentLevel;
363367

364368
if (currentBitIndex >= LTC_FRAME_BITS)
365-
needNewFrame = true;
369+
needNewFrame.store(true, std::memory_order_relaxed);
366370
}
367371
}
368372
}

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

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

MainComponent.cpp

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -939,26 +939,33 @@ MainComponent::MainComponent()
939939
startTimerHz(60);
940940
startAudioDeviceScan();
941941

942-
// GPU-accelerated rendering (Windows only).
943-
// On Windows, JUCE's OpenGL context offloads image compositing from GDI
944-
// to the GPU, reducing message-thread load from repaint().
945-
// On macOS, this is DISABLED: JUCE still renders into software images
946-
// and then uploads them as textures through Apple's deprecated
947-
// OpenGL-to-Metal translation layer, which adds overhead rather than
948-
// reducing it. CoreGraphics already uses Metal internally for
949-
// compositing, so native rendering is faster without OpenGL.
950-
#if JUCE_WINDOWS
951-
glContext.attachTo(*this);
952-
#endif
942+
// GPU-accelerated rendering: DISABLED for thread safety.
943+
//
944+
// When glContext.attachTo(*this) is active, JUCE calls paint() for ALL
945+
// child components on the OpenGL thread -- not the message thread.
946+
// Meanwhile, timerCallback() writes juce::String members (artist, title,
947+
// playState, sourceName, etc.) on the message thread. juce::String is
948+
// reference-counted: a concurrent read during write can corrupt the
949+
// refcount and crash. This affects TimecodeDisplay, ProDJLinkView,
950+
// all juce::Label instances, and any component that reads String data
951+
// in paint().
952+
//
953+
// JUCE's OpenGL renderer paints into software images on the CPU and
954+
// only uses the GPU for the final texture upload + composite. With
955+
// the waveform image cache, HiDPI deck image cache, and targeted
956+
// dirty-rect repainting already in place, the performance difference
957+
// is negligible. Windows DWM already hardware-accelerates the native
958+
// GDI composite path.
959+
//
960+
// For a live performance application, eliminating the entire class of
961+
// GL-thread data races is worth more than the marginal compositing
962+
// speedup. If GPU acceleration is ever re-enabled, all juce::String
963+
// members read in paint() must be replaced with atomic-safe types
964+
// (fixed char arrays, packed atomics, or a SpinLock-protected snapshot).
953965
}
954966

955967
MainComponent::~MainComponent()
956968
{
957-
// 0. Detach OpenGL before destroying any components (Windows only)
958-
#if JUCE_WINDOWS
959-
glContext.detach();
960-
#endif
961-
962969
// 1. Stop our UI timer first -- no more timerCallback() after this
963970
stopTimer();
964971

@@ -1021,9 +1028,15 @@ MainComponent::~MainComponent()
10211028
// 9. Explicitly shut down each engine (timers, threads, sockets)
10221029
// BEFORE engines.clear() destroys the objects, so all HighResolutionTimer
10231030
// threads are stopped while the message manager is still alive.
1031+
// Also disconnect shared pointers (TrackMap, MixerMap, ProDJLink, DbServer)
1032+
// so no stale references survive into AppSettings destruction.
10241033
for (auto& eng : engines)
10251034
{
10261035
eng->setMidiClockEnabled(false);
1036+
eng->setTrackMap(nullptr);
1037+
eng->setMixerMap(nullptr);
1038+
eng->setSharedProDJLinkInput(nullptr);
1039+
eng->setDbServerClient(nullptr);
10271040
eng->stopMtcOutput();
10281041
eng->stopArtnetOutput();
10291042
eng->stopLtcOutput();
@@ -1033,7 +1046,7 @@ MainComponent::~MainComponent()
10331046
eng->stopLtcInput();
10341047
}
10351048

1036-
// 9. Now safe to destroy engine objects
1049+
// 10. Now safe to destroy engine objects
10371050
engines.clear();
10381051
}
10391052

@@ -1083,6 +1096,12 @@ void MainComponent::removeEngine(int index)
10831096

10841097
// Explicitly stop all protocols on the engine being deleted BEFORE
10851098
// erasing it, so destructors don't race with any pending callbacks.
1099+
// Disconnect shared pointers first to prevent stale access during stop.
1100+
engines[(size_t)index]->setTrackMap(nullptr);
1101+
engines[(size_t)index]->setMixerMap(nullptr);
1102+
engines[(size_t)index]->setSharedProDJLinkInput(nullptr);
1103+
engines[(size_t)index]->setDbServerClient(nullptr);
1104+
engines[(size_t)index]->setMidiClockEnabled(false);
10861105
engines[(size_t)index]->getTriggerOutput().setSharedMidiOutput(nullptr);
10871106
engines[(size_t)index]->stopMtcOutput();
10881107
engines[(size_t)index]->stopArtnetOutput();
@@ -1732,7 +1751,7 @@ void MainComponent::openMixerMapEditor()
17321751
}
17331752

17341753
auto* editor = new MixerMapEditor(sharedMixerMap,
1735-
sharedProDJLinkInput.getMixerChannelCount() > 4);
1754+
djmModelFromString(sharedProDJLinkInput.getDJMModel()));
17361755
editor->onChange = [this]
17371756
{
17381757
sharedMixerMap.save();
@@ -3944,8 +3963,11 @@ void MainComponent::timerCallback()
39443963
// uses its own audio-callback-driven auto-increment, so it's similarly
39453964
// decoupled. If UI stalls (resize, modal dialog), outputs continue
39463965
// transmitting the last-known timecode until the next tick() updates it.
3947-
for (auto& eng : engines)
3948-
eng->tick();
3966+
for (int i = 0; i < (int)engines.size(); ++i)
3967+
{
3968+
engines[(size_t)i]->setStatusTextVisible(i == selectedEngine);
3969+
engines[(size_t)i]->tick();
3970+
}
39493971

39503972
// Update UI for selected engine
39513973
auto& eng = currentEngine();
@@ -4087,7 +4109,10 @@ void MainComponent::timerCallback()
40874109
{
40884110
// Track changed -- clear old waveform immediately (avoids stale cursor)
40894111
waveformDisplay.clearWaveform();
4090-
displayedWaveformTrackId = 0;
4112+
// Mark this track as "attempted" so we don't re-enter this block
4113+
// every frame. The retry path below uses hasWaveformData() to
4114+
// detect when the async waveform query completes.
4115+
displayedWaveformTrackId = wfTrackId;
40914116

40924117
// Try to populate from cache (may not have waveform yet)
40934118
juce::String pdlIP = sharedProDJLinkInput.getPlayerIP(pdlPlayer);
@@ -4096,10 +4121,9 @@ void MainComponent::timerCallback()
40964121
{
40974122
waveformDisplay.setColorWaveformData(meta.waveformData,
40984123
meta.waveformEntryCount, meta.waveformBytesPerEntry);
4099-
displayedWaveformTrackId = wfTrackId;
41004124
}
41014125
}
4102-
else if (wfTrackId != 0 && displayedWaveformTrackId == 0)
4126+
else if (wfTrackId != 0 && !waveformDisplay.hasWaveformData())
41034127
{
41044128
// Waveform not yet loaded -- retry from cache (async: arrives after metadata)
41054129
juce::String pdlIP = sharedProDJLinkInput.getPlayerIP(pdlPlayer);
@@ -4108,7 +4132,6 @@ void MainComponent::timerCallback()
41084132
{
41094133
waveformDisplay.setColorWaveformData(meta.waveformData,
41104134
meta.waveformEntryCount, meta.waveformBytesPerEntry);
4111-
displayedWaveformTrackId = wfTrackId;
41124135
}
41134136
}
41144137
else if (wfTrackId == 0 && displayedWaveformTrackId != 0)

MainComponent.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -480,9 +480,10 @@ class MainComponent : public juce::Component,
480480
int getArtNetAddressFromCombos(const juce::ComboBox& cmbNet, const juce::ComboBox& cmbSub,
481481
const juce::ComboBox& cmbUni);
482482

483-
#if JUCE_WINDOWS
484-
juce::OpenGLContext glContext; // GPU-accelerated rendering (Windows only)
485-
#endif
483+
// OpenGL context removed: see constructor comment for rationale.
484+
// Keeping the juce_opengl module in Projucer is harmless and allows
485+
// re-enabling GPU rendering in the future if the thread safety issues
486+
// are addressed.
486487

487488
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainComponent)
488489
};

0 commit comments

Comments
 (0)