Skip to content

Commit ed5e14f

Browse files
committed
feat: add slice note ranges
1 parent 642c472 commit ed5e14f

9 files changed

Lines changed: 401 additions & 109 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
1010
- Loop crossfade (FADE) parameter — smooths loop and ping-pong seams with equal-power crossfading, adjustable from 0–100% of the slice length
1111
- Fade region overlay and crossfade source cursor in the waveform view
1212
- Repitch MODE parameter with Linear and Cubic options for higher-quality Repitch playback
13+
- Slice note ranges with `NOTE`/`RANGE` switching plus per-slice `LOW`, `HIGH`, and `ROOT` note controls for chromatic trigger zones and transposition
1314

1415
### Changed
1516
- Gain control moved from the Playback module to the Amp module in the signal chain
@@ -21,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
2122
- Bungee algorithm no longer produces loud static when Grain is set to Smooth
2223
- Fade overlay now updates immediately when changing global crossfade, loop mode, or reverse settings
2324
- Loop fade cursors now appear on the first loop or ping-pong seam, and saved projects once again restore their linked sample path correctly
25+
- Context-bar note editing no longer flips single-note slices into range mode while dragging, and note names now stay read-only beside the numeric controls
2426

2527
## [0.11.0] - 2026-03-31
2628
### Added

src/PluginProcessor.cpp

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ static constexpr uint64_t kValidLockMask =
6161
| kLockFilterDrive | kLockFilterKeyTrack | kLockFilterEnvAttack | kLockFilterEnvDecay
6262
| kLockFilterEnvSustain | kLockFilterEnvRelease | kLockFilterEnvAmount
6363
| kLockFilterAsym | kLockCrossfade | kLockRepitchMode
64-
| kLockLoopStart | kLockLoopLength;
64+
| kLockLoopStart | kLockLoopLength
65+
| kLockHighNote | kLockSliceRootNote;
6566

6667
// Copies a global parameter value into a slice field based on the lock bit.
6768
// Used when locking a parameter to snapshot the current effective value.
@@ -116,6 +117,11 @@ static Slice sanitiseRestoredSlice (Slice s)
116117
s.endSample = s.startSample + kMinSliceLengthSamples;
117118

118119
s.midiNote = juce::jlimit (0, kMaxMidiNote, s.midiNote);
120+
s.highNote = juce::jlimit (0, kMaxMidiNote, s.highNote);
121+
s.sliceRootNote = juce::jlimit (0, kMaxMidiNote, s.sliceRootNote);
122+
if (s.highNote < s.midiNote)
123+
s.highNote = s.midiNote;
124+
s.sliceRootNote = juce::jlimit (s.midiNote, s.highNote, s.sliceRootNote);
119125
s.bpm = juce::jlimit (20.0f, 999.0f, s.bpm);
120126
s.pitchSemitones = juce::jlimit (-48.0f, 48.0f, s.pitchSemitones);
121127
s.algorithm = juce::jlimit (0, 2, s.algorithm);
@@ -1414,9 +1420,35 @@ void IntersectProcessor::handleCommand (const Command& cmd)
14141420
s.lockMask |= kLockLoopLength;
14151421
break;
14161422
case FieldMidiNote:
1423+
{
1424+
const bool wasSingleNote = (s.highNote == s.midiNote);
14171425
s.midiNote = juce::jlimit (0, kMaxMidiNote, (int) val);
1426+
1427+
if (wasSingleNote)
1428+
{
1429+
s.highNote = s.midiNote;
1430+
s.sliceRootNote = s.midiNote;
1431+
}
1432+
else
1433+
{
1434+
if (s.highNote < s.midiNote)
1435+
s.highNote = s.midiNote;
1436+
s.sliceRootNote = juce::jlimit (s.midiNote, s.highNote, s.sliceRootNote);
1437+
}
1438+
1439+
sliceManager.rebuildMidiMap();
1440+
break;
1441+
}
1442+
case FieldHighNote:
1443+
{
1444+
s.highNote = juce::jlimit (s.midiNote, kMaxMidiNote, (int) val);
1445+
s.sliceRootNote = juce::jlimit (s.midiNote, s.highNote, s.sliceRootNote);
14181446
sliceManager.rebuildMidiMap();
14191447
break;
1448+
}
1449+
case FieldSliceRootNote:
1450+
s.sliceRootNote = juce::jlimit (s.midiNote, s.highNote, (int) val);
1451+
break;
14201452
}
14211453
}
14221454
break;
@@ -1463,7 +1495,9 @@ void IntersectProcessor::handleCommand (const Command& cmd)
14631495
auto& dst = sliceManager.getSlice (newIdx);
14641496
int savedNote = dst.midiNote; // assigned by createSlice
14651497
dst = src; // copy all params, lockMask, colour
1466-
dst.midiNote = savedNote; // restore unique MIDI note
1498+
dst.midiNote = savedNote; // restore unique MIDI note
1499+
dst.highNote = savedNote; // reset to single-note
1500+
dst.sliceRootNote = savedNote;
14671501
if (cmd.intParam1 >= 0) // ctrl-drag: use explicit position
14681502
{
14691503
dst.startSample = cmd.intParam1;
@@ -1519,7 +1553,9 @@ void IntersectProcessor::handleCommand (const Command& cmd)
15191553
dst = srcCopy;
15201554
dst.startSample = s;
15211555
dst.endSample = e;
1522-
dst.midiNote = juce::jlimit (0, kMaxMidiNote, baseNote + i);
1556+
dst.midiNote = juce::jlimit (0, kMaxMidiNote, baseNote + i);
1557+
dst.highNote = dst.midiNote;
1558+
dst.sliceRootNote = dst.midiNote;
15231559
dst.colour = savedColour;
15241560
dst.active = true;
15251561
dst.loopStartOffset = 0;
@@ -1570,7 +1606,9 @@ void IntersectProcessor::handleCommand (const Command& cmd)
15701606
dst = srcCopy;
15711607
dst.startSample = s;
15721608
dst.endSample = e;
1573-
dst.midiNote = juce::jlimit (0, kMaxMidiNote, baseNote + subIdx);
1609+
dst.midiNote = juce::jlimit (0, kMaxMidiNote, baseNote + subIdx);
1610+
dst.highNote = dst.midiNote;
1611+
dst.sliceRootNote = dst.midiNote;
15741612
dst.colour = savedColour;
15751613
dst.active = true;
15761614
dst.loopStartOffset = 0;
@@ -1960,6 +1998,7 @@ void IntersectProcessor::processMidi (juce::MidiBuffer& midi)
19601998
voicePool.muteGroup (mg, voiceIdx);
19611999

19622000
p.sliceIdx = sliceIdx;
2001+
p.sliceRootNote = s.sliceRootNote;
19632002
voicePool.startVoice (voiceIdx, p, sliceManager, sampleData);
19642003
}
19652004
}
@@ -2333,7 +2372,7 @@ void IntersectProcessor::getStateInformation (juce::MemoryBlock& destData)
23332372

23342373
// Optional v24 extension block for fields added without changing the base version.
23352374
stream.writeInt (kStateExtensionMagic);
2336-
stream.writeInt (2);
2375+
stream.writeInt (3);
23372376
stream.writeInt (numSlices);
23382377
for (int i = 0; i < numSlices; ++i)
23392378
stream.writeInt (sliceManager.getSlice (i).repitchMode);
@@ -2344,6 +2383,14 @@ void IntersectProcessor::getStateInformation (juce::MemoryBlock& destData)
23442383
stream.writeInt (s.loopStartOffset);
23452384
stream.writeInt (s.loopLength);
23462385
}
2386+
// Extension v3: per-slice note ranges
2387+
stream.writeInt (numSlices);
2388+
for (int i = 0; i < numSlices; ++i)
2389+
{
2390+
const auto& s = sliceManager.getSlice (i);
2391+
stream.writeInt (s.highNote);
2392+
stream.writeInt (s.sliceRootNote);
2393+
}
23472394
}
23482395

23492396
void IntersectProcessor::setStateInformation (const void* data, int sizeInBytes)
@@ -2471,6 +2518,8 @@ void IntersectProcessor::setStateInformation (const void* data, int sizeInBytes)
24712518
std::vector<int> repitchModes;
24722519
std::vector<int> loopStartOffsets;
24732520
std::vector<int> loopLengths;
2521+
std::vector<int> highNotes;
2522+
std::vector<int> sliceRootNotes;
24742523
};
24752524

24762525
const auto postSliceBasePosition = stream.getPosition();
@@ -2480,6 +2529,8 @@ void IntersectProcessor::setStateInformation (const void* data, int sizeInBytes)
24802529
result.repitchModes.assign ((size_t) validatedNumSlices, 0);
24812530
result.loopStartOffsets.assign ((size_t) validatedNumSlices, 0);
24822531
result.loopLengths.assign ((size_t) validatedNumSlices, 0);
2532+
result.highNotes.assign ((size_t) validatedNumSlices, -1);
2533+
result.sliceRootNotes.assign ((size_t) validatedNumSlices, -1);
24832534

24842535
juce::MemoryInputStream trialStream (data, (size_t) sizeInBytes, false);
24852536
if (! trialStream.setPosition (postSliceBasePosition))
@@ -2579,6 +2630,27 @@ void IntersectProcessor::setStateInformation (const void* data, int sizeInBytes)
25792630
}
25802631
}
25812632
}
2633+
2634+
if (extensionVersion >= 3)
2635+
{
2636+
if (! requireBytes (4))
2637+
return result;
2638+
2639+
const int storedNoteRanges = juce::jlimit (0, storedNumSlices, trialStream.readInt());
2640+
for (int i = 0; i < storedNoteRanges; ++i)
2641+
{
2642+
if (! requireBytes (8))
2643+
return result;
2644+
2645+
const int highNote = trialStream.readInt();
2646+
const int sliceRootNote = trialStream.readInt();
2647+
if (i < validatedNumSlices)
2648+
{
2649+
result.highNotes[(size_t) i] = highNote;
2650+
result.sliceRootNotes[(size_t) i] = sliceRootNote;
2651+
}
2652+
}
2653+
}
25822654
}
25832655
else
25842656
{
@@ -2633,6 +2705,20 @@ void IntersectProcessor::setStateInformation (const void* data, int sizeInBytes)
26332705
parsed.repitchMode = postSliceResult->repitchModes[(size_t) i];
26342706
parsed.loopStartOffset = postSliceResult->loopStartOffsets[(size_t) i];
26352707
parsed.loopLength = postSliceResult->loopLengths[(size_t) i];
2708+
2709+
if (postSliceResult->highNotes[(size_t) i] >= 0)
2710+
{
2711+
parsed.highNote = postSliceResult->highNotes[(size_t) i];
2712+
parsed.sliceRootNote = postSliceResult->sliceRootNotes[(size_t) i];
2713+
}
2714+
else
2715+
{
2716+
// Legacy: no range data — single-note trigger, root = global rootNote
2717+
// to preserve existing filter key-track sound
2718+
parsed.highNote = parsed.midiNote;
2719+
parsed.sliceRootNote = sliceManager.rootNote.load();
2720+
}
2721+
26362722
sliceManager.getSlice (i) = sanitiseRestoredSlice (parsed);
26372723
}
26382724

src/PluginProcessor.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ class IntersectProcessor : public juce::AudioProcessor
125125
FieldCrossfade,
126126
FieldLoopStart,
127127
FieldLoopLength,
128+
FieldHighNote,
129+
FieldSliceRootNote,
128130
};
129131

130132
enum class MidiEditAction

src/audio/Slice.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ enum LockBit : uint64_t
4141
kLockCrossfade = 0x200000000ull,
4242
kLockRepitchMode = 0x400000000ull,
4343
kLockLoopStart = 0x800000000ull,
44-
kLockLoopLength = 0x1000000000ull
44+
kLockLoopLength = 0x1000000000ull,
45+
kLockHighNote = 0x2000000000ull, // bit 37
46+
kLockSliceRootNote = 0x4000000000ull // bit 38
4547
};
4648

4749
inline int getMaxCrossfadeLengthSamples (int sliceLen, bool /*pingPong*/)
@@ -88,6 +90,8 @@ struct Slice
8890
int startSample = 0;
8991
int endSample = 0;
9092
int midiNote = kDefaultRootNote;
93+
int highNote = kDefaultRootNote; // high end of note range
94+
int sliceRootNote = kDefaultRootNote; // root note for pitch transpose
9195
float bpm = 120.0f;
9296
float pitchSemitones = 0.0f;
9397
int algorithm = 0; // 0=Repitch, 1=Stretch, 2=Bungee

src/audio/SliceManager.cpp

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ int SliceManager::createSlice (int start, int end)
3030
s.active = true;
3131
s.startSample = start;
3232
s.endSample = end;
33-
s.midiNote = nextMidiNote();
33+
s.midiNote = nextMidiNote();
34+
s.highNote = s.midiNote;
35+
s.sliceRootNote = s.midiNote;
3436
s.lockMask = 0;
3537

3638
// Default override values
@@ -107,8 +109,9 @@ void SliceManager::rebuildMidiMap()
107109
{
108110
if (slices[i].active)
109111
{
110-
int note = slices[i].midiNote;
111-
if (note >= 0 && note < kMidiNoteCount)
112+
int lo = juce::jlimit (0, kMaxMidiNote, slices[i].midiNote);
113+
int hi = juce::jlimit (lo, kMaxMidiNote, slices[i].highNote);
114+
for (int note = lo; note <= hi; ++note)
112115
{
113116
if (midiMap[note] < 0)
114117
midiMap[note] = i;
@@ -145,8 +148,14 @@ int SliceManager::nextMidiNote() const
145148
{
146149
int highest = rootNote.load() - 1;
147150
for (int i = 0; i < numSlices; ++i)
148-
if (slices[i].active && slices[i].midiNote > highest)
149-
highest = slices[i].midiNote;
151+
{
152+
if (slices[i].active)
153+
{
154+
int top = juce::jmax (slices[i].midiNote, slices[i].highNote);
155+
if (top > highest)
156+
highest = top;
157+
}
158+
}
150159
return std::min (highest + 1, kMaxMidiNote);
151160
}
152161

@@ -182,6 +191,11 @@ void SliceManager::repackMidiNotes (bool sortByPosition)
182191

183192
int root = rootNote.load();
184193
for (int i = 0; i < numSlices; ++i)
185-
slices[i].midiNote = std::min (root + i, kMaxMidiNote);
194+
{
195+
int note = std::min (root + i, kMaxMidiNote);
196+
slices[i].midiNote = note;
197+
slices[i].highNote = note;
198+
slices[i].sliceRootNote = note;
199+
}
186200
rebuildMidiMap();
187201
}

src/audio/VoicePool.cpp

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -919,12 +919,21 @@ void VoicePool::startVoice (int voiceIdx, const VoiceStartParams& p,
919919
float pitchSt = sm.resolveParam (sliceIdx, kLockPitch, s.pitchSemitones, p.globalPitch);
920920
float cents = sm.resolveParam (sliceIdx, kLockCentsDetune, s.centsDetune, p.globalCentsDetune);
921921
float pitch = pitchSt + cents / 100.0f;
922-
float pitchRatio = std::pow (2.0f, pitch / 12.0f);
923922

924923
bool stretchOn = sm.resolveParam (sliceIdx, kLockStretch,
925924
s.stretchEnabled ? 1.0f : 0.0f,
926925
p.globalStretch ? 1.0f : 0.0f) > 0.5f;
927926

927+
// Range transpose: chromatic offset from slice root note.
928+
// Ignored for Repitch+stretch where pitch is tied to BPM-locked speed.
929+
const float rangeTranspose = (float) (p.note - p.sliceRootNote);
930+
const bool repitchWithStretch = (algo == 0 && stretchOn
931+
&& p.dawBpm > 0.0f && sliceBpm > 0.0f);
932+
if (! repitchWithStretch)
933+
pitch += rangeTranspose;
934+
935+
float pitchRatio = std::pow (2.0f, pitch / 12.0f);
936+
928937
float tonality = sm.resolveParam (sliceIdx, kLockTonality, s.tonalityHz, p.globalTonality);
929938
float formant = sm.resolveParam (sliceIdx, kLockFormant, s.formantSemitones, p.globalFormant);
930939
bool fComp = sm.resolveParam (sliceIdx, kLockFormantComp,
@@ -966,7 +975,7 @@ void VoicePool::startVoice (int voiceIdx, const VoiceStartParams& p,
966975
const float keyTrackPercent = sm.resolveParam (sliceIdx, kLockFilterKeyTrack,
967976
s.filterKeyTrack, p.globalFilterKeyTrack);
968977
const float keyTrackNorm = juce::jlimit (0.0f, 1.0f, keyTrackPercent / 100.0f);
969-
const float noteRatio = std::pow (2.0f, ((float) p.note - (float) p.rootNote) / 12.0f);
978+
const float noteRatio = std::pow (2.0f, ((float) p.note - (float) p.sliceRootNote) / 12.0f);
970979
v.filterKeyTrackRatio = std::pow (noteRatio, keyTrackNorm);
971980
const float filterEnvAttack = sm.resolveParam (sliceIdx, kLockFilterEnvAttack,
972981
s.filterEnvAttackSec, p.globalFilterEnvAttackSec);

src/audio/VoicePool.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ struct VoiceStartParams
5050
float globalFilterEnvAmount = 0.0f;
5151
float globalCrossfadePct = 0.0f;
5252
int rootNote = kDefaultRootNote;
53+
int sliceRootNote = kDefaultRootNote; // per-slice root for range transpose
5354
};
5455

5556
struct PreviewStretchParams

0 commit comments

Comments
 (0)