1111
1212static constexpr int kStretchBlockSize = 128 ;
1313
14+ static inline float dbToLinear (float dB)
15+ {
16+ if (dB <= -100 .0f ) return 0 .0f ;
17+ return std::pow (10 .0f , dB / 20 .0f );
18+ }
19+
1420VoicePool::VoicePool ()
1521{
1622 for (auto & p : voicePositions)
@@ -112,6 +118,7 @@ void VoicePool::startVoice (int voiceIdx, int sliceIdx, float velocity, int note
112118 bool globalStretchEnabled, float dawBpmVal,
113119 float globalTonality, float globalFormant, bool globalFormantComp,
114120 int globalGrainMode, float globalVolume,
121+ bool globalReleaseTail,
115122 const SampleData& sample)
116123{
117124 auto & v = voices[voiceIdx];
@@ -161,7 +168,12 @@ void VoicePool::startVoice (int voiceIdx, int sliceIdx, float velocity, int note
161168 // Convert grainMode choice index (0=Fast, 1=Normal, 2=Smooth) to log2 hop adjust (-1, 0, +1)
162169 int hopAdj = grainMode - 1 ;
163170
164- v.volume = sm.resolveParam (sliceIdx, kLockVolume , s.volume , globalVolume);
171+ v.volume = dbToLinear (sm.resolveParam (sliceIdx, kLockVolume , s.volume , globalVolume));
172+
173+ v.releaseTail = sm.resolveParam (sliceIdx, kLockReleaseTail ,
174+ s.releaseTail ? 1 .0f : 0 .0f ,
175+ globalReleaseTail ? 1 .0f : 0 .0f ) > 0 .5f ;
176+ v.sampleEnd = sample.getNumFrames ();
165177
166178 // Reset stretch state
167179 v.stretchActive = false ;
@@ -260,17 +272,23 @@ static void fillStretchBlock (Voice& v, const SampleData& sample)
260272 for (int i = 0 ; i < inputSamples; ++i)
261273 {
262274 double pos = v.stretchSrcPos ;
275+ // Clamp to sample buffer bounds for safety
276+ pos = juce::jlimit (0.0 , (double ) v.sampleEnd - 1 , pos);
263277 v.stretchInBufL [(size_t ) i] = sample.getInterpolatedSample (pos, 0 );
264278 v.stretchInBufR [(size_t ) i] = sample.getInterpolatedSample (pos, 1 );
265- v.stretchSrcPos += 1.0 ;
279+ v.stretchSrcPos += ( double ) v. direction ;
266280
267281 // Check bounds
268- if (v.stretchSrcPos >= v.endSample )
282+ if (v.direction > 0 && v. stretchSrcPos >= v.endSample )
269283 {
270284 if (v.pingPong )
271285 {
272286 v.stretchSrcPos = v.endSample - 1 ;
273- // For simplicity, just clamp — ping-pong with stretch is edge case
287+ v.direction = -1 ;
288+ }
289+ else if (v.releaseTail && v.stretchSrcPos < v.sampleEnd )
290+ {
291+ // Continue reading beyond slice end during release tail
274292 }
275293 else
276294 {
@@ -286,6 +304,14 @@ static void fillStretchBlock (Voice& v, const SampleData& sample)
286304 break ;
287305 }
288306 }
307+ else if (v.direction < 0 && v.stretchSrcPos <= v.startSample )
308+ {
309+ if (v.pingPong )
310+ {
311+ v.stretchSrcPos = v.startSample ;
312+ v.direction = 1 ;
313+ }
314+ }
289315 }
290316
291317 v.stretchOutBufL .resize (kStretchBlockSize );
@@ -305,6 +331,21 @@ static void fillBungeeBlock (Voice& v, const SampleData& sample)
305331 if (! v.bungeeStretcher )
306332 return ;
307333
334+ // Ping pong boundary checks before requesting next grain
335+ if (v.pingPong )
336+ {
337+ if (v.bungeeSpeed > 0.0 && v.bungeeSrcPos >= v.endSample )
338+ {
339+ v.bungeeSrcPos = v.endSample - 1 ;
340+ v.bungeeSpeed = -std::abs (v.bungeeSpeed );
341+ }
342+ else if (v.bungeeSpeed < 0.0 && v.bungeeSrcPos <= v.startSample )
343+ {
344+ v.bungeeSrcPos = v.startSample ;
345+ v.bungeeSpeed = std::abs (v.bungeeSpeed );
346+ }
347+ }
348+
308349 auto & stretcher = *v.bungeeStretcher ;
309350
310351 // Set up the request for this grain
@@ -327,13 +368,16 @@ static void fillBungeeBlock (Voice& v, const SampleData& sample)
327368 int maxIn = stretcher.maxInputFrameCount ();
328369 v.bungeeInputBuf .resize ((size_t ) maxIn * 2 );
329370
371+ // Determine effective end for reading (release tail allows reading past slice end)
372+ int effectiveEnd = v.releaseTail && !v.pingPong ? v.sampleEnd : v.endSample ;
373+
330374 // Fill input buffer (non-interleaved: ch0 then ch1)
331375 for (int i = 0 ; i < numFrames; ++i)
332376 {
333377 double pos = inputChunk.begin + i;
334378
335379 float sL = 0 .0f , sR = 0 .0f ;
336- if (pos >= v.startSample && pos < v. endSample )
380+ if (pos >= v.startSample && pos < effectiveEnd )
337381 {
338382 sL = sample.getInterpolatedSample (pos, 0 );
339383 sR = sample.getInterpolatedSample (pos, 1 );
@@ -346,8 +390,8 @@ static void fillBungeeBlock (Voice& v, const SampleData& sample)
346390 int muteHead = 0 , muteTail = 0 ;
347391 if (inputChunk.begin < v.startSample )
348392 muteHead = v.startSample - inputChunk.begin ;
349- if (inputChunk.end > v. endSample )
350- muteTail = inputChunk.end - v. endSample ;
393+ if (inputChunk.end > effectiveEnd )
394+ muteTail = inputChunk.end - effectiveEnd ;
351395
352396 stretcher.analyseGrain (v.bungeeInputBuf .data (), (intptr_t ) maxIn, muteHead, muteTail);
353397
@@ -407,10 +451,24 @@ void VoicePool::processSample (const SampleData& sample, double sampleRate,
407451 // Fill output buffer if empty
408452 if (v.stretchOutReadPos >= v.stretchOutAvail )
409453 {
410- if (v.stretchSrcPos >= v.endSample && !v.pingPong )
454+ bool pastEnd = (v.direction > 0 )
455+ ? (v.stretchSrcPos >= v.endSample )
456+ : (v.stretchSrcPos <= v.startSample );
457+
458+ if (pastEnd && !v.pingPong )
411459 {
412- if (v.envelope .getState () != AdsrEnvelope::Release)
413- v.envelope .noteOff ();
460+ if (v.releaseTail && v.stretchSrcPos < v.sampleEnd && v.stretchSrcPos >= 0 )
461+ {
462+ // Release tail: trigger noteOff but keep filling
463+ if (v.envelope .getState () != AdsrEnvelope::Release)
464+ v.envelope .noteOff ();
465+ fillStretchBlock (v, sample);
466+ }
467+ else
468+ {
469+ if (v.envelope .getState () != AdsrEnvelope::Release)
470+ v.envelope .noteOff ();
471+ }
414472 }
415473 else
416474 {
@@ -444,10 +502,23 @@ void VoicePool::processSample (const SampleData& sample, double sampleRate,
444502 // Fill output buffer if empty
445503 if (v.bungeeOutReadPos >= v.bungeeOutAvail )
446504 {
447- if (v.bungeeSrcPos >= v.endSample && !v.pingPong )
505+ bool pastEnd = (v.bungeeSpeed > 0.0 )
506+ ? (v.bungeeSrcPos >= v.endSample )
507+ : (v.bungeeSrcPos <= v.startSample );
508+
509+ if (pastEnd && !v.pingPong )
448510 {
449- if (v.envelope .getState () != AdsrEnvelope::Release)
450- v.envelope .noteOff ();
511+ if (v.releaseTail && v.bungeeSrcPos < v.sampleEnd && v.bungeeSrcPos >= 0 )
512+ {
513+ if (v.envelope .getState () != AdsrEnvelope::Release)
514+ v.envelope .noteOff ();
515+ fillBungeeBlock (v, sample);
516+ }
517+ else
518+ {
519+ if (v.envelope .getState () != AdsrEnvelope::Release)
520+ v.envelope .noteOff ();
521+ }
451522 }
452523 else
453524 {
@@ -517,6 +588,12 @@ void VoicePool::processSample (const SampleData& sample, double sampleRate,
517588 if (newPos < v.startSample )
518589 newPos += len;
519590 }
591+ else if (v.releaseTail && newPos >= v.endSample && newPos < v.sampleEnd )
592+ {
593+ // Release tail: trigger noteOff but keep reading beyond slice end
594+ if (v.envelope .getState () != AdsrEnvelope::Release)
595+ v.envelope .noteOff ();
596+ }
520597 else
521598 {
522599 if (v.envelope .getState () != AdsrEnvelope::Release)
0 commit comments