@@ -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
23492396void 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
0 commit comments