Skip to content

Commit fc4b1d7

Browse files
committed
v0.10.6: fix sample restore timeline drift
1 parent 3140998 commit fc4b1d7

11 files changed

Lines changed: 284 additions & 31 deletions

src/PluginEditor.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,8 @@ void IntersectEditor::applyTheme (const juce::String& themeName)
378378
{
379379
setTheme (theme);
380380
processor.sliceManager.setSlicePalette (getTheme().slicePalette);
381+
processor.sliceManager.recolourFromPalette();
382+
processor.markUiSnapshotDirty();
381383
float scale = processor.apvts.getRawParameterValue (ParamIds::uiScale)->load();
382384
saveUserSettings (scale, themeName);
383385
repaint();
@@ -392,6 +394,8 @@ void IntersectEditor::applyTheme (const juce::String& themeName)
392394
setTheme (ThemeData::darkTheme());
393395

394396
processor.sliceManager.setSlicePalette (getTheme().slicePalette);
397+
processor.sliceManager.recolourFromPalette();
398+
processor.markUiSnapshotDirty();
395399
float scale = processor.apvts.getRawParameterValue (ParamIds::uiScale)->load();
396400
saveUserSettings (scale, themeName);
397401
repaint();

src/PluginProcessor.cpp

Lines changed: 185 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ static Slice sanitiseRestoredSlice (Slice s)
141141
return s;
142142
}
143143

144+
constexpr int kCurrentStateVersion = 22;
145+
constexpr std::array<double, 6> kLegacyCommonSampleRates { 44100.0, 48000.0, 88200.0,
146+
96000.0, 176400.0, 192000.0 };
147+
144148
static bool isCoalescableCommand (IntersectProcessor::CommandType type)
145149
{
146150
return type == IntersectProcessor::CmdSetSliceParam
@@ -354,16 +358,36 @@ void IntersectProcessor::prepareToPlay (double sampleRate, int /*samplesPerBlock
354358
currentSampleRate = sampleRate;
355359
voicePool.setSampleRate (sampleRate);
356360
std::fill (std::begin (heldNotes), std::end (heldNotes), false);
361+
362+
const double decodedSampleRate = sampleData.getDecodedSampleRate();
363+
if (sampleData.getFilePath().isNotEmpty()
364+
&& (decodedSampleRate <= 0.0 || std::abs (decodedSampleRate - sampleRate) > 0.01))
365+
{
366+
if (sampleData.isLoaded() && sampleData.getNumFrames() > 0)
367+
{
368+
primePendingSliceTimelineRemap (kCurrentStateVersion,
369+
sampleData.getNumFrames(),
370+
decodedSampleRate,
371+
sampleData.getSourceNumFrames(),
372+
sampleData.getSourceSampleRate());
373+
}
374+
requestSampleLoad (juce::File (sampleData.getFilePath()), LoadKindRelink);
375+
}
357376
}
358377

359378
void IntersectProcessor::releaseResources() {}
360379

361-
void IntersectProcessor::requestSampleLoad (const juce::File& file, LoadKind kind)
380+
int IntersectProcessor::requestSampleLoad (const juce::File& file, LoadKind kind)
362381
{
363382
const int token = nextLoadToken.fetch_add (1, std::memory_order_relaxed) + 1;
364383
latestLoadToken.store (token, std::memory_order_release);
365384
latestLoadKind.store ((int) kind, std::memory_order_release);
366385

386+
if (kind == LoadKindReplace)
387+
clearPendingSliceTimelineRemap();
388+
else if (pendingSliceTimelineRemap.active.load (std::memory_order_acquire))
389+
pendingSliceTimelineRemap.expectedLoadToken.store (token, std::memory_order_release);
390+
367391
// Keep only the latest completed decode payload.
368392
auto* oldDecoded = completedLoadData.exchange (nullptr, std::memory_order_acq_rel);
369393
delete oldDecoded;
@@ -381,7 +405,7 @@ void IntersectProcessor::requestSampleLoad (const juce::File& file, LoadKind kin
381405
auto* old = completedLoadFailure.exchange (payload, std::memory_order_acq_rel);
382406
delete old;
383407
}
384-
return;
408+
return token;
385409
}
386410

387411
const double sr = currentSampleRate > 0.0 ? currentSampleRate : 44100.0;
@@ -411,6 +435,7 @@ void IntersectProcessor::requestSampleLoad (const juce::File& file, LoadKind kin
411435
};
412436

413437
fileLoadPool.addJob (new SampleDecodeJob (file, sr, token, kind, onSuccess, onFailure), true);
438+
return token;
414439
}
415440

416441
void IntersectProcessor::loadFileAsync (const juce::File& file)
@@ -423,6 +448,133 @@ void IntersectProcessor::relinkFileAsync (const juce::File& file)
423448
requestSampleLoad (file, LoadKindRelink);
424449
}
425450

451+
void IntersectProcessor::clearPendingSliceTimelineRemap()
452+
{
453+
pendingSliceTimelineRemap.active.store (false, std::memory_order_release);
454+
pendingSliceTimelineRemap.expectedLoadToken.store (0, std::memory_order_release);
455+
pendingSliceTimelineRemap.savedStateVersion.store (0, std::memory_order_release);
456+
pendingSliceTimelineRemap.savedDecodedNumFrames.store (0, std::memory_order_release);
457+
pendingSliceTimelineRemap.savedDecodedSampleRate.store (0.0, std::memory_order_release);
458+
pendingSliceTimelineRemap.savedSourceNumFrames.store (0, std::memory_order_release);
459+
pendingSliceTimelineRemap.savedSourceSampleRate.store (0.0, std::memory_order_release);
460+
}
461+
462+
void IntersectProcessor::primePendingSliceTimelineRemap (int savedStateVersion,
463+
int savedDecodedNumFrames,
464+
double savedDecodedSampleRate,
465+
int savedSourceNumFrames,
466+
double savedSourceSampleRate)
467+
{
468+
pendingSliceTimelineRemap.savedStateVersion.store (savedStateVersion, std::memory_order_release);
469+
pendingSliceTimelineRemap.savedDecodedNumFrames.store (savedDecodedNumFrames, std::memory_order_release);
470+
pendingSliceTimelineRemap.savedDecodedSampleRate.store (savedDecodedSampleRate, std::memory_order_release);
471+
pendingSliceTimelineRemap.savedSourceNumFrames.store (savedSourceNumFrames, std::memory_order_release);
472+
pendingSliceTimelineRemap.savedSourceSampleRate.store (savedSourceSampleRate, std::memory_order_release);
473+
pendingSliceTimelineRemap.active.store (true, std::memory_order_release);
474+
}
475+
476+
bool IntersectProcessor::applyPendingSliceTimelineRemap()
477+
{
478+
if (! pendingSliceTimelineRemap.active.load (std::memory_order_acquire))
479+
return false;
480+
481+
const int expectedToken = pendingSliceTimelineRemap.expectedLoadToken.load (std::memory_order_acquire);
482+
if (expectedToken <= 0 || expectedToken != latestLoadToken.load (std::memory_order_acquire))
483+
return false;
484+
485+
const int numSlices = sliceManager.getNumSlices();
486+
if (numSlices <= 0)
487+
{
488+
clearPendingSliceTimelineRemap();
489+
return false;
490+
}
491+
492+
const auto sampleSnap = sampleData.getSnapshot();
493+
if (sampleSnap == nullptr)
494+
return false;
495+
496+
const int targetFrames = sampleSnap->decodedNumFrames > 0
497+
? sampleSnap->decodedNumFrames
498+
: sampleSnap->buffer.getNumSamples();
499+
const double targetRate = sampleSnap->decodedSampleRate;
500+
if (targetFrames <= 0)
501+
return false;
502+
503+
int sourceFrames = pendingSliceTimelineRemap.savedDecodedNumFrames.load (std::memory_order_acquire);
504+
double sourceRate = pendingSliceTimelineRemap.savedDecodedSampleRate.load (std::memory_order_acquire);
505+
const int savedStateVersion = pendingSliceTimelineRemap.savedStateVersion.load (std::memory_order_acquire);
506+
507+
if ((sourceFrames <= 0 || sourceRate <= 0.0) && savedStateVersion < kCurrentStateVersion)
508+
{
509+
int maxEnd = 0;
510+
for (int i = 0; i < numSlices; ++i)
511+
maxEnd = juce::jmax (maxEnd, sliceManager.getSlice (i).endSample);
512+
513+
int savedSourceFrames = pendingSliceTimelineRemap.savedSourceNumFrames.load (std::memory_order_acquire);
514+
double savedSourceRate = pendingSliceTimelineRemap.savedSourceSampleRate.load (std::memory_order_acquire);
515+
if (savedSourceFrames <= 0)
516+
savedSourceFrames = sampleSnap->sourceNumFrames;
517+
if (savedSourceRate <= 0.0)
518+
savedSourceRate = sampleSnap->sourceSampleRate;
519+
if (maxEnd > 0 && savedSourceFrames > 0 && savedSourceRate > 0.0)
520+
{
521+
const double sourceDurationSec = (double) savedSourceFrames / savedSourceRate;
522+
if (sourceDurationSec > 0.0)
523+
{
524+
double bestRate = 0.0;
525+
double bestError = 1.0e12;
526+
for (double candidateRate : kLegacyCommonSampleRates)
527+
{
528+
const double expectedFrames = sourceDurationSec * candidateRate;
529+
const double error = std::abs (expectedFrames - (double) maxEnd);
530+
if (error < bestError)
531+
{
532+
bestError = error;
533+
bestRate = candidateRate;
534+
}
535+
}
536+
537+
if (bestRate > 0.0)
538+
{
539+
const double expectedFrames = sourceDurationSec * bestRate;
540+
const double relativeError = expectedFrames > 0.0 ? bestError / expectedFrames : 1.0;
541+
if (relativeError <= 0.03)
542+
{
543+
sourceRate = bestRate;
544+
sourceFrames = juce::jmax (1, (int) std::round (expectedFrames));
545+
}
546+
}
547+
}
548+
}
549+
}
550+
551+
if (sourceFrames <= 0)
552+
{
553+
clearPendingSliceTimelineRemap();
554+
return false;
555+
}
556+
557+
const double ratio = (double) targetFrames / (double) sourceFrames;
558+
const bool needsRemap = std::abs (ratio - 1.0) > 0.0001
559+
|| (sourceRate > 0.0 && targetRate > 0.0 && std::abs (sourceRate - targetRate) > 0.01);
560+
561+
if (! needsRemap)
562+
{
563+
clearPendingSliceTimelineRemap();
564+
return false;
565+
}
566+
567+
for (int i = 0; i < numSlices; ++i)
568+
{
569+
auto& s = sliceManager.getSlice (i);
570+
s.startSample = juce::jmax (0, (int) std::round ((double) s.startSample * ratio));
571+
s.endSample = juce::jmax (s.startSample + 1, (int) std::round ((double) s.endSample * ratio));
572+
}
573+
574+
clearPendingSliceTimelineRemap();
575+
return true;
576+
}
577+
426578
void IntersectProcessor::clearVoicesBeforeSampleSwap()
427579
{
428580
// Stop lazy chop before killing voices; its preview voice and buffer pointer
@@ -470,6 +622,7 @@ void IntersectProcessor::publishUiSliceSnapshot()
470622
snap.sampleLoaded = (sampleSnap != nullptr);
471623
snap.sampleMissing = sampleMissing.load (std::memory_order_relaxed);
472624
snap.sampleNumFrames = sampleSnap ? sampleSnap->buffer.getNumSamples() : 0;
625+
snap.sampleSampleRate = sampleSnap ? sampleSnap->decodedSampleRate : 0.0;
473626
if (sampleSnap != nullptr)
474627
snap.sampleFileName = sampleSnap->fileName;
475628
else if (snap.sampleMissing && missingFilePath.isNotEmpty())
@@ -813,8 +966,11 @@ void IntersectProcessor::handleCommand (const Command& cmd)
813966
if (sel >= 0 && sel < sliceManager.getNumSlices())
814967
{
815968
auto& s = sliceManager.getSlice (sel);
969+
const double timelineRate = sampleData.getDecodedSampleRate() > 0.0
970+
? sampleData.getDecodedSampleRate()
971+
: currentSampleRate;
816972
float newBpm = GrainEngine::calcStretchBpm (
817-
s.startSample, s.endSample, cmd.floatParam1, currentSampleRate);
973+
s.startSample, s.endSample, cmd.floatParam1, timelineRate);
818974
s.bpm = newBpm;
819975
s.lockMask |= kLockBpm;
820976
}
@@ -1611,6 +1767,7 @@ void IntersectProcessor::processBlock (juce::AudioBuffer<float>& buffer,
16111767
sliceManager.clearAll();
16121768
else
16131769
{
1770+
applyPendingSliceTimelineRemap();
16141771
clampSlicesToSampleBounds();
16151772
sliceManager.rebuildMidiMap();
16161773
}
@@ -1760,7 +1917,7 @@ void IntersectProcessor::getStateInformation (juce::MemoryBlock& destData)
17601917
juce::MemoryOutputStream stream (destData, false);
17611918

17621919
// Version
1763-
stream.writeInt (21);
1920+
stream.writeInt (kCurrentStateVersion);
17641921

17651922
// APVTS state
17661923
auto state = apvts.copyState();
@@ -1833,6 +1990,11 @@ void IntersectProcessor::getStateInformation (juce::MemoryBlock& destData)
18331990
stream.writeString (sampleData.getFilePath());
18341991
stream.writeString (sampleData.getFileName());
18351992

1993+
stream.writeInt (sampleData.getNumFrames());
1994+
stream.writeDouble (sampleData.getDecodedSampleRate());
1995+
stream.writeInt (sampleData.getSourceNumFrames());
1996+
stream.writeDouble (sampleData.getSourceSampleRate());
1997+
18361998
// v12: snap-to-zero-crossing toggle
18371999
stream.writeBool (snapToZeroCrossing.load());
18382000

@@ -1847,7 +2009,7 @@ void IntersectProcessor::setStateInformation (const void* data, int sizeInBytes)
18472009
juce::MemoryInputStream stream (data, (size_t) sizeInBytes, false);
18482010

18492011
int version = stream.readInt();
1850-
if (version != 19 && version != 20 && version != 21)
2012+
if (version != 19 && version != 20 && version != 21 && version != kCurrentStateVersion)
18512013
return;
18522014

18532015
// APVTS state
@@ -1934,6 +2096,18 @@ void IntersectProcessor::setStateInformation (const void* data, int sizeInBytes)
19342096
// Path-based sample restore
19352097
auto filePath = stream.readString();
19362098
auto fileName = stream.readString();
2099+
int savedDecodedNumFrames = 0;
2100+
double savedDecodedSampleRate = 0.0;
2101+
int savedSourceNumFrames = 0;
2102+
double savedSourceSampleRate = 0.0;
2103+
2104+
if (version >= kCurrentStateVersion)
2105+
{
2106+
savedDecodedNumFrames = stream.readInt();
2107+
savedDecodedSampleRate = stream.readDouble();
2108+
savedSourceNumFrames = stream.readInt();
2109+
savedSourceSampleRate = stream.readDouble();
2110+
}
19372111

19382112
clearVoicesBeforeSampleSwap();
19392113
sampleData.clear();
@@ -1946,6 +2120,11 @@ void IntersectProcessor::setStateInformation (const void* data, int sizeInBytes)
19462120
sampleData.setFileName (fileName.isNotEmpty() ? fileName : restoredFile.getFileName());
19472121
sampleData.setFilePath (filePath);
19482122
sampleAvailability.store ((int) SampleStateEmpty, std::memory_order_relaxed);
2123+
primePendingSliceTimelineRemap (version,
2124+
savedDecodedNumFrames,
2125+
savedDecodedSampleRate,
2126+
savedSourceNumFrames,
2127+
savedSourceSampleRate);
19492128
// Preserve restored slices while loading, and report missing path via relink state.
19502129
requestSampleLoad (restoredFile, LoadKindRelink);
19512130
}
@@ -1956,6 +2135,7 @@ void IntersectProcessor::setStateInformation (const void* data, int sizeInBytes)
19562135
sampleData.setFileName ({});
19572136
sampleData.setFilePath ({});
19582137
sampleAvailability.store ((int) SampleStateEmpty, std::memory_order_relaxed);
2138+
clearPendingSliceTimelineRemap();
19592139
}
19602140

19612141
sliceManager.rebuildMidiMap();

src/PluginProcessor.h

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ class IntersectProcessor : public juce::AudioProcessor
180180
bool sampleLoaded = false;
181181
bool sampleMissing = false;
182182
int sampleNumFrames = 0;
183+
double sampleSampleRate = 0.0;
183184
juce::String sampleFileName;
184185
std::array<Slice, SliceManager::kMaxSlices> slices {};
185186
};
@@ -194,6 +195,8 @@ class IntersectProcessor : public juce::AudioProcessor
194195
return uiSnapshotVersion.load (std::memory_order_acquire);
195196
}
196197

198+
void markUiSnapshotDirty() { uiSnapshotDirty.store (true, std::memory_order_release); }
199+
197200
struct MidiBoundaryPreviewState
198201
{
199202
int sliceIdx = -1;
@@ -287,6 +290,17 @@ class IntersectProcessor : public juce::AudioProcessor
287290
juce::File file;
288291
};
289292

293+
struct PendingSliceTimelineRemap
294+
{
295+
std::atomic<bool> active { false };
296+
std::atomic<int> expectedLoadToken { 0 };
297+
std::atomic<int> savedStateVersion { 0 };
298+
std::atomic<int> savedDecodedNumFrames { 0 };
299+
std::atomic<double> savedDecodedSampleRate { 0.0 };
300+
std::atomic<int> savedSourceNumFrames { 0 };
301+
std::atomic<double> savedSourceSampleRate { 0.0 };
302+
};
303+
290304
void drainCommands();
291305
void handleCommand (const Command& cmd);
292306
void processMidi (juce::MidiBuffer& midi);
@@ -299,10 +313,17 @@ class IntersectProcessor : public juce::AudioProcessor
299313
void setMidiBoundaryPreviewState (int sliceIdx, int startSample, int endSample, bool isStart);
300314
void commitMidiSliceBoundaryGestureIfIdle (int blockSamples);
301315
void clearMidiEditGestureState();
302-
void requestSampleLoad (const juce::File& file, LoadKind kind);
316+
int requestSampleLoad (const juce::File& file, LoadKind kind);
303317
void clearVoicesBeforeSampleSwap();
304318
void clampSlicesToSampleBounds();
305319
void publishUiSliceSnapshot();
320+
void clearPendingSliceTimelineRemap();
321+
void primePendingSliceTimelineRemap (int savedStateVersion,
322+
int savedDecodedNumFrames,
323+
double savedDecodedSampleRate,
324+
int savedSourceNumFrames,
325+
double savedSourceSampleRate);
326+
bool applyPendingSliceTimelineRemap();
306327
UndoManager::Snapshot makeSnapshot();
307328
void captureSnapshot();
308329
void restoreSnapshot (const UndoManager::Snapshot& snap);
@@ -348,6 +369,7 @@ class IntersectProcessor : public juce::AudioProcessor
348369
std::atomic<int> uiSliceSnapshotIndex { 0 };
349370
std::atomic<bool> uiSnapshotDirty { true };
350371
std::atomic<uint32_t> uiSnapshotVersion { 0 };
372+
PendingSliceTimelineRemap pendingSliceTimelineRemap;
351373

352374
bool heldNotes[128] = {};
353375

0 commit comments

Comments
 (0)