@@ -56,7 +56,7 @@ SamplerNode::SamplerNode(var params) :
5656 autoKeyLiveMode = controlsCC.addBoolParameter (" Auto Key" , " If checked, hitting an unrecorded note will play it repitched from the closest found" , false );
5757 autoKeyFadeTimeMS = controlsCC.addIntParameter (" Auto Key Fade" , " Fade for anticlick when repitching auto keys, in milliseconds" , 50 );
5858
59- autoKeyLoopFade = controlsCC.addBoolParameter (" Auto Key Loop Fade" , " If checked, generated auto-key samples are shortened and overlap-blended so the loop start and end match more cleanly." , false );
59+ autoKeyLoopFade = controlsCC.addBoolParameter (" Auto Key Loop Fade" , " If checked, generated auto-key samples are shortened and overlap-blended so the loop start and end match more cleanly." , false );
6060 autoKeyMode = controlsCC.addEnumParameter (" Auto Key Algorithm" , " Algorithm used to pitch-shift when computing auto keys.\n Resample: clean, no wobble, slight formant shift.\n RubberBand: phase vocoder, preserves formants, may wobble on tonal sounds." );
6161 autoKeyMode->addOption (" RubberBand" , RUBBERBAND )->addOption (" Resample" , RESAMPLE );
6262
@@ -195,28 +195,28 @@ void SamplerNode::resetAllNotes()
195195 }
196196}
197197
198- void SamplerNode::computeAutoKeys ()
199- {
200- for (int i = 0 ; i < samplerNotes.size (); i++)
201- {
202- NoteState ns = samplerNotes[i]->state ->getValueDataAsEnum <NoteState>();
203- if (ns == PROCESSING || samplerNotes[i]->isProxyNote ()) clearNote (i);
204- }
205-
206- int startKey = startAutoKey->intValue ();
207- int endKey = endAutoKey->intValue ();
208-
209- // Grab UI settings to pass to threads
210- bool formants = rbPreserveFormants->boolValue ();
211- int transients = rbTransients->getValueDataAsEnum <int >();
212- int pitchMode = rbPitchMode->getValueDataAsEnum <int >();
213- int windowSize = rbWindowSize->getValueDataAsEnum <int >();
214- bool shouldLoopFade = autoKeyLoopFade->boolValue ();
215-
216- for (int i = startKey; i <= endKey; i++)
217- {
218- SamplerNote* n = samplerNotes[i];
219- if (n == nullptr ) return ;
198+ void SamplerNode::computeAutoKeys ()
199+ {
200+ for (int i = 0 ; i < samplerNotes.size (); i++)
201+ {
202+ NoteState ns = samplerNotes[i]->state ->getValueDataAsEnum <NoteState>();
203+ if (ns == PROCESSING || samplerNotes[i]->isProxyNote ()) clearNote (i);
204+ }
205+
206+ int startKey = startAutoKey->intValue ();
207+ int endKey = endAutoKey->intValue ();
208+
209+ // Grab UI settings to pass to threads
210+ bool formants = rbPreserveFormants->boolValue ();
211+ int transients = rbTransients->getValueDataAsEnum <int >();
212+ int pitchMode = rbPitchMode->getValueDataAsEnum <int >();
213+ int windowSize = rbWindowSize->getValueDataAsEnum <int >();
214+ bool shouldLoopFade = autoKeyLoopFade->boolValue ();
215+
216+ for (int i = startKey; i <= endKey; i++)
217+ {
218+ SamplerNote* n = samplerNotes[i];
219+ if (n == nullptr ) return ;
220220 NoteState ns = n->state ->getValueDataAsEnum <NoteState>();
221221 if (ns != EMPTY ) continue ;
222222
@@ -235,17 +235,17 @@ void SamplerNode::computeAutoKeys()
235235 {
236236 closestNote = highI;
237237 break ;
238- }
239- }
240- }
241-
242- if (closestNote != -1 )
243- {
244- double shift = MidiMessage::getMidiNoteInHertz (i) / MidiMessage::getMidiNoteInHertz (closestNote);
245- n->computeAutoKey (samplerNotes[closestNote], shift, getFadeNumSamples (autoKeyFadeTimeMS->intValue ()), autoKeyMode->getValueDataAsEnum <AutoKeyAlgorithm>(), formants, transients, pitchMode, windowSize, shouldLoopFade);
246- }
247- }
248- }
238+ }
239+ }
240+ }
241+
242+ if (closestNote != -1 )
243+ {
244+ double shift = MidiMessage::getMidiNoteInHertz (i) / MidiMessage::getMidiNoteInHertz (closestNote);
245+ n->computeAutoKey (samplerNotes[closestNote], shift, getFadeNumSamples (autoKeyFadeTimeMS->intValue ()), autoKeyMode->getValueDataAsEnum <AutoKeyAlgorithm>(), formants, transients, pitchMode, windowSize, shouldLoopFade);
246+ }
247+ }
248+ }
249249
250250
251251void SamplerNode::updateBuffers ()
@@ -1050,11 +1050,11 @@ SamplerNode::SamplerNote::~SamplerNote()
10501050 stopThread (1000 );
10511051}
10521052
1053- void SamplerNode::SamplerNote::setAutoKey (SamplerNote* remoteNote, double shift)
1054- {
1055- autoKeyFromNote = remoteNote;
1056- if (autoKeyFromNote == nullptr )
1057- {
1053+ void SamplerNode::SamplerNote::setAutoKey (SamplerNote* remoteNote, double shift)
1054+ {
1055+ autoKeyFromNote = remoteNote;
1056+ if (autoKeyFromNote == nullptr )
1057+ {
10581058 rtPitchedBuffer.clear ();
10591059 return ;
10601060 }
@@ -1073,28 +1073,28 @@ void SamplerNode::SamplerNote::setAutoKey(SamplerNote* remoteNote, double shift)
10731073 ));
10741074 }
10751075
1076- pitcher->reset ();
1077- pitcher->setMaxProcessSize (Transport::getInstance ()->blockSize );
1078- pitcher->setPitchScale (shift);
1079- rtPitchReadSample = 0 ;
1080- rtPitchedBuffer.setSize (autoKeyFromNote->buffer .getNumChannels (), Transport::getInstance ()->blockSize );
1081- }
1082-
1083- void SamplerNode::SamplerNote::computeAutoKey (SamplerNote* remoteNote, double shift, int fadeSamples, int algorithm, bool formants, int transients, int pitchMode, int windowSize, bool shouldLoopFade)
1084- {
1085- shifting = shift;
1086- fadeNumSamples = fadeSamples;
1087- autoKeyAlgorithm = algorithm;
1088- autoKeyFromNote = remoteNote;
1089-
1090- optFormants = formants;
1091- optTransients = transients;
1092- optPitchMode = pitchMode;
1093- optWindow = windowSize;
1094- loopFadeEnabled = shouldLoopFade;
1095-
1096- startThread ();
1097- }
1076+ pitcher->reset ();
1077+ pitcher->setMaxProcessSize (Transport::getInstance ()->blockSize );
1078+ pitcher->setPitchScale (shift);
1079+ rtPitchReadSample = 0 ;
1080+ rtPitchedBuffer.setSize (autoKeyFromNote->buffer .getNumChannels (), Transport::getInstance ()->blockSize );
1081+ }
1082+
1083+ void SamplerNode::SamplerNote::computeAutoKey (SamplerNote* remoteNote, double shift, int fadeSamples, int algorithm, bool formants, int transients, int pitchMode, int windowSize, bool shouldLoopFade)
1084+ {
1085+ shifting = shift;
1086+ fadeNumSamples = fadeSamples;
1087+ autoKeyAlgorithm = algorithm;
1088+ autoKeyFromNote = remoteNote;
1089+
1090+ optFormants = formants;
1091+ optTransients = transients;
1092+ optPitchMode = pitchMode;
1093+ optWindow = windowSize;
1094+ loopFadeEnabled = shouldLoopFade;
1095+
1096+ startThread ();
1097+ }
10981098
10991099void SamplerNode::SamplerNote::run ()
11001100{
@@ -1126,6 +1126,8 @@ void SamplerNode::SamplerNote::run()
11261126
11271127 AudioBuffer<float > pitchedFull (numChannels, numSamples);
11281128 pitchedFull.clear ();
1129+ AudioBuffer<float > loopContinuation (numChannels, 0 );
1130+ int renderedNumSamples = numSamples;
11291131
11301132 if (autoKeyAlgorithm == SamplerNode::RESAMPLE )
11311133 {
@@ -1170,30 +1172,24 @@ void SamplerNode::SamplerNote::run()
11701172 rubberBand.setPitchScale (safeShift);
11711173 rubberBand.setTimeRatio (1.0 );
11721174
1173- // The render can either settle into silence or include post-roll from
1174- // the start so the generated note can be overlap-looped afterward.
1175- int minAntiClick = (int )(Transport::getInstance ()->sampleRate * 0.005 );
1176- const int loopFadeSamples = jmin (jmax (0 , numSamples - 1 ), jmax (fadeNumSamples, minAntiClick));
1177- const int paddedInputSamples = numSamples + loopFadeSamples;
1178-
1179- AudioBuffer<float > paddedSource (numChannels, paddedInputSamples);
1180- paddedSource.clear ();
1181- for (int ch = 0 ; ch < numChannels; ++ch)
1182- {
1183- paddedSource.copyFrom (ch, 0 , sourceCopy, ch, 0 , numSamples);
1184-
1185- if (loopFadeSamples > 0 )
1186- {
1187- if (loopFadeEnabled)
1188- {
1189- paddedSource.copyFrom (ch, numSamples, sourceCopy, ch, 0 , loopFadeSamples);
1190- }
1191- else
1192- {
1193- paddedSource.applyGainRamp (ch, numSamples - loopFadeSamples, loopFadeSamples, 1 .0f , 0 .0f );
1194- }
1195- }
1196- }
1175+ // In loop-fade mode we append a short post-roll from the start so the
1176+ // rendered note contains the material needed to close the loop seam.
1177+ const int loopFadeSamples = loopFadeEnabled
1178+ ? jmin (jmax (0 , numSamples - 1 ), fadeNumSamples)
1179+ : 0 ;
1180+ const int paddedInputSamples = numSamples + loopFadeSamples;
1181+
1182+ AudioBuffer<float > paddedSource (numChannels, paddedInputSamples);
1183+ paddedSource.clear ();
1184+ for (int ch = 0 ; ch < numChannels; ++ch)
1185+ {
1186+ paddedSource.copyFrom (ch, 0 , sourceCopy, ch, 0 , numSamples);
1187+
1188+ if (loopFadeEnabled && loopFadeSamples > 0 )
1189+ {
1190+ paddedSource.copyFrom (ch, numSamples, sourceCopy, ch, 0 , loopFadeSamples);
1191+ }
1192+ }
11971193
11981194 rubberBand.setExpectedInputDuration ((size_t )paddedInputSamples);
11991195
@@ -1235,45 +1231,71 @@ void SamplerNode::SamplerNote::run()
12351231 }
12361232 }
12371233
1238- // 6. Copy the requested note body. Loop overlap is applied afterward so
1239- // both algorithms share the same seam treatment.
1240- for (int ch = 0 ; ch < numChannels; ++ch)
1241- {
1242- const int availableMainSamples = jmin (numSamples, collected);
1243- if (availableMainSamples > 0 )
1244- pitchedFull.copyFrom (ch, 0 , fullOutput, ch, 0 , availableMainSamples);
1245- }
1246- }
1247-
1248- if (loopFadeEnabled)
1249- {
1250- const int crossfadeSamples = jmin (jmax (0 , pitchedFull.getNumSamples () - 1 ), fadeNumSamples);
1251- if (crossfadeSamples > 0 )
1252- {
1253- const int loopedNumSamples = pitchedFull.getNumSamples () - crossfadeSamples;
1254- AudioBuffer<float > loopedBuffer (numChannels, loopedNumSamples);
1255-
1256- for (int ch = 0 ; ch < numChannels; ++ch)
1257- {
1258- loopedBuffer.copyFrom (ch, 0 , pitchedFull, ch, 0 , loopedNumSamples);
1259-
1260- for (int i = 0 ; i < crossfadeSamples; ++i)
1261- {
1262- const float alpha = crossfadeSamples == 1 ? 1 .0f : (float )i / (float )(crossfadeSamples - 1 );
1263- const float tailGain = std::cos (alpha * MathConstants<float >::halfPi);
1264- const float headGain = std::sin (alpha * MathConstants<float >::halfPi);
1265- const float tailSample = pitchedFull.getSample (ch, loopedNumSamples + i);
1266- const float headSample = pitchedFull.getSample (ch, i);
1267- loopedBuffer.setSample (ch, i, tailSample * tailGain + headSample * headGain);
1268- }
1269- }
1270-
1271- pitchedFull.makeCopyOf (loopedBuffer);
1272- }
1273- }
1274-
1275- // Replace the buffer
1276- buffer.makeCopyOf (pitchedFull);
1234+ // 6. Copy the requested note body. Loop overlap is applied afterward so
1235+ // both algorithms share the same seam treatment.
1236+ for (int ch = 0 ; ch < numChannels; ++ch)
1237+ {
1238+ const int availableMainSamples = jmin (numSamples, collected);
1239+ if (availableMainSamples > 0 )
1240+ pitchedFull.copyFrom (ch, 0 , fullOutput, ch, 0 , availableMainSamples);
1241+ }
1242+
1243+ renderedNumSamples = jmax (0 , jmin (numSamples, collected));
1244+
1245+ if (loopFadeEnabled && loopFadeSamples > 0 )
1246+ {
1247+ const int availableContinuation = jmin (loopFadeSamples, jmax (0 , collected - renderedNumSamples));
1248+ if (availableContinuation > 0 )
1249+ {
1250+ loopContinuation.setSize (numChannels, availableContinuation);
1251+ for (int ch = 0 ; ch < numChannels; ++ch)
1252+ loopContinuation.copyFrom (ch, 0 , fullOutput, ch, renderedNumSamples, availableContinuation);
1253+ }
1254+ }
1255+ }
1256+
1257+ if (loopFadeEnabled)
1258+ {
1259+ const int crossfadeSamples = jmin (jmax (0 , renderedNumSamples / 2 ), fadeNumSamples);
1260+ if (crossfadeSamples > 0 )
1261+ {
1262+ const int loopedNumSamples = renderedNumSamples - crossfadeSamples;
1263+ AudioBuffer<float > loopedBuffer (numChannels, loopedNumSamples);
1264+ loopedBuffer.clear ();
1265+ const int bodySamples = jmax (0 , loopedNumSamples - crossfadeSamples);
1266+
1267+ for (int ch = 0 ; ch < numChannels; ++ch)
1268+ {
1269+ if (bodySamples > 0 )
1270+ loopedBuffer.copyFrom (ch, 0 , pitchedFull, ch, crossfadeSamples, bodySamples);
1271+
1272+ for (int i = 0 ; i < crossfadeSamples; ++i)
1273+ {
1274+ const float alpha = crossfadeSamples == 1 ? 1 .0f : (float )i / (float )(crossfadeSamples - 1 );
1275+ const float tailGain = std::cos (alpha * MathConstants<float >::halfPi);
1276+ const float headGain = std::sin (alpha * MathConstants<float >::halfPi);
1277+ const float tailSample = loopContinuation.getNumSamples () > i
1278+ ? loopContinuation.getSample (ch, i)
1279+ : pitchedFull.getSample (ch, i);
1280+ const float headSample = pitchedFull.getSample (ch, loopedNumSamples + i);
1281+ loopedBuffer.setSample (ch, bodySamples + i, headSample * tailGain + tailSample * headGain);
1282+ }
1283+ }
1284+
1285+ pitchedFull.makeCopyOf (loopedBuffer);
1286+ renderedNumSamples = loopedNumSamples;
1287+ }
1288+ }
1289+ else if (renderedNumSamples > 0 && renderedNumSamples < pitchedFull.getNumSamples ())
1290+ {
1291+ AudioBuffer<float > trimmedBuffer (numChannels, renderedNumSamples);
1292+ trimmedBuffer.makeCopyOf (pitchedFull, true );
1293+ trimmedBuffer.setSize (numChannels, renderedNumSamples, true , false , true );
1294+ pitchedFull.makeCopyOf (trimmedBuffer);
1295+ }
1296+
1297+ // Replace the buffer
1298+ buffer.makeCopyOf (pitchedFull);
12771299
12781300 autoKeyFromNote = nullptr ;
12791301 state->setValueWithData (NoteState::FILLED );
0 commit comments