diff --git a/src/AudioIO.cpp b/src/AudioIO.cpp index 05422afec..800766d02 100644 --- a/src/AudioIO.cpp +++ b/src/AudioIO.cpp @@ -503,6 +503,8 @@ enum { MIDI_MINIMAL_LATENCY_MS = 1 }; +constexpr size_t TimeQueueGrainSize = 2000; + #ifdef EXPERIMENTAL_SCRUBBING_SUPPORT #include "tracks/ui/Scrubbing.h" @@ -521,25 +523,18 @@ enum { /* -This work queue class, with the aid of the playback ring -buffers, coordinates three threads during scrub play: +This work queue class coordinates two threads during scrub play: The UI thread which specifies scrubbing intervals to play, -The Audio thread which consumes those specifications a first time -and fills the ring buffers with samples for play, - -The PortAudio thread which consumes from the ring buffers, then -also consumes a second time from this queue, -to figure out how to update mTime - --- which the UI thread, in turn, uses to redraw the play head indicator -in the right place. +and the Audio thread which consumes those specifications +and fills the ring buffers with samples for play (to be consumed by yet another +thread, spawned by PortAudio). Audio produces samples for PortAudio, which consumes them, both in -approximate real time. The UI thread might go idle and so the others +approximate real time. The UI thread might go idle and so Audio might catch up, emptying the queue and causing scrub to go silent. -The UI thread will not normally outrun the others -- because InitEntry() +The UI thread will not normally outrun Audio -- because InitEntry() limits the real time duration over which each enqueued interval will play. So a small, fixed queue size should be adequate. */ @@ -569,13 +564,6 @@ struct AudioIO::ScrubQueue // If not, we can wait to enqueue again later dd.Cancel(); } - - // So the play indicator starts out unconfused: - { - Entry &entry = mEntries[mTrailingIdx]; - entry.mS0 = entry.mS1 = s0; - entry.mPlayed = entry.mDuration = 1; - } } ~ScrubQueue() {} @@ -654,16 +642,16 @@ struct AudioIO::ScrubQueue { // ?? // Queue wasn't long enough. Write side (UI thread) - // has overtaken the trailing read side (PortAudio thread), despite + // has overtaken the trailing read side (Audio thread), despite // my comments above! We lose some work requests then. // wxASSERT(false); return false; } } - void Transformer(sampleCount &startSample, sampleCount &endSample, - sampleCount &duration, - Maybe &cleanup) + void Consumer(sampleCount &startSample, sampleCount &endSample, + sampleCount &duration, + Maybe &cleanup) { // Audio thread is ready for the next interval. @@ -704,7 +692,8 @@ struct AudioIO::ScrubQueue // Discard entire queue entry mDebt -= dur; toDiscard -= dur; - dur = 0; // So Consumer() will handle abandoned entry correctly + dur = 0; + mTrailingIdx = mMiddleIdx; mMiddleIdx = (mMiddleIdx + 1) % Size; } else { @@ -733,6 +722,7 @@ struct AudioIO::ScrubQueue startSample = entry.mS0; endSample = entry.mS1; duration = entry.mDuration; + mTrailingIdx = mMiddleIdx; mMiddleIdx = (mMiddleIdx + 1) % Size; mCredit += duration; } @@ -745,42 +735,6 @@ struct AudioIO::ScrubQueue mLastTransformerTimeMillis = now; } - double Consumer(unsigned long frames) - { - // Portaudio thread consumes samples and must update - // the time for the indicator. This finds the time value. - - // MAY ADVANCE mTrailingIdx, BUT IT NEVER CATCHES UP TO mMiddleIdx. - - wxMutexLocker locker(mUpdating); - - // Mark entries as partly or fully "consumed" for - // purposes of mTime update. It should not happen that - // frames exceed the total of samples to be consumed, - // but in that case we just use the t1 of the latest entry. - while (1) - { - Entry *pEntry = &mEntries[mTrailingIdx]; - auto remaining = pEntry->mDuration - pEntry->mPlayed; - if (frames >= remaining) - { - // remaining is not more than frames - frames -= remaining.as_size_t(); - pEntry->mPlayed = pEntry->mDuration; - } - else - { - pEntry->mPlayed += frames; - break; - } - const unsigned next = (mTrailingIdx + 1) % Size; - if (next == mMiddleIdx) - break; - mTrailingIdx = next; - } - return mEntries[mTrailingIdx].GetTime(mRate); - } - private: struct Entry { @@ -789,7 +743,6 @@ private: , mS1(0) , mGoal(0) , mDuration(0) - , mPlayed(0) {} bool Init(Entry *previous, sampleCount s0, sampleCount s1, @@ -912,7 +865,6 @@ private: mS0 = s0; mS1 = s1; - mPlayed = 0; mDuration = duration; return true; } @@ -921,18 +873,9 @@ private: { mGoal = previous.mGoal; mS0 = mS1 = previous.mS1; - mPlayed = 0; mDuration = duration; } - double GetTime(double rate) const - { - return - (mS0.as_double() + - (mS1 - mS0).as_double() * mPlayed.as_double() / mDuration.as_double()) - / rate; - } - // These sample counts are initialized in the UI, producer, thread: sampleCount mS0; sampleCount mS1; @@ -944,10 +887,6 @@ private: // The middleman Audio thread does not change these entries, but only // changes indices in the queue structure. - - // This increases from 0 to mDuration as the PortAudio, consumer, - // thread catches up. When they are equal, this entry can be discarded: - sampleCount mPlayed; }; struct Duration { @@ -2001,6 +1940,7 @@ int AudioIO::StartStream(const TransportTracks &tracks, mPlaybackMixers.reset(); mCaptureBuffers.reset(); mResample.reset(); + mTimeQueue.mData.reset(); #ifdef EXPERIMENTAL_MIDI_OUT streamStartTime = 0; @@ -2120,6 +2060,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) @@ -2277,6 +2223,8 @@ bool AudioIO::AllocateBuffers( mMinCaptureSecsToCopy = 0.2 + 0.2 * std::min(size_t(16), mCaptureTracks.size()); + mTimeQueue.mHead = {}; + mTimeQueue.mTail = {}; bool bDone; do { @@ -2321,6 +2269,11 @@ bool AudioIO::AllocateBuffers( mPlaybackBuffers[i] = std::make_unique(floatSample, playbackBufferSize); + const auto timeQueueSize = + (playbackBufferSize + TimeQueueGrainSize - 1) + / TimeQueueGrainSize; + mTimeQueue.mData.reinit( timeQueueSize ); + mTimeQueue.mSize = timeQueueSize; // use track time for the end time, not real time! WaveTrackConstArray mixTracks; @@ -2419,6 +2372,7 @@ void AudioIO::StartStreamCleanup(bool bOnlyBuffers) mPlaybackMixers.reset(); mCaptureBuffers.reset(); mResample.reset(); + mTimeQueue.mData.reset(); if(!bOnlyBuffers) { @@ -2767,6 +2721,7 @@ void AudioIO::StopStream() { mPlaybackBuffers.reset(); mPlaybackMixers.reset(); + mTimeQueue.mData.reset(); } // @@ -3365,7 +3320,7 @@ AudioThread::ExitCode AudioThread::Entry() gAudioIO->mAudioThreadFillBuffersLoopActive = false; if (gAudioIO->mPlaybackSchedule.Interactive()) { - // Rely on the Wait() in ScrubQueue::Transformer() + // Rely on the Wait() in ScrubQueue::Consumer() // This allows the scrubbing update interval to be made very short without // playback becoming intermittent. } @@ -3975,10 +3930,12 @@ void AudioIO::FillBuffers() // How many samples to produce for each channel. auto frames = available; bool progress = true; + auto toProcess = frames; #ifdef EXPERIMENTAL_SCRUBBING_SUPPORT if (mPlaybackSchedule.Interactive()) // scrubbing and play-at-speed are not limited by the real time // and length accumulators + toProcess = frames = limitSampleBufferSize(frames, mScrubDuration); else #endif @@ -3987,6 +3944,7 @@ void AudioIO::FillBuffers() if (deltat > realTimeRemaining) { frames = realTimeRemaining * mRate; + toProcess = frames; // Don't fall into an infinite loop, if loop-playing a selection // that is so short, it has no samples: detect that case progress = @@ -4000,56 +3958,41 @@ void AudioIO::FillBuffers() } if (!progress) - frames = available; + frames = available, toProcess = 0; +#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT + else if ( mPlaybackSchedule.Interactive() && mSilentScrub) + 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 // resampling, format conversion, and possibly time track // warping - decltype(mPlaybackMixers[i]->Process(frames)) - processed = 0; samplePtr warpedSamples; - //don't do anything if we have no length. In particular, Process() will fail an wxAssert - //that causes a crash since this is not the GUI thread and wxASSERT is a GUI call. - // don't generate either if scrubbing at zero speed. -#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT - const bool silent = - mPlaybackSchedule.Interactive() && mSilentScrub; -#else - const bool silent = false; -#endif - - if (progress && !silent && frames > 0) + if (frames > 0) { - processed = mPlaybackMixers[i]->Process(frames); - wxASSERT(processed <= frames); + size_t processed = 0; + if ( toProcess ) + processed = mPlaybackMixers[i]->Process( toProcess ); + //wxASSERT(processed <= toProcess); warpedSamples = mPlaybackMixers[i]->GetBuffer(); - const auto put = mPlaybackBuffers[i]->Put - (warpedSamples, floatSample, processed); - // wxASSERT(put == processed); + const auto put = mPlaybackBuffers[i]->Put( + warpedSamples, floatSample, processed, frames - processed); + // wxASSERT(put == frames); // but we can't assert in this thread wxUnusedVar(put); - } - - //if looping and processed is less than the full chunk/block/buffer that gets pulled from - //other longer tracks, then we still need to advance the ring buffers or - //we'll trip up on ourselves when we start them back up again. - //if not looping we never start them up again, so its okay to not do anything - // If scrubbing, we may be producing some silence. Otherwise this should not happen, - // but makes sure anyway that we produce equal - // numbers of samples for all channels for this pass of the do-loop. - if(processed < frames && !mPlaybackSchedule.PlayingStraight()) - { - mSilentBuf.Resize(frames, floatSample); - ClearSamples(mSilentBuf.ptr(), floatSample, 0, frames); - const auto put = mPlaybackBuffers[i]->Put - (mSilentBuf.ptr(), floatSample, frames - processed); - // wxASSERT(put == frames - processed); - // but we can't assert in this thread - wxUnusedVar(put); - } + } } available -= frames; @@ -4067,7 +4010,7 @@ void AudioIO::FillBuffers() if (!done && mScrubDuration <= 0) { sampleCount startSample, endSample; - mScrubQueue->Transformer(startSample, endSample, mScrubDuration, cleanup); + mScrubQueue->Consumer(startSample, endSample, mScrubDuration, cleanup); if (mScrubDuration < 0) { // Can't play anything @@ -4078,16 +4021,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; } } } @@ -5055,6 +5004,8 @@ int AudioIO::AudioCallback(const void *inputBuffer, void *outputBuffer, if (mStreamToken > 0) { + decltype(framesPerBuffer) maxLen = 0; + // // Mix and copy to PortAudio's output buffer // @@ -5127,7 +5078,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 = @@ -5209,6 +5159,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); @@ -5240,7 +5194,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 ) @@ -5324,14 +5278,7 @@ int AudioIO::AudioCallback(const void *inputBuffer, void *outputBuffer, if (numPlaybackTracks == 0) 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 ) ); -#endif em.RealtimeProcessEnd(); @@ -5464,10 +5411,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? @@ -5646,23 +5595,18 @@ bool AudioIO::PlaybackSchedule::Overruns( double trackTime ) const return (ReversedTime() ? trackTime <= mT1 : trackTime >= mT1); } -void AudioIO::PlaybackSchedule::TrackTimeUpdate(double realElapsed) +double AudioIO::PlaybackSchedule::AdvancedTrackTime( + double time, double realElapsed, double speed ) const { - // Update mTime within the PortAudio callback - - if (Interactive()) - return; - if (ReversedTime()) realElapsed *= -1.0; - auto time = GetTrackTime(); + // Defense against cases that might cause loops not to terminate + if ( fabs(mT0 - mT1) < 1e-9 ) + return mT0; + if (mTimeTrack) { - // Defense against a case that might cause the do-loop not to terminate - if ( fabs(mT0 - mT1) < 1e-9 ) { - SetTrackTime( mT0 ); - return; - } + wxASSERT( speed == 1.0 ); double total; bool foundTotal = false; @@ -5693,7 +5637,7 @@ void AudioIO::PlaybackSchedule::TrackTimeUpdate(double realElapsed) } while ( true ); } else { - time += realElapsed; + time += realElapsed * speed; // Wrap to start if looping if (Looping()) { @@ -5705,7 +5649,77 @@ void AudioIO::PlaybackSchedule::TrackTimeUpdate(double realElapsed) } } } - SetTrackTime( time ); + + return time; +} + +void AudioIO::PlaybackSchedule::TrackTimeUpdate(double realElapsed) +{ + // Update mTime within the PortAudio callback + + if (Interactive()) + return; + + auto time = GetTrackTime(); + auto newTime = AdvancedTrackTime( time, realElapsed, 1.0 ); + 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 diff --git a/src/AudioIO.h b/src/AudioIO.h index c0e684354..46b96b446 100644 --- a/src/AudioIO.h +++ b/src/AudioIO.h @@ -778,8 +778,6 @@ private: bool mInputMixerWorks; float mMixerOutputVol; - GrowableSampleBuffer mSilentBuf; - AudioIOListener* mListener; friend class AudioThread; @@ -809,6 +807,7 @@ private: std::unique_ptr mScrubQueue; bool mSilentScrub; + double mScrubSpeed; sampleCount mScrubDuration; #endif @@ -954,6 +953,13 @@ private: // Returns true if time equals t1 or is on opposite side of t1, to t0 bool Overruns( double trackTime ) const; + // Compute the NEW track time for the given one and a real duration, + // taking into account whether the schedule is for looping + double AdvancedTrackTime( + double trackTime, double realElapsed, double speed) const; + + // Use the function above in the callback after consuming samples from the + // playback ring buffers, during usual straight or looping play void TrackTimeUpdate(double realElapsed); // Convert a nonnegative real duration to an increment of track time @@ -977,6 +983,27 @@ private: void RealTimeRestart(); } mPlaybackSchedule; + + // Another circular buffer + // Holds track time values corresponding to every nth sample in the playback + // buffers, for some large n + 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. + alignas(64) struct Cursor { + size_t mIndex {}; + size_t mRemainder {}; + } mHead, mTail; + + void Producer( + const PlaybackSchedule &schedule, double rate, double scrubSpeed, + size_t nSamples ); + double Consumer( size_t nSamples, double rate ); + } mTimeQueue; }; #endif diff --git a/src/RingBuffer.cpp b/src/RingBuffer.cpp index 50c204732..2020cd521 100644 --- a/src/RingBuffer.cpp +++ b/src/RingBuffer.cpp @@ -69,16 +69,18 @@ size_t RingBuffer::AvailForPut() } size_t RingBuffer::Put(samplePtr buffer, sampleFormat format, - size_t samplesToCopy) + size_t samplesToCopy, size_t padding) { auto start = mStart.load( std::memory_order_acquire ); auto end = mEnd.load( std::memory_order_relaxed ); - samplesToCopy = std::min( samplesToCopy, Free( start, end ) ); + const auto free = Free( start, end ); + samplesToCopy = std::min( samplesToCopy, free ); + padding = std::min( padding, free - samplesToCopy ); auto src = buffer; size_t copied = 0; auto pos = end; - while(samplesToCopy) { + while ( samplesToCopy ) { auto block = std::min( samplesToCopy, mBufferSize - pos ); CopySamples(src, format, @@ -91,6 +93,14 @@ size_t RingBuffer::Put(samplePtr buffer, sampleFormat format, copied += block; } + while ( padding ) { + const auto block = std::min( padding, mBufferSize - pos ); + ClearSamples( mBuffer.ptr(), mFormat, pos, block ); + pos = (pos + block) % mBufferSize; + padding -= block; + copied += block; + } + // Atomically update the end pointer with release, so the nonatomic writes // just done to the buffer don't get reordered after mEnd.store(pos, std::memory_order_release); diff --git a/src/RingBuffer.h b/src/RingBuffer.h index 4f993a2ff..dad54b709 100644 --- a/src/RingBuffer.h +++ b/src/RingBuffer.h @@ -24,7 +24,9 @@ class RingBuffer { // size_t AvailForPut(); - size_t Put(samplePtr buffer, sampleFormat format, size_t samples); + size_t Put(samplePtr buffer, sampleFormat format, size_t samples, + // optional number of trailing zeroes + size_t padding = 0); size_t Clear(sampleFormat format, size_t samples); //