@@ -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" ));
0 commit comments