@@ -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+
144148static 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
359378void 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
416441void 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+
426578void 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 ();
0 commit comments