Skip to content

Commit 310cdf9

Browse files
tucktuckg00seclaude
andcommitted
v0.5.0: Width increase, REV, multi-slice triggers, voices, multi-output, UI fixes
- Wider interface (750 -> 900px base width) - SLICES/ROOT right-aligned with grey values in SliceControlBar - Row 2 reordered: ATK DEC SUS REL TAIL REV PP MUTE STRETCH GAIN VOICES - REV (reverse) toggle: per-slice lockable, works with all 3 algo modes - Bungee ping-pong crossfade (64-sample) eliminates dropout/pop on direction change - Multi-slice MIDI triggers: multiple slices on same MIDI note all fire - Configurable voice count (1-32, default 16) via VOICES param - Multi-output support: 16 stereo output buses, per-slice OUT routing (1-16) - Serialization v11 (backward compat with v9/v10) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a6aab53 commit 310cdf9

14 files changed

Lines changed: 516 additions & 201 deletions

project/src/PluginEditor.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#include "PluginEditor.h"
22

3-
static constexpr int kBaseW = 750;
3+
static constexpr int kBaseW = 900;
44
static constexpr int kBaseH = 550;
55
static constexpr int kHeaderH = 66;
66
static constexpr int kSliceLaneH = 30;

project/src/PluginProcessor.cpp

Lines changed: 108 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,22 @@
44

55
IntersectProcessor::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

2863
void 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
{

project/src/PluginProcessor.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class IntersectProcessor : public juce::AudioProcessor
2020
juce::AudioProcessorEditor* createEditor() override;
2121
bool hasEditor() const override { return true; }
2222

23+
bool isBusesLayoutSupported (const BusesLayout& layouts) const override;
2324
const juce::String getName() const override { return "INTERSECT"; }
2425
bool acceptsMidi() const override { return true; }
2526
bool producesMidi() const override { return true; }
@@ -71,6 +72,8 @@ class IntersectProcessor : public juce::AudioProcessor
7172
FieldGrainMode,
7273
FieldVolume,
7374
FieldReleaseTail,
75+
FieldReverse,
76+
FieldOutputBus,
7477
};
7578

7679
struct Command
@@ -135,6 +138,8 @@ class IntersectProcessor : public juce::AudioProcessor
135138
std::atomic<float>* formantCompParam = nullptr;
136139
std::atomic<float>* grainModeParam = nullptr;
137140
std::atomic<float>* releaseTailParam = nullptr;
141+
std::atomic<float>* reverseParam = nullptr;
142+
std::atomic<float>* maxVoicesParam = nullptr;
138143

139144
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (IntersectProcessor)
140145
};

project/src/audio/Slice.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ enum LockBit : uint32_t
1919
kLockFormantComp = 4096,
2020
kLockGrainMode = 8192,
2121
kLockVolume = 16384,
22-
kLockReleaseTail = 32768
22+
kLockReleaseTail = 32768,
23+
kLockReverse = 65536,
24+
kLockOutputBus = 131072
2325
};
2426

2527
struct Slice
@@ -44,6 +46,8 @@ struct Slice
4446
int grainMode = 0; // Bungee: -1=Fast, 0=Normal, +1=Smooth
4547
float volume = 0.0f;
4648
bool releaseTail = false;
49+
bool reverse = false;
50+
int outputBus = 0;
4751
uint32_t lockMask = 0;
4852
juce::Colour colour { 0.4f, 0.7f, 0.95f, 1.0f };
4953
};

project/src/audio/SliceManager.cpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,20 @@ void SliceManager::clearAll()
104104
void SliceManager::rebuildMidiMap()
105105
{
106106
midiMap.fill (-1);
107+
for (auto& v : midiMapMulti)
108+
v.clear();
109+
107110
for (int i = 0; i < numSlices; ++i)
108111
{
109112
if (slices[i].active)
110113
{
111114
int note = slices[i].midiNote;
112115
if (note >= 0 && note < 128)
113-
midiMap[note] = i;
116+
{
117+
if (midiMap[note] < 0)
118+
midiMap[note] = i;
119+
midiMapMulti[note].push_back (i);
120+
}
114121
}
115122
}
116123
}
@@ -122,6 +129,14 @@ int SliceManager::midiNoteToSlice (int note) const
122129
return midiMap[note];
123130
}
124131

132+
const std::vector<int>& SliceManager::midiNoteToSlices (int note) const
133+
{
134+
static const std::vector<int> empty;
135+
if (note < 0 || note >= 128)
136+
return empty;
137+
return midiMapMulti[note];
138+
}
139+
125140
float SliceManager::resolveParam (int sliceIdx, LockBit lockBit, float sliceValue, float globalDefault) const
126141
{
127142
if (sliceIdx < 0 || sliceIdx >= numSlices)

project/src/audio/SliceManager.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class SliceManager
1414
void clearAll();
1515
void rebuildMidiMap();
1616
int midiNoteToSlice (int note) const;
17+
const std::vector<int>& midiNoteToSlices (int note) const;
1718

1819
float resolveParam (int sliceIdx, LockBit lockBit, float sliceValue, float globalDefault) const;
1920

@@ -30,5 +31,6 @@ class SliceManager
3031
private:
3132
std::array<Slice, kMaxSlices> slices;
3233
int numSlices = 0;
33-
std::array<int, 128> midiMap;
34+
std::array<int, 128> midiMap; // first slice for note (legacy compat)
35+
std::array<std::vector<int>, 128> midiMapMulti; // all slices for note
3436
};

project/src/audio/Voice.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ struct Voice
3232
float volume = 1.0f;
3333
bool releaseTail = false;
3434
int sampleEnd = 0; // actual end of sample buffer (for release tail)
35+
int outputBus = 0; // output bus index (0-15)
3536

3637
// WSOLA fields (legacy, still used for basic WSOLA fallback)
3738
bool wsolaActive = false;
@@ -62,4 +63,9 @@ struct Voice
6263
double bungeePitch = 1.0;
6364
double bungeeSpeed = 1.0;
6465
bool bungeeResetNeeded = false;
66+
67+
// Bungee ping-pong crossfade fields
68+
int bungeePPFade = 0;
69+
int bungeePPFadeLen = 64;
70+
std::vector<float> bungeePPFadeL, bungeePPFadeR;
6571
};

0 commit comments

Comments
 (0)