44
55IntersectProcessor::IntersectProcessor ()
66 : AudioProcessor (BusesProperties()
7- .withOutput (" Output" , juce::AudioChannelSet::stereo(), true)),
7+ .withOutput (" Main" , juce::AudioChannelSet::stereo(), true)
8+ .withOutput (" Out 2" , juce::AudioChannelSet::stereo(), false)
9+ .withOutput (" Out 3" , juce::AudioChannelSet::stereo(), false)
10+ .withOutput (" Out 4" , juce::AudioChannelSet::stereo(), false)
11+ .withOutput (" Out 5" , juce::AudioChannelSet::stereo(), false)
12+ .withOutput (" Out 6" , juce::AudioChannelSet::stereo(), false)
13+ .withOutput (" Out 7" , juce::AudioChannelSet::stereo(), false)
14+ .withOutput (" Out 8" , juce::AudioChannelSet::stereo(), false)
15+ .withOutput (" Out 9" , juce::AudioChannelSet::stereo(), false)
16+ .withOutput (" Out 10" , juce::AudioChannelSet::stereo(), false)
17+ .withOutput (" Out 11" , juce::AudioChannelSet::stereo(), false)
18+ .withOutput (" Out 12" , juce::AudioChannelSet::stereo(), false)
19+ .withOutput (" Out 13" , juce::AudioChannelSet::stereo(), false)
20+ .withOutput (" Out 14" , juce::AudioChannelSet::stereo(), false)
21+ .withOutput (" Out 15" , juce::AudioChannelSet::stereo(), false)
22+ .withOutput (" Out 16" , juce::AudioChannelSet::stereo(), false)),
823 apvts (*this , nullptr , " PARAMETERS" , ParamLayout::createLayout())
924{
1025 masterVolParam = apvts.getRawParameterValue (ParamIds::masterVolume);
@@ -23,6 +38,26 @@ IntersectProcessor::IntersectProcessor()
2338 formantCompParam = apvts.getRawParameterValue (ParamIds::defaultFormantComp);
2439 grainModeParam = apvts.getRawParameterValue (ParamIds::defaultGrainMode);
2540 releaseTailParam = apvts.getRawParameterValue (ParamIds::defaultReleaseTail);
41+ reverseParam = apvts.getRawParameterValue (ParamIds::defaultReverse);
42+ maxVoicesParam = apvts.getRawParameterValue (ParamIds::maxVoices);
43+ }
44+
45+ bool IntersectProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
46+ {
47+ // Main output must be stereo
48+ if (layouts.outputBuses .isEmpty ())
49+ return false ;
50+ if (layouts.outputBuses [0 ] != juce::AudioChannelSet::stereo ())
51+ return false ;
52+
53+ // Additional outputs: stereo or disabled
54+ for (int i = 1 ; i < layouts.outputBuses .size (); ++i)
55+ {
56+ if (! layouts.outputBuses [i].isDisabled ()
57+ && layouts.outputBuses [i] != juce::AudioChannelSet::stereo ())
58+ return false ;
59+ }
60+ return true ;
2661}
2762
2863void IntersectProcessor::prepareToPlay (double sampleRate, int /* samplesPerBlock*/ )
@@ -153,6 +188,8 @@ void IntersectProcessor::handleCommand (const Command& cmd)
153188 case FieldGrainMode: s.grainMode = (int ) val; s.lockMask |= kLockGrainMode ; break ;
154189 case FieldVolume: s.volume = val; s.lockMask |= kLockVolume ; break ;
155190 case FieldReleaseTail: s.releaseTail = val > 0 .5f ; s.lockMask |= kLockReleaseTail ; break ;
191+ case FieldReverse: s.reverse = val > 0 .5f ; s.lockMask |= kLockReverse ; break ;
192+ case FieldOutputBus: s.outputBus = juce::jlimit (0 , 15 , (int ) val); s.lockMask |= kLockOutputBus ; break ;
156193 case FieldMidiNote:
157194 s.midiNote = juce::jlimit (0 , 127 , (int ) val);
158195 sliceManager.rebuildMidiMap ();
@@ -188,6 +225,8 @@ void IntersectProcessor::handleCommand (const Command& cmd)
188225 dst.grainMode = src.grainMode ;
189226 dst.volume = src.volume ;
190227 dst.releaseTail = src.releaseTail ;
228+ dst.reverse = src.reverse ;
229+ dst.outputBus = src.outputBus ;
191230 dst.lockMask = src.lockMask ;
192231 dst.colour = src.colour ;
193232 // midiNote is already assigned by createSlice
@@ -258,8 +297,8 @@ void IntersectProcessor::processMidi (juce::MidiBuffer& midi)
258297 }
259298 else
260299 {
261- int sliceIdx = sliceManager.midiNoteToSlice (note);
262- if ( sliceIdx >= 0 )
300+ const auto & sliceIndices = sliceManager.midiNoteToSlices (note);
301+ for ( int sliceIdx : sliceIndices )
263302 {
264303 if (midiSelectsSlice.load (std::memory_order_relaxed))
265304 sliceManager.selectedSlice = sliceIdx;
@@ -292,6 +331,7 @@ void IntersectProcessor::processMidi (juce::MidiBuffer& midi)
292331 (int ) grainModeParam->load (),
293332 masterVolParam->load (),
294333 releaseTailParam->load () > 0 .5f ,
334+ reverseParam->load () > 0 .5f ,
295335 sampleData);
296336 }
297337 }
@@ -320,22 +360,63 @@ void IntersectProcessor::processBlock (juce::AudioBuffer<float>& buffer,
320360 }
321361
322362 drainCommands ();
363+
364+ // Update max active voices from param
365+ voicePool.setMaxActiveVoices ((int ) maxVoicesParam->load ());
366+
323367 processMidi (midi);
324368
325369 if (! sampleData.isLoaded ())
326370 return ;
327371
328- auto * outL = buffer.getWritePointer (0 );
329- auto * outR = buffer.getNumChannels () > 1 ? buffer.getWritePointer (1 ) : nullptr ;
372+ // Collect write pointers for all enabled output buses
373+ static constexpr int kMaxBuses = 16 ;
374+ float * busL[kMaxBuses ] = {};
375+ float * busR[kMaxBuses ] = {};
376+ int numActiveBuses = 0 ;
377+
378+ for (int b = 0 ; b < std::min (getBusCount (false ), kMaxBuses ); ++b)
379+ {
380+ auto * bus = getBus (false , b);
381+ if (bus != nullptr && bus->isEnabled ())
382+ {
383+ int chOff = getChannelIndexInProcessBlockBuffer (false , b, 0 );
384+ busL[b] = buffer.getWritePointer (chOff);
385+ busR[b] = buffer.getNumChannels () > chOff + 1
386+ ? buffer.getWritePointer (chOff + 1 ) : nullptr ;
387+ if (b + 1 > numActiveBuses) numActiveBuses = b + 1 ;
388+ }
389+ }
390+
391+ buffer.clear ();
330392
331- for ( int i = 0 ; i < buffer. getNumSamples (); ++i )
393+ if (numActiveBuses <= 1 )
332394 {
333- float sL = 0 .0f , sR = 0 .0f ;
334- voicePool.processSample (sampleData, currentSampleRate, sL , sR );
395+ // Fast path: single stereo output
396+ for (int i = 0 ; i < buffer.getNumSamples (); ++i)
397+ {
398+ float sL = 0 .0f , sR = 0 .0f ;
399+ voicePool.processSample (sampleData, currentSampleRate, sL , sR );
400+ if (busL[0 ]) busL[0 ][i] = sL ;
401+ if (busR[0 ]) busR[0 ][i] = sR ;
402+ }
403+ }
404+ else
405+ {
406+ // Multi-out: route each voice to its assigned bus
407+ for (int i = 0 ; i < buffer.getNumSamples (); ++i)
408+ {
409+ for (int vi = 0 ; vi < voicePool.getMaxActiveVoices (); ++vi)
410+ {
411+ float vL = 0 .0f , vR = 0 .0f ;
412+ voicePool.processVoiceSample (vi, sampleData, currentSampleRate, vL, vR);
335413
336- outL[i] = sL ;
337- if (outR != nullptr )
338- outR[i] = sR ;
414+ int bus = voicePool.getVoice (vi).outputBus ;
415+ if (bus < 0 || bus >= numActiveBuses) bus = 0 ;
416+ if (busL[bus]) busL[bus][i] += vL;
417+ if (busR[bus]) busR[bus][i] += vR;
418+ }
419+ }
339420 }
340421
341422 // Pass through MIDI
@@ -352,7 +433,7 @@ void IntersectProcessor::getStateInformation (juce::MemoryBlock& destData)
352433 juce::MemoryOutputStream stream (destData, false );
353434
354435 // Version
355- stream.writeInt (10 );
436+ stream.writeInt (11 );
356437
357438 // APVTS state
358439 auto state = apvts.copyState ();
@@ -399,6 +480,9 @@ void IntersectProcessor::getStateInformation (juce::MemoryBlock& destData)
399480 stream.writeFloat (s.volume );
400481 // v10 fields
401482 stream.writeBool (s.releaseTail );
483+ // v11 fields
484+ stream.writeBool (s.reverse );
485+ stream.writeInt (s.outputBus );
402486 }
403487
404488 // v9: store file path only (no PCM)
@@ -411,7 +495,7 @@ void IntersectProcessor::setStateInformation (const void* data, int sizeInBytes)
411495 juce::MemoryInputStream stream (data, (size_t ) sizeInBytes, false );
412496
413497 int version = stream.readInt ();
414- if (version < 9 || version > 10 )
498+ if (version < 9 || version > 11 )
415499 return ;
416500
417501 // APVTS state
@@ -472,6 +556,17 @@ void IntersectProcessor::setStateInformation (const void* data, int sizeInBytes)
472556 s.releaseTail = stream.readBool ();
473557 }
474558
559+ if (version >= 11 )
560+ {
561+ s.reverse = stream.readBool ();
562+ s.outputBus = stream.readInt ();
563+ }
564+ else
565+ {
566+ s.reverse = false ;
567+ s.outputBus = 0 ;
568+ }
569+
475570 // v9 -> v10 migration: convert linear volume (0-1) to dB
476571 if (version == 9 )
477572 {
0 commit comments