diff --git a/src/AudioIO.cpp b/src/AudioIO.cpp index f0c47d226..ccaf8621d 100644 --- a/src/AudioIO.cpp +++ b/src/AudioIO.cpp @@ -2123,6 +2123,12 @@ int AudioIO::StartStream(const TransportTracks &tracks, mPlaybackMixers[ii]->Reposition( time ); mPlaybackSchedule.RealTimeInit( time ); } + + // Now that we are done with SetTrackTime(): + mTimeQueue.mLastTime = mPlaybackSchedule.GetTrackTime(); + if (mTimeQueue.mData) + mTimeQueue.mData[0] = mTimeQueue.mLastTime; + // else recording only without overdub #ifdef EXPERIMENTAL_SCRUBBING_SUPPORT if (scrubbing) @@ -4021,6 +4027,15 @@ void AudioIO::FillBuffers() toProcess = 0; #endif + // Update the time queue. This must be done before writing to the + // ring buffers of samples, for proper synchronization with the + // consumer side in the PortAudio thread, which reads the time + // queue after reading the sample queues. The sample queues use + // atomic variables, the time queue doesn't. + mTimeQueue.Producer( mPlaybackSchedule, mRate, + (mPlaybackSchedule.Interactive() ? mScrubSpeed : 1.0), + frames); + for (i = 0; i < mPlaybackTracks.size(); i++) { // The mixer here isn't actually mixing: it's just doing @@ -4069,16 +4084,22 @@ void AudioIO::FillBuffers() else { mSilentScrub = (endSample == startSample); + double startTime, endTime; + startTime = startSample.as_double() / mRate; + endTime = endSample.as_double() / mRate; + auto diff = (endSample - startSample).as_long_long(); + if (mScrubDuration == 0) + mScrubSpeed = 0; + else + mScrubSpeed = + double(std::abs(diff)) / mScrubDuration.as_double(); if (!mSilentScrub) { - double startTime, endTime, speed; - startTime = startSample.as_double() / mRate; - endTime = endSample.as_double() / mRate; - auto diff = (endSample - startSample).as_long_long(); - speed = double(std::abs(diff)) / mScrubDuration.as_double(); for (i = 0; i < mPlaybackTracks.size(); i++) - mPlaybackMixers[i]->SetTimesAndSpeed(startTime, endTime, speed); + mPlaybackMixers[i]->SetTimesAndSpeed( + startTime, endTime, mScrubSpeed); } + mTimeQueue.mLastTime = startTime; } } } @@ -5046,6 +5067,8 @@ int AudioIO::AudioCallback(const void *inputBuffer, void *outputBuffer, if (mStreamToken > 0) { + decltype(framesPerBuffer) maxLen = 0; + // // Mix and copy to PortAudio's output buffer // @@ -5118,7 +5141,6 @@ int AudioIO::AudioCallback(const void *inputBuffer, void *outputBuffer, bool selected = false; int group = 0; int chanCnt = 0; - decltype(framesPerBuffer) maxLen = 0; // Choose a common size to take from all ring buffers const auto toGet = @@ -5200,6 +5222,10 @@ int AudioIO::AudioCallback(const void *inputBuffer, void *outputBuffer, // PRL: Bug1104: // There can be a difference of len in different loop passes if one channel // of a stereo track ends before the other! Take a max! + + // PRL: More recent rewrites of FillBuffers should guarantee a + // padding out of the ring buffers so that equal lengths are + // available, so maxLen ought to increase from 0 only once maxLen = std::max(maxLen, len); @@ -5231,7 +5257,7 @@ int AudioIO::AudioCallback(const void *inputBuffer, void *outputBuffer, } #endif - // Last channel seen now + // Last channel of a track seen now len = maxLen; if( !cutQuickly && selected ) @@ -5316,12 +5342,10 @@ int AudioIO::AudioCallback(const void *inputBuffer, void *outputBuffer, CallbackCheckCompletion(callbackReturn, 0); #ifdef EXPERIMENTAL_SCRUBBING_SUPPORT - // Update the current time position, for scrubbing - // "Consume" only as much as the ring buffers produced, which may - // be less than framesPerBuffer (during "stutter") // wxASSERT( maxLen == toGet ); if (mPlaybackSchedule.Interactive()) - mPlaybackSchedule.SetTrackTime( mScrubQueue->Consumer( maxLen ) ); + // Just do this to free space in scrub queue + mScrubQueue->Consumer( maxLen ); #endif em.RealtimeProcessEnd(); @@ -5455,10 +5479,12 @@ int AudioIO::AudioCallback(const void *inputBuffer, void *outputBuffer, } } - // Update the current time position if not scrubbing - // (Already did it above, for scrubbing) - - mPlaybackSchedule.TrackTimeUpdate( framesPerBuffer / mRate ); + // Update the position seen by drawing code + if (mPlaybackSchedule.Interactive()) + // To do: do this in all cases and remove TrackTimeUpdate + mPlaybackSchedule.SetTrackTime( mTimeQueue.Consumer( maxLen, mRate ) ); + else + mPlaybackSchedule.TrackTimeUpdate( framesPerBuffer / mRate ); // Record the reported latency from PortAudio. // TODO: Don't recalculate this with every callback? @@ -5707,6 +5733,63 @@ void AudioIO::PlaybackSchedule::TrackTimeUpdate(double realElapsed) SetTrackTime( newTime ); } +void AudioIO::TimeQueue::Producer( + const PlaybackSchedule &schedule, double rate, double scrubSpeed, + size_t nSamples ) +{ + if ( ! mData ) + // Recording only. Don't fill the queue. + return; + + // Don't check available space: assume it is enough because of coordination + // with RingBuffer. + auto index = mTail.mIndex; + auto time = mLastTime; + auto remainder = mTail.mRemainder; + auto space = TimeQueueGrainSize - remainder; + + while ( nSamples >= space ) { + time = schedule.AdvancedTrackTime( time, space / rate, scrubSpeed ); + index = (index + 1) % mSize; + mData[ index ] = time; + nSamples -= space; + remainder = 0; + space = TimeQueueGrainSize; + } + + // Last odd lot + if ( nSamples > 0 ) + time = schedule.AdvancedTrackTime( time, nSamples / rate, scrubSpeed ); + + mLastTime = time; + mTail.mRemainder = remainder + nSamples; + mTail.mIndex = index; +} + +double AudioIO::TimeQueue::Consumer( size_t nSamples, double rate ) +{ + if ( ! mData ) { + // Recording only. No scrub or playback time warp. Don't use the queue. + return ( mLastTime += nSamples / rate ); + } + + // Don't check available space: assume it is enough because of coordination + // with RingBuffer. + auto remainder = mHead.mRemainder; + auto space = TimeQueueGrainSize - remainder; + if ( nSamples >= space ) { + remainder = 0, + mHead.mIndex = (mHead.mIndex + 1) % mSize, + nSamples -= space; + if ( nSamples >= TimeQueueGrainSize ) + mHead.mIndex = + (mHead.mIndex + ( nSamples / TimeQueueGrainSize ) ) % mSize, + nSamples %= TimeQueueGrainSize; + } + mHead.mRemainder = remainder + nSamples; + return mData[ mHead.mIndex ]; +} + double AudioIO::PlaybackSchedule::TrackDuration(double realElapsed) const { if (mTimeTrack) diff --git a/src/AudioIO.h b/src/AudioIO.h index 08b78a84d..46b96b446 100644 --- a/src/AudioIO.h +++ b/src/AudioIO.h @@ -807,6 +807,7 @@ private: std::unique_ptr mScrubQueue; bool mSilentScrub; + double mScrubSpeed; sampleCount mScrubDuration; #endif @@ -989,6 +990,7 @@ private: struct TimeQueue { Doubles mData; size_t mSize{ 0 }; + double mLastTime {}; // These need not be updated atomically, because we rely on the atomics // in the playback ring buffers to supply the synchronization. Still, // align them to avoid false sharing. @@ -996,7 +998,12 @@ private: size_t mIndex {}; size_t mRemainder {}; } mHead, mTail; - } mTimeQueue; + + void Producer( + const PlaybackSchedule &schedule, double rate, double scrubSpeed, + size_t nSamples ); + double Consumer( size_t nSamples, double rate ); + } mTimeQueue; }; #endif