Skip to content

Commit 8502220

Browse files
committed
better rubberband loop fading
1 parent 6eec484 commit 8502220

1 file changed

Lines changed: 146 additions & 124 deletions

File tree

‎Source/Node/nodes/sampler/SamplerNode.cpp‎

Lines changed: 146 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -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.\nResample: clean, no wobble, slight formant shift.\nRubberBand: 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

251251
void 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

10991099
void 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

Comments
 (0)