Skip to content

Commit 86a3c1d

Browse files
authored
Update V1.9.8
1 parent 69a7b3b commit 86a3c1d

12 files changed

Lines changed: 2096 additions & 109 deletions

AppSettings.h

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,115 @@ struct CuePoint
110110
}
111111
};
112112

113+
//==============================================================================
114+
// GeneratorCuePoint -- a cue inside a Generator preset. Same trigger
115+
// payload as CuePoint (MIDI/OSC/Art-Net) but the trigger position is
116+
// expressed as an absolute SMPTE timecode (HH:MM:SS:FF) rather than as
117+
// milliseconds from track start. This matches the rest of the Generator
118+
// preset editor (which already speaks in TC for Start / Stop) and means a
119+
// cue at "01:00:30:00" works identically whether the preset has an audio
120+
// file (TC follows the audio playhead) or runs in pure TC-generation mode.
121+
//==============================================================================
122+
struct GeneratorCuePoint
123+
{
124+
juce::String positionTC = "00:00:00:00"; // HH:MM:SS:FF -- when to fire
125+
juce::String name; // user label
126+
127+
// Trigger config -- mirrors CuePoint exactly so the same dispatch
128+
// helpers (firing MIDI / OSC / DMX) can be reused.
129+
int midiChannel = 0; // 0-15 (displayed as 1-16)
130+
int midiNoteNum = -1; // -1 = disabled
131+
int midiNoteVel = 127;
132+
int midiCCNum = -1; // -1 = disabled
133+
int midiCCVal = 127;
134+
135+
juce::String oscAddress;
136+
juce::String oscArgs;
137+
138+
int artnetCh = 0; // 0 = disabled, 1-512
139+
int artnetVal = 255;
140+
141+
bool hasMidiTrigger() const { return midiNoteNum >= 0 || midiCCNum >= 0; }
142+
bool hasOscTrigger() const { return oscAddress.isNotEmpty(); }
143+
bool hasArtnetTrigger() const { return artnetCh > 0; }
144+
bool hasAnyTrigger() const { return hasMidiTrigger() || hasOscTrigger() || hasArtnetTrigger(); }
145+
146+
/// Convert positionTC to total milliseconds (frame -> ms uses the
147+
/// supplied frame rate; default 30 matches the rest of the codebase
148+
/// when the caller doesn't know the engine's effective fps). This is
149+
/// the form the engine uses to compare against its current TC and
150+
/// decide whether a cue should fire.
151+
uint32_t positionMs(double fps = 30.0) const
152+
{
153+
auto parts = juce::StringArray::fromTokens(positionTC, ":.", "");
154+
int h = 0, m = 0, s = 0, f = 0;
155+
if (parts.size() >= 1) h = parts[0].getIntValue();
156+
if (parts.size() >= 2) m = parts[1].getIntValue();
157+
if (parts.size() >= 3) s = parts[2].getIntValue();
158+
if (parts.size() >= 4) f = parts[3].getIntValue();
159+
if (fps <= 0.0) fps = 30.0;
160+
double totalSec = h * 3600.0 + m * 60.0 + s + (double) f / fps;
161+
return (uint32_t) juce::jmax(0.0, totalSec * 1000.0);
162+
}
163+
164+
juce::var toVar() const
165+
{
166+
auto* obj = new juce::DynamicObject();
167+
obj->setProperty("positionTC", positionTC);
168+
if (name.isNotEmpty())
169+
obj->setProperty("name", name);
170+
171+
if (midiNoteNum >= 0 || midiCCNum >= 0)
172+
{
173+
obj->setProperty("midiChannel", midiChannel);
174+
obj->setProperty("midiNoteNum", midiNoteNum);
175+
obj->setProperty("midiNoteVel", midiNoteVel);
176+
obj->setProperty("midiCCNum", midiCCNum);
177+
obj->setProperty("midiCCVal", midiCCVal);
178+
}
179+
if (oscAddress.isNotEmpty())
180+
{
181+
obj->setProperty("oscAddress", oscAddress);
182+
if (oscArgs.isNotEmpty())
183+
obj->setProperty("oscArgs", oscArgs);
184+
}
185+
if (artnetCh > 0)
186+
{
187+
obj->setProperty("artnetCh", artnetCh);
188+
obj->setProperty("artnetVal", artnetVal);
189+
}
190+
return juce::var(obj);
191+
}
192+
193+
void fromVar(const juce::var& v)
194+
{
195+
auto* obj = v.getDynamicObject();
196+
if (!obj) return;
197+
198+
auto getInt = [&](const char* key, int def) {
199+
auto val = obj->getProperty(key);
200+
return val.isVoid() ? def : (int)val;
201+
};
202+
auto getString = [&](const char* key, const juce::String& def = {}) {
203+
auto val = obj->getProperty(key);
204+
return val.isVoid() ? def : val.toString();
205+
};
206+
207+
positionTC = getString("positionTC", "00:00:00:00");
208+
if (positionTC.isEmpty()) positionTC = "00:00:00:00";
209+
name = getString("name");
210+
midiChannel = juce::jlimit(0, 15, getInt("midiChannel", 0));
211+
midiNoteNum = juce::jlimit(-1, 127, getInt("midiNoteNum", -1));
212+
midiNoteVel = juce::jlimit(0, 127, getInt("midiNoteVel", 127));
213+
midiCCNum = juce::jlimit(-1, 127, getInt("midiCCNum", -1));
214+
midiCCVal = juce::jlimit(0, 127, getInt("midiCCVal", 127));
215+
oscAddress = getString("oscAddress");
216+
oscArgs = getString("oscArgs");
217+
artnetCh = juce::jlimit(0, 512, getInt("artnetCh", 0));
218+
artnetVal = juce::jlimit(0, 255, getInt("artnetVal", 255));
219+
}
220+
};
221+
113222
//==============================================================================
114223
// TrackMapEntry -- per-track config: offset, triggers, cue points
115224
//==============================================================================
@@ -836,9 +945,26 @@ struct GeneratorPreset
836945
juce::String audioFilePath; // empty = no audio playback
837946
bool audioLoop = false; // loop file when reaching its end
838947

948+
// Cue points -- triggers that fire when the generated TC reaches each
949+
// cue's positionTC. Sorted by positionTC (compared as ms) so the
950+
// engine can do a forward linear scan during playback.
951+
std::vector<GeneratorCuePoint> cuePoints;
952+
839953
std::string key() const { return name.toLowerCase().trim().toStdString(); }
840954
bool hasValidKey() const { return name.trim().isNotEmpty(); }
841955

956+
bool hasCuePoints() const { return ! cuePoints.empty(); }
957+
958+
/// Sort cue points by position (ms), with frame-rate cancellation: any
959+
/// reasonable fps gives the same ordering since positionTC ordering is
960+
/// dominated by H/M/S, frames only matter in tie-breaking.
961+
void sortCuePoints()
962+
{
963+
std::sort(cuePoints.begin(), cuePoints.end(),
964+
[](const GeneratorCuePoint& a, const GeneratorCuePoint& b)
965+
{ return a.positionMs() < b.positionMs(); });
966+
}
967+
842968
juce::var toVar() const
843969
{
844970
auto* obj = new juce::DynamicObject();
@@ -847,6 +973,14 @@ struct GeneratorPreset
847973
obj->setProperty("stopTC", stopTC);
848974
obj->setProperty("audioFilePath", audioFilePath);
849975
obj->setProperty("audioLoop", audioLoop);
976+
977+
if (! cuePoints.empty())
978+
{
979+
juce::Array<juce::var> arr;
980+
for (auto& cp : cuePoints)
981+
arr.add(cp.toVar());
982+
obj->setProperty("cuePoints", arr);
983+
}
850984
return juce::var(obj);
851985
}
852986

@@ -861,6 +995,19 @@ struct GeneratorPreset
861995
audioLoop = (bool) obj->getProperty("audioLoop");
862996
if (startTC.isEmpty()) startTC = "00:00:00:00";
863997
if (stopTC.isEmpty()) stopTC = "00:00:00:00";
998+
999+
cuePoints.clear();
1000+
auto cuesVar = obj->getProperty("cuePoints");
1001+
if (auto* arr = cuesVar.getArray())
1002+
{
1003+
cuePoints.reserve((size_t) arr->size());
1004+
for (auto& item : *arr)
1005+
{
1006+
GeneratorCuePoint cp;
1007+
cp.fromVar(item);
1008+
cuePoints.push_back(std::move(cp));
1009+
}
1010+
}
8641011
}
8651012
};
8661013

@@ -1207,7 +1354,8 @@ struct EngineSettings
12071354
generatorAudioSampleRate = getDouble("generatorAudioSampleRate", 0.0);
12081355
generatorAudioBufferSize = getInt("generatorAudioBufferSize", 0);
12091356
generatorAudioFileChannelMode = getInt("generatorAudioFileChannelMode", 0);
1210-
proDJLinkPlayer = juce::jlimit(1, 8, getInt("proDJLinkPlayer", 1));
1357+
// proDJLinkPlayer ids: 1-6 = players, 7 = XF-A, 8 = XF-B, 9 = MASTER
1358+
proDJLinkPlayer = juce::jlimit(1, 9, getInt("proDJLinkPlayer", 1));
12111359
trackMapEnabled = getBool("trackMapEnabled", getBool("tcnetTrackMapEnabled", false));
12121360
midiClockEnabled = getBool("midiClockEnabled", getBool("tcnetMidiClock", false));
12131361
oscBpmAddr = getString("oscBpmAddr", getString("tcnetOscBpmAddr", "/composition/tempocontroller/tempo"));

DbServerClient.h

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -957,7 +957,21 @@ class DbServerClient : private juce::Thread
957957
msg.strArgs[i] = arg.stringValue;
958958
if (arg.type == 0x14 && arg.blobData.getSize() > 0)
959959
msg.blobArgs[i] = std::move(arg.blobData);
960-
lastArg = arg;
960+
961+
// The next-iteration safety check (zero-length blob detection)
962+
// only needs lastArg.type, lastArg.numericValue, and lastArg.ok.
963+
// Do NOT copy `arg` whole here: when the branch above moved
964+
// arg.blobData out, JUCE's MemoryBlock move-assignment leaves
965+
// arg.blobData with data=nullptr but size=originalSize, so a
966+
// subsequent copy-assignment of the MemoryBlock would call
967+
// memcpy(dst, nullptr, size) inside MemoryBlock::operator=,
968+
// crashing the whole dbserver worker thread. Manifests when
969+
// reading any blob field (artwork JPEG, ANLZ waveform tags,
970+
// etc.) -- exactly the path that loads images in the PDL view.
971+
lastArg = FieldResult{};
972+
lastArg.type = arg.type;
973+
lastArg.numericValue = arg.numericValue;
974+
lastArg.ok = arg.ok;
961975
}
962976

963977
msg.ok = true;

GeneratorAudioPlayer.h

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,45 @@ class GeneratorAudioPlayer : private juce::AudioIODeviceCallback
3535
public:
3636
GeneratorAudioPlayer()
3737
{
38-
formatManager.registerBasicFormats(); // WAV, AIFF, FLAC, OGG (+ MP3 if JUCE_USE_MP3AUDIOFORMAT was set in Projucer)
39-
#if JUCE_USE_MP3AUDIOFORMAT
40-
// Belt and braces: register MP3 explicitly even though
41-
// registerBasicFormats() already includes it. Duplicate registration
42-
// is harmless -- createReaderFor returns the first format that
43-
// accepts the extension.
44-
formatManager.registerFormat(new juce::MP3AudioFormat(), false);
45-
#endif
38+
// Registers WAV, AIFF, FLAC, OGG, and -- when the Projucer flag
39+
// JUCE_USE_MP3AUDIOFORMAT is set -- MP3. Do NOT re-register MP3
40+
// explicitly afterwards; JUCE_DEBUG asserts on duplicate format
41+
// registration (jassertfalse in AudioFormatManager::registerFormat).
42+
formatManager.registerBasicFormats();
4643
}
4744

4845
~GeneratorAudioPlayer() override
4946
{
50-
// Stop the loader thread first so it cannot fire a new load against
51-
// members we are about to tear down. (Belt and braces: the
52-
// LoaderThread destructor also does this on its own destruction,
53-
// but doing it explicitly here means the closeDevice / unloadFile
54-
// calls that follow run with no concurrent file I/O.)
47+
// Order matters here. Members are destroyed in reverse declaration
48+
// order, which means the std::atomic<> flags (deviceOpen, userPaused,
49+
// shouldPlay, fileLoadedAtomic, ...) are torn down BEFORE the
50+
// deviceManager. If the audio device had any callback still in
51+
// flight when ~AudioDeviceManager() runs, that callback would read
52+
// already-destroyed atomics -- the std::atomic load crash on shutdown
53+
// we hunted in v1.9.7/v1.9.8.
54+
//
55+
// Step 1: latch shuttingDown so any in-flight or imminent audio
56+
// callback returns immediately without touching other atomics.
57+
// The acquire/release pairing on shuttingDown ensures the callback
58+
// sees the flag if it observes the store.
59+
shuttingDown.store(true, std::memory_order_release);
60+
61+
// Step 2: closeDevice() calls removeAudioCallback, which JUCE
62+
// documents as blocking until any in-flight callback returns. We
63+
// call it explicitly here (rather than relying on the deviceManager
64+
// destructor) so the audio thread is provably idle BEFORE we leave
65+
// the user-defined destructor body and member destruction begins.
66+
// The loader thread is stopped first because its tick can call back
67+
// into transport / reader objects that closeDevice will tear down.
5568
loaderThread.stop();
5669
closeDevice();
5770
unloadFile();
71+
// Belt-and-braces: ensure the AudioDeviceManager is fully closed
72+
// and has no callbacks attached. closeDevice already did this for
73+
// the case where a device was open, but this also covers the path
74+
// where ~GeneratorAudioPlayer runs without ever having opened one.
75+
deviceManager.removeAudioCallback(this);
76+
deviceManager.closeAudioDevice();
5877
}
5978

6079
//==========================================================================
@@ -575,6 +594,14 @@ class GeneratorAudioPlayer : private juce::AudioIODeviceCallback
575594
if (outputChannelData[ch])
576595
std::memset(outputChannelData[ch], 0, sizeof(float) * (size_t) numSamples);
577596

597+
// Shutting-down guard: if our destructor has latched this flag, we
598+
// return immediately without touching any other atomic / object.
599+
// This makes the callback's lifetime trivially safe even if some
600+
// exotic driver path delivers a final tick after removeAudioCallback
601+
// has been issued (which JUCE itself protects against on most
602+
// backends, but a single atomic load is cheap insurance).
603+
if (shuttingDown.load(std::memory_order_acquire)) return;
604+
578605
if (userPaused.load(std::memory_order_acquire)) return;
579606
if (! transport.isPlaying()) return;
580607
if (! fileLoadedAtomic.load(std::memory_order_acquire)) return;
@@ -659,6 +686,8 @@ class GeneratorAudioPlayer : private juce::AudioIODeviceCallback
659686

660687
void audioDeviceAboutToStart(juce::AudioIODevice* device) override
661688
{
689+
if (shuttingDown.load(std::memory_order_acquire)) return;
690+
662691
if (device)
663692
{
664693
currentSampleRate = device->getCurrentSampleRate();
@@ -675,6 +704,8 @@ class GeneratorAudioPlayer : private juce::AudioIODeviceCallback
675704

676705
void audioDeviceStopped() override
677706
{
707+
if (shuttingDown.load(std::memory_order_acquire)) return;
708+
678709
const juce::ScopedLock sl(transportLock);
679710
transport.releaseResources();
680711
}
@@ -696,6 +727,7 @@ class GeneratorAudioPlayer : private juce::AudioIODeviceCallback
696727
juce::AudioBuffer<float> scratchBuffer;
697728

698729
std::atomic<bool> deviceOpen { false };
730+
std::atomic<bool> shuttingDown { false }; // set in dtor before closeDevice; audio callback returns immediately if true
699731
std::atomic<int> selectedChannel { -1 };
700732
std::atomic<bool> loopFlag { false };
701733
std::atomic<bool> userPaused { false }; // logical pause state -- see pause() comment

0 commit comments

Comments
 (0)