Support for scrubbing in playback engine, but unused

This commit is contained in:
Paul-Licameli 2015-04-16 21:31:10 -04:00
parent 5abfd25a34
commit d988c3329f
5 changed files with 642 additions and 68 deletions

View File

@ -341,6 +341,296 @@ wxArrayLong AudioIO::mCachedSampleRates;
double AudioIO::mCachedBestRateIn = 0.0;
double AudioIO::mCachedBestRateOut;
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
/*
This work queue class, with the aid of the playback ring
buffers, coordinates three 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.
Audio produces samples for PortAudio, which consumes them, both in
approximate real time. The UI thread might go idle and so the others
might catch up, emptying the queue and causing scrub to go silent.
The UI thread will not normally outrun the others -- because InitEntry()
limits the real time duration over which each enqueued interval will play.
So a small, fixed queue size should be adequate.
*/
struct AudioIO::ScrubQueue
{
ScrubQueue(double t0, double t1, wxLongLong startClockMillis,
double rate, double maxSpeed, double minStutter)
: mTrailingIdx(0)
, mMiddleIdx(1)
, mLeadingIdx(2)
, mRate(rate)
, mMinStutter(lrint(std::max(0.0, minStutter) * mRate))
, mLastScrubTimeMillis(startClockMillis)
, mUpdating()
{
bool success = InitEntry(mEntries[mMiddleIdx],
t0, t1, maxSpeed, false, NULL, false);
if (!success)
{
// StartClock equals now? Really?
--mLastScrubTimeMillis;
success = InitEntry(mEntries[mMiddleIdx],
t0, t1, maxSpeed, false, NULL, false);
}
wxASSERT(success);
// So the play indicator starts out unconfused:
{
Entry &entry = mEntries[mTrailingIdx];
entry.mS0 = entry.mS1 = mEntries[mMiddleIdx].mS0;
entry.mPlayed = entry.mDuration = 1;
}
}
~ScrubQueue() {}
bool Producer(double startTime, double end, double maxSpeed, bool bySpeed, bool maySkip)
{
// Main thread indicates a scrubbing interval
// MAY ADVANCE mLeadingIdx, BUT IT NEVER CATCHES UP TO mTrailingIdx.
wxCriticalSectionLocker locker(mUpdating);
const unsigned next = (mLeadingIdx + 1) % Size;
if (next != mTrailingIdx)
{
Entry &previous = mEntries[(mLeadingIdx + Size - 1) % Size];
if (startTime < 0.0)
// Use the previous end as new start.
startTime = previous.mS1 / mRate;
// Might reject the request because of zero duration,
// or a too-short "stutter"
const bool success =
(InitEntry(mEntries[mLeadingIdx], startTime, end, maxSpeed,
bySpeed, &previous, maySkip));
if (success)
mLeadingIdx = next;
return success;
}
else
{
// ??
// Queue wasn't long enough. Write side (UI thread)
// has overtaken the trailing read side (PortAudio thread), despite
// my comments above! We lose some work requests then.
// wxASSERT(false);
return false;
}
}
void Transformer(long &startSample, long &endSample, long &duration)
{
// Audio thread is ready for the next interval.
// MAY ADVANCE mMiddleIdx, WHICH MAY EQUAL mLeadingIdx, BUT DOES NOT PASS IT.
wxCriticalSectionLocker locker(mUpdating);
if (mMiddleIdx != mLeadingIdx)
{
// There is work in the queue
Entry &entry = mEntries[mMiddleIdx];
startSample = entry.mS0;
endSample = entry.mS1;
duration = entry.mDuration;
const unsigned next = (mMiddleIdx + 1) % Size;
mMiddleIdx = next;
}
else
{
// next entry is not yet ready
startSample = endSample = duration = -1L;
}
}
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.
wxCriticalSectionLocker 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];
unsigned long remaining = pEntry->mDuration - pEntry->mPlayed;
if (frames >= remaining)
{
frames -= remaining;
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
{
Entry()
: mS0(0)
, mS1(0)
, mGoal(0)
, mDuration(0)
, mPlayed(0)
{}
bool Init(long s0, long s1, long duration, Entry *previous,
double maxSpeed, long minStutter, bool adjustStart)
{
if (duration <= 0)
return false;
double speed = double(abs(s1 - s0)) / duration;
bool maxed = false;
// May change the requested speed (or reject)
if (speed > maxSpeed)
{
// Reduce speed to the maximum selected in the user interface.
speed = maxSpeed;
maxed = true;
}
else if (!adjustStart &&
previous &&
previous->mGoal >= 0 &&
previous->mGoal == s1)
{
// In case the mouse has not moved, and playback
// is catching up to the mouse at maximum speed,
// continue at no less than maximum. (Without this
// the final catch-up can make a slow scrub interval
// that drops the pitch and sounds wrong.)
duration = lrint(speed * duration / maxSpeed);
if (duration <= 0)
{
previous->mGoal = -1;
return false;
}
speed = maxSpeed;
maxed = true;
}
if (maxed)
{
// When playback follows a fast mouse movement by "stuttering"
// at maximum playback, don't make stutters too short to be useful.
if (adjustStart && duration < minStutter)
return false;
}
else if (speed < GetMinScrubSpeed())
// Mixers were set up to go only so slowly, not slower.
// This will put a request for some silence in the work queue.
speed = 0.0;
// No more rejections.
// Before we change s1:
mGoal = maxed ? s1 : -1;
// May change s1 or s0 to match speed change:
long diff = lrint(speed * duration);
if (adjustStart)
{
if (s0 < s1)
s0 = s1 - diff;
else
s0 = s1 + diff;
}
else
{
// adjust end
if (s0 < s1)
s1 = s0 + diff;
else
s1 = s0 - diff;
}
mS0 = s0;
mS1 = s1;
mPlayed = 0;
mDuration = duration;
return true;
}
double GetTime(double rate) const
{
return (mS0 + ((mS1 - mS0) * mPlayed) / double(mDuration)) / rate;
}
// These sample counts are initialized in the UI, producer, thread:
long mS0;
long mS1;
long mGoal;
// This field is initialized in the UI thread too, and
// this work queue item corresponds to exactly this many samples of
// playback output:
long mDuration;
// 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:
long mPlayed;
};
bool InitEntry(Entry &entry, double t0, double end, double maxSpeed,
bool bySpeed, Entry *previous, bool maySkip)
{
const wxLongLong clockTime(::wxGetLocalTimeMillis());
const long duration =
mRate * (clockTime - mLastScrubTimeMillis).ToDouble() / 1000.0;
const long s0 = t0 * mRate;
const long s1 = bySpeed
? s0 + lrint(duration * end) // end is a speed
: lrint(end * mRate); // end is a time
const bool success =
entry.Init(s0, s1, duration, previous, maxSpeed, mMinStutter, maySkip);
if (success)
mLastScrubTimeMillis = clockTime;
return success;
}
enum { Size = 10 };
Entry mEntries[Size];
unsigned mTrailingIdx;
unsigned mMiddleIdx;
unsigned mLeadingIdx;
const double mRate;
const long mMinStutter;
wxLongLong mLastScrubTimeMillis;
wxCriticalSection mUpdating;
};
#endif
const int AudioIO::StandardRates[] = {
8000,
11025,
@ -553,7 +843,7 @@ AudioIO::AudioIO()
mLastRecordingOffset = 0.0;
mNumCaptureChannels = 0;
mPaused = false;
mPlayLooped = false;
mPlayMode = PLAY_STRAIGHT;
mListener = NULL;
mUpdateMeters = false;
@ -616,6 +906,12 @@ AudioIO::AudioIO()
#endif
mLastPlaybackTimeMillis = 0;
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
mScrubQueue = NULL;
mScrubDuration = 0;
mSilentScrub = false;
#endif
}
AudioIO::~AudioIO()
@ -648,6 +944,10 @@ AudioIO::~AudioIO()
DeleteSamples(mSilentBuf);
delete mThread;
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
delete mScrubQueue;
#endif
}
void AudioIO::SetMixer(int inputSource)
@ -1203,7 +1503,7 @@ int AudioIO::StartStream(WaveTrackArray playbackTracks,
#ifdef EXPERIMENTAL_MIDI_OUT
mMidiPlaybackTracks = midiPlaybackTracks;
#endif
mPlayLooped = options.playLooped;
mPlayMode = options.playLooped ? PLAY_LOOPED : PLAY_STRAIGHT;
mCutPreviewGapStart = options.cutPreviewGapStart;
mCutPreviewGapLen = options.cutPreviewGapLen;
mPlaybackBuffers = NULL;
@ -1211,16 +1511,51 @@ int AudioIO::StartStream(WaveTrackArray playbackTracks,
mCaptureBuffers = NULL;
mResample = NULL;
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
// Scrubbing is not compatible with looping or recording or a time track!
const double scrubDelay = lrint(options.scrubDelay * sampleRate) / sampleRate;
bool scrubbing = (scrubDelay > 0);
double maxScrubSpeed = options.maxScrubSpeed;
double minScrubStutter = options.minScrubStutter;
if (scrubbing)
{
if (mCaptureTracks.GetCount() > 0 ||
mPlayMode == PLAY_LOOPED ||
mTimeTrack != NULL ||
options.maxScrubSpeed < GetMinScrubSpeed())
{
wxASSERT(false);
scrubbing = false;
}
}
if (scrubbing)
{
mPlayMode = PLAY_SCRUB;
}
#endif
// mWarpedTime and mWarpedLength are irrelevant when scrubbing,
// else they are used in updating mTime,
// and when not scrubbing or playing looped, mTime is also used
// in the test for termination of playback.
// with ComputeWarpedLength, it is now possible the calculate the warped length with 100% accuracy
// (ignoring accumulated rounding errors during playback) which fixes the 'missing sound at the end' bug
mWarpedTime = 0.0;
if (mTimeTrack)
// Following gives negative when mT0 > mT1
mWarpedLength = mTimeTrack->ComputeWarpedLength(mT0, mT1);
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
if (scrubbing)
mWarpedLength = 0.0;
else
mWarpedLength = mT1 - mT0;
// PRL allow backwards play
mWarpedLength = abs(mWarpedLength);
#endif
{
if (mTimeTrack)
// Following gives negative when mT0 > mT1
mWarpedLength = mTimeTrack->ComputeWarpedLength(mT0, mT1);
else
mWarpedLength = mT1 - mT0;
// PRL allow backwards play
mWarpedLength = abs(mWarpedLength);
}
//
// The RingBuffer sizes, and the max amount of the buffer to
@ -1229,8 +1564,22 @@ int AudioIO::StartStream(WaveTrackArray playbackTracks,
// killing performance.
//
// (warped) playback time to produce with each filling of the buffers
// by the Audio thread (except at the end of playback):
// usually, make fillings fewer and longer for less CPU usage.
// But for useful scrubbing, we can't run too far ahead without checking
// mouse input, so make fillings more and shorter.
// What Audio thread produces for playback is then consumed by the PortAudio
// thread, in many smaller pieces.
double playbackTime = 4.0;
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
if (scrubbing)
playbackTime = scrubDelay;
#endif
mPlaybackSamplesToCopy = playbackTime * mRate;
// Capacity of the playback buffer.
mPlaybackRingBufferSecs = 10.0;
mMaxPlaybackSecsToCopy = 4.0;
mCaptureRingBufferSecs = 4.5 + 0.5 * std::min(size_t(16), mCaptureTracks.GetCount());
mMinCaptureSecsToCopy = 0.2 + 0.2 * std::min(size_t(16), mCaptureTracks.GetCount());
@ -1306,9 +1655,9 @@ int AudioIO::StartStream(WaveTrackArray playbackTracks,
// Allocate output buffers. For every output track we allocate
// a ring buffer of five seconds
sampleCount playbackBufferSize =
(sampleCount)(mRate * mPlaybackRingBufferSecs + 0.5f);
(sampleCount)lrint(mRate * mPlaybackRingBufferSecs);
sampleCount playbackMixBufferSize =
(sampleCount)(mRate * mMaxPlaybackSecsToCopy + 0.5f);
(sampleCount)mPlaybackSamplesToCopy;
// In the extraordinarily rare case that we can't even afford 100 samples, just give up.
if(playbackBufferSize < 100 || playbackMixBufferSize < 100)
@ -1326,6 +1675,9 @@ int AudioIO::StartStream(WaveTrackArray playbackTracks,
memset(mPlaybackMixers, 0, sizeof(Mixer*)*mPlaybackTracks.GetCount());
const Mixer::WarpOptions &warpOptions =
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
scrubbing ? Mixer::WarpOptions(GetMinScrubSpeed(), GetMaxScrubSpeed()) :
#endif
Mixer::WarpOptions(mTimeTrack);
for (unsigned int i = 0; i < mPlaybackTracks.GetCount(); i++)
@ -1379,7 +1731,7 @@ int AudioIO::StartStream(WaveTrackArray playbackTracks,
// try deleting everything, halving our buffer size, and try again.
StartStreamCleanup(true);
mPlaybackRingBufferSecs *= 0.5;
mMaxPlaybackSecsToCopy *= 0.5;
mPlaybackSamplesToCopy /= 2;
mCaptureRingBufferSecs *= 0.5;
mMinCaptureSecsToCopy *= 0.5;
bDone = false;
@ -1414,6 +1766,20 @@ int AudioIO::StartStream(WaveTrackArray playbackTracks,
AILASetStartTime();
#endif
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
delete mScrubQueue;
if (scrubbing)
{
mScrubQueue =
new ScrubQueue(mT0, mT1, options.scrubStartClockTimeMillis,
sampleRate, maxScrubSpeed, minScrubStutter);
mScrubDuration = 0;
mSilentScrub = false;
}
else
mScrubQueue = NULL;
#endif
// We signal the audio thread to call FillBuffers, to prime the RingBuffers
// so that they will have data in them when the stream starts. Having the
// audio thread call FillBuffers here makes the code more predictable, since
@ -1531,6 +1897,14 @@ void AudioIO::StartStreamCleanup(bool bOnlyBuffers)
mPortStreamV19 = NULL;
mStreamToken = 0;
}
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
if (mScrubQueue)
{
delete mScrubQueue;
mScrubQueue = 0;
}
#endif
}
#ifdef EXPERIMENTAL_MIDI_OUT
@ -1943,6 +2317,14 @@ void AudioIO::StopStream()
mNumCaptureChannels = 0;
mNumPlaybackChannels = 0;
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
if (mScrubQueue)
{
delete mScrubQueue;
mScrubQueue = 0;
}
#endif
}
void AudioIO::SetPaused(bool state)
@ -1967,6 +2349,24 @@ bool AudioIO::IsPaused()
return mPaused;
}
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
bool AudioIO::EnqueueScrubByPosition(double endTime, double maxSpeed, bool maySkip)
{
if (mScrubQueue)
return mScrubQueue->Producer(-1.0, endTime, maxSpeed, false, maySkip);
else
return false;
}
bool AudioIO::EnqueueScrubBySignedSpeed(double speed, double maxSpeed, bool maySkip)
{
if (mScrubQueue)
return mScrubQueue->Producer(-1.0, speed, maxSpeed, true, maySkip);
else
return false;
}
#endif
bool AudioIO::IsBusy()
{
if (mStreamToken != 0)
@ -2026,7 +2426,12 @@ double AudioIO::NormalizeStreamTime(double absoluteTime) const
// mode. In this case, we should jump over a defined "gap" in the
// audio.
absoluteTime = LimitStreamTime(absoluteTime);
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
// Limit the time between t0 and t1 if not scrubbing.
// Should the limiting be necessary in any play mode if there are no bugs?
if (mPlayMode != PLAY_SCRUB)
#endif
absoluteTime = LimitStreamTime(absoluteTime);
if (mCutPreviewGapLen > 0)
{
@ -2810,12 +3215,7 @@ void AudioIO::FillBuffers()
// things simple, we only write as much data as is vacant in
// ALL buffers, and advance the global time by that much.
// MB: subtract a few samples because the code below has rounding errors
int commonlyAvail = GetCommonlyAvailPlayback() - 10;
//
// Determine how much this will globally advance playback time
//
double secsAvail = commonlyAvail / mRate;
int available = GetCommonlyAvailPlayback() - 10;
//
// Don't fill the buffers at all unless we can do the
@ -2826,35 +3226,44 @@ void AudioIO::FillBuffers()
// The exception is if we're at the end of the selected
// region - then we should just fill the buffer.
//
if (secsAvail >= mMaxPlaybackSecsToCopy ||
(!mPlayLooped && (secsAvail > 0 && mWarpedTime+secsAvail >= mWarpedLength)))
if (available >= mPlaybackSamplesToCopy ||
(mPlayMode == PLAY_STRAIGHT &&
available > 0 &&
mWarpedTime+(available/mRate) >= mWarpedLength))
{
// Limit maximum buffer size (increases performance)
if (secsAvail > mMaxPlaybackSecsToCopy)
secsAvail = mMaxPlaybackSecsToCopy;
double deltat; // this is warped time
if (available > mPlaybackSamplesToCopy)
available = mPlaybackSamplesToCopy;
// msmeyer: When playing a very short selection in looped
// mode, the selection must be copied to the buffer multiple
// times, to ensure, that the buffer has a reasonable size
// This is the purpose of this loop.
// PRL: or, when scrubbing, we may get work repeatedly from the
// scrub queue.
bool done = false;
do {
deltat = secsAvail;
if( mWarpedTime + deltat > mWarpedLength )
{
deltat = mWarpedLength - mWarpedTime;
mWarpedTime = mWarpedLength;
if( deltat < 0.0 ) // this should never happen
deltat = 0.0;
}
// How many samples to produce for each channel.
long frames = available;
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
if (mPlayMode == PLAY_SCRUB)
// scrubbing does not use warped time and length
frames = std::min(frames, mScrubDuration);
else
#endif
{
mWarpedTime += deltat;
double deltat = frames / mRate;
if (mWarpedTime + deltat > mWarpedLength)
{
frames = (mWarpedLength - mWarpedTime) * mRate;
mWarpedTime = mWarpedLength;
if (frames < 0) // this should never happen
frames = 0;
}
else
mWarpedTime += deltat;
}
secsAvail -= deltat;
for( i = 0; i < mPlaybackTracks.GetCount(); i++ )
{
// The mixer here isn't actually mixing: it's just doing
@ -2864,9 +3273,17 @@ void AudioIO::FillBuffers()
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.
if(deltat > 0.0)
// don't generate either if scrubbing at zero speed.
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
const bool silent = (mPlayMode == PLAY_SCRUB) && mSilentScrub;
#else
const bool silent = false;
#endif
if (!silent && frames > 0)
{
processed = mPlaybackMixers[i]->Process(lrint(deltat * mRate));
processed = mPlaybackMixers[i]->Process(frames);
wxASSERT(processed <= frames);
warpedSamples = mPlaybackMixers[i]->GetBuffer();
mPlaybackBuffers[i]->Put(warpedSamples, floatSample, processed);
}
@ -2875,31 +3292,81 @@ void AudioIO::FillBuffers()
//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(processed < lrint(deltat * mRate) && mPlayLooped)
// 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 && mPlayMode != PLAY_STRAIGHT)
{
if(mLastSilentBufSize < lrint(deltat * mRate))
if(mLastSilentBufSize < frames)
{
//delete old if necessary
if(mSilentBuf)
DeleteSamples(mSilentBuf);
mLastSilentBufSize=lrint(deltat * mRate);
mLastSilentBufSize = frames;
mSilentBuf = NewSamples(mLastSilentBufSize, floatSample);
ClearSamples(mSilentBuf, floatSample, 0, mLastSilentBufSize);
}
mPlaybackBuffers[i]->Put(mSilentBuf, floatSample, lrint(deltat * mRate) - processed);
mPlaybackBuffers[i]->Put(mSilentBuf, floatSample, frames - processed);
}
}
// msmeyer: If playing looped, check if we are at the end of the buffer
// and if yes, restart from the beginning.
if (mPlayLooped && mWarpedTime >= mWarpedLength)
{
for (i = 0; i < mPlaybackTracks.GetCount(); i++)
mPlaybackMixers[i]->Restart();
mWarpedTime = 0.0;
}
available -= frames;
wxASSERT(available >= 0);
} while (mPlayLooped && secsAvail > 0 && deltat > 0);
switch (mPlayMode)
{
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
case PLAY_SCRUB:
{
mScrubDuration -= frames;
wxASSERT(mScrubDuration >= 0);
done = (available == 0);
if (!done && mScrubDuration <= 0)
{
long startSample, endSample;
mScrubQueue->Transformer(startSample, endSample, mScrubDuration);
if (mScrubDuration < 0)
{
// Can't play anything
// Stop even if we don't fill up available
mScrubDuration = 0;
done = true;
}
else
{
mSilentScrub = (endSample == startSample);
if (!mSilentScrub)
{
double startTime, endTime, speed;
startTime = startSample / mRate;
endTime = endSample / mRate;
speed = double(abs(endSample - startSample)) / mScrubDuration;
for (i = 0; i < mPlaybackTracks.GetCount(); i++)
mPlaybackMixers[i]->SetTimesAndSpeed(startTime, endTime, speed);
}
}
}
}
break;
#endif
case PLAY_LOOPED:
{
done = (available == 0);
// msmeyer: If playing looped, check if we are at the end of the buffer
// and if yes, restart from the beginning.
if (mWarpedTime >= mWarpedLength)
{
for (i = 0; i < mPlaybackTracks.GetCount(); i++)
mPlaybackMixers[i]->Restart();
mWarpedTime = 0.0;
}
}
break;
default:
done = true;
break;
}
} while (!done);
}
} // end of playback buffering
@ -3552,6 +4019,12 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer,
}
}
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
// While scrubbing, ignore seek requests
if (gAudioIO->mSeek && gAudioIO->mPlayMode == AudioIO::PLAY_SCRUB)
gAudioIO->mSeek = 0.0;
else
#endif
if (gAudioIO->mSeek)
{
int token = gAudioIO->mStreamToken;
@ -3627,6 +4100,7 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer,
int group = 0;
int chanCnt = 0;
float rate = 0.0;
int maxLen = 0;
for (t = 0; t < numPlaybackTracks; t++)
{
WaveTrack *vt = gAudioIO->mPlaybackTracks[t];
@ -3663,7 +4137,7 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer,
// this is original code prior to r10680 -RBD
if (cut)
{
gAudioIO->mPlaybackBuffers[t]->Discard(framesPerBuffer);
len = gAudioIO->mPlaybackBuffers[t]->Discard(framesPerBuffer);
// keep going here.
// we may still need to issue a paComplete.
}
@ -3674,6 +4148,9 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer,
(int)framesPerBuffer);
chanCnt++;
}
// There should not be a difference of len in different loop passes...
// but anyway take a max.
maxLen = std::max(maxLen, len);
if (linkFlag)
@ -3714,14 +4191,15 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer,
// the end, then we've actually finished playing the entire
// selection.
// msmeyer: We never finish if we are playing looped
// PRL: or scrubbing.
if (len == 0 &&
!gAudioIO->mPlayLooped) {
gAudioIO->mPlayMode == AudioIO::PLAY_STRAIGHT) {
if ((gAudioIO->ReversedTime()
? gAudioIO->mTime <= gAudioIO->mT1
: gAudioIO->mTime >= gAudioIO->mT1))
callbackReturn = paComplete;
}
if (cut) // no samples to process, they've been discarded
continue;
@ -3770,6 +4248,14 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer,
chanCnt = 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")
if (gAudioIO->mPlayMode == AudioIO::PLAY_SCRUB)
gAudioIO->mTime = gAudioIO->mScrubQueue->Consumer(maxLen);
#endif
em.RealtimeProcessEnd();
gAudioIO->mLastPlaybackTimeMillis = ::wxGetLocalTimeMillis();
@ -3864,7 +4350,11 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer,
}
}
// Update the current time position
// Update the current time position if not scrubbing
// (Already did it above, for scrubbing)
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
if (gAudioIO->mPlayMode != AudioIO::PLAY_SCRUB)
#endif
{
double delta = framesPerBuffer / gAudioIO->mRate;
if (gAudioIO->ReversedTime())
@ -3878,7 +4368,7 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer,
}
// Wrap to start if looping
if (gAudioIO->mPlayLooped)
if (gAudioIO->mPlayMode == AudioIO::PLAY_LOOPED)
{
while (gAudioIO->ReversedTime()
? gAudioIO->mTime <= gAudioIO->mT1

View File

@ -84,6 +84,12 @@ struct AudioIOStartStreamOptions
, playLooped(false)
, cutPreviewGapStart(0.0)
, cutPreviewGapLen(0.0)
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
, scrubDelay(0.0)
, maxScrubSpeed(1.0)
, minScrubStutter(0.0)
, scrubStartClockTimeMillis(-1)
#endif
{}
TimeTrack *timeTrack;
@ -92,6 +98,24 @@ struct AudioIOStartStreamOptions
double cutPreviewGapStart;
double cutPreviewGapLen;
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
// Positive value indicates that scrubbing will happen
// (do not specify a time track, looping, or recording, which
// are all incompatible with scrubbing):
double scrubDelay;
// We need a limiting value for the speed of the first scrub
// interval:
double maxScrubSpeed;
// When maximum speed scrubbing skips to follow the mouse,
// this is the minimum amount of playback at the maximum speed:
double minScrubStutter;
// Scrubbing needs the time of start of the mouse movement that began
// the scrub:
wxLongLong scrubStartClockTimeMillis;
#endif
};
class AUDACITY_DLL_API AudioIO {
@ -137,6 +161,37 @@ class AUDACITY_DLL_API AudioIO {
* by the specified amount from where it is now */
void SeekStream(double seconds) { mSeek = seconds; }
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
static double GetMaxScrubSpeed() { return 32.0; } // Is five octaves enough for your amusement?
static double GetMinScrubSpeed() { return 0.01; }
/** \brief enqueue a new end time, using the last end as the new start,
* to be played over the same duration, as between this and the last
* enqueuing (or the starting of the stream). Except, we do not exceed maximum
* scrub speed, so may need to adjust either the start or the end.
* If maySkip is true, then when mouse movement exceeds maximum scrub speed,
* adjust the beginning of the scrub interval rather than the end, so that
* the scrub skips or "stutters" to stay near the cursor.
* But if the "stutter" is too short for the minimum, then there is no effect
* on the work queue.
* Return true if some work was really enqueued.
*/
bool EnqueueScrubByPosition(double endTime, double maxSpeed, bool maySkip);
/** \brief enqueue a new positive or negative scrubbing speed,
* using the last end as the new start,
* to be played over the same duration, as between this and the last
* enqueueing (or the starting of the stream). Except, we do not exceed maximum
* scrub speed, so may need to adjust either the start or the end.
* If maySkip is true, then when mouse movement exceeds maximum scrub speed,
* adjust the beginning of the scrub interval rather than the end, so that
* the scrub skips or "stutters" to stay near the cursor.
* But if the "stutter" is too short for the minimum, then there is no effect
* on the work queue.
* Return true if some work was really enqueued.
*/
bool EnqueueScrubBySignedSpeed(double speed, double maxSpeed, bool maySkip);
#endif
/** \brief Returns true if audio i/o is busy starting, stopping, playing,
* or recording.
*
@ -525,7 +580,7 @@ private:
double mSeek;
double mPlaybackRingBufferSecs;
double mCaptureRingBufferSecs;
double mMaxPlaybackSecsToCopy;
long mPlaybackSamplesToCopy;
double mMinCaptureSecsToCopy;
bool mPaused;
PaStream *mPortStreamV19;
@ -572,7 +627,13 @@ private:
bool mInputMixerWorks;
float mMixerOutputVol;
bool mPlayLooped;
enum {
PLAY_STRAIGHT,
PLAY_LOOPED,
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
PLAY_SCRUB,
#endif
} mPlayMode;
double mCutPreviewGapStart;
double mCutPreviewGapLen;
@ -631,6 +692,14 @@ private:
// Serialize main thread and PortAudio thread's attempts to pause and change
// the state used by the third, Audio thread.
wxMutex mSuspendAudioThread;
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
struct ScrubQueue;
ScrubQueue *mScrubQueue;
bool mSilentScrub;
long mScrubDuration;
#endif
};
#endif

View File

@ -166,4 +166,8 @@
// Define to enable Nyquist audio clip boundary control (Steve Daulton Dec 2014)
// #define EXPERIMENTAL_NYQUIST_SPLIT_CONTROL
// Paul Licameli (PRL) 16 Apr 2015
//Support for scrubbing in the AudioIO engine, without calls to it
#define EXPERIMENTAL_SCRUBBING_SUPPORT
#endif

View File

@ -275,6 +275,7 @@ Mixer::Mixer(int numInputTracks, WaveTrack **inputTracks,
mBufferSize = outBufferSize;
mInterleaved = outInterleaved;
mRate = outRate;
mSpeed = 1.0;
mFormat = outFormat;
mApplyTrackGains = true;
mGains = new float[mNumChannels];
@ -424,7 +425,7 @@ sampleCount Mixer::MixVariableRates(int *channelFlags, WaveTrack *track,
Resample * pResample)
{
const double trackRate = track->GetRate();
const double initialWarp = mRate / trackRate;
const double initialWarp = mRate / mSpeed / trackRate;
const double tstep = 1.0 / trackRate;
int sampleSize = SAMPLE_SIZE(floatSample);
@ -634,7 +635,6 @@ sampleCount Mixer::Process(sampleCount maxToProcess)
// return 0;
int i, j;
sampleCount out;
sampleCount maxOut = 0;
int *channelFlags = new int[mNumChannels];
@ -669,16 +669,14 @@ sampleCount Mixer::Process(sampleCount maxToProcess)
break;
}
}
if (mbVariableRates || track->GetRate() != mRate)
out = MixVariableRates(channelFlags, track,
&mSamplePos[i], mSampleQueue[i],
&mQueueStart[i], &mQueueLen[i], mResample[i]);
maxOut = std::max(maxOut,
MixVariableRates(channelFlags, track,
&mSamplePos[i], mSampleQueue[i],
&mQueueStart[i], &mQueueLen[i], mResample[i]));
else
out = MixSameRate(channelFlags, track, &mSamplePos[i]);
if (out > maxOut)
maxOut = out;
maxOut = std::max(maxOut,
MixSameRate(channelFlags, track, &mSamplePos[i]));
double t = (double)mSamplePos[i] / (double)track->GetRate();
if (mT0 > mT1)
@ -764,6 +762,15 @@ void Mixer::Reposition(double t)
}
}
void Mixer::SetTimesAndSpeed(double t0, double t1, double speed)
{
wxASSERT(isfinite(speed));
mT0 = t0;
mT1 = t1;
mSpeed = abs(speed);
Reposition(t0);
}
MixerSpec::MixerSpec( int numTracks, int maxNumChannels )
{
mNumTracks = mNumChannels = numTracks;

View File

@ -121,6 +121,9 @@ class AUDACITY_DLL_API Mixer {
/// Process() is called.
void Reposition(double t);
// Used in scrubbing.
void SetTimesAndSpeed(double t0, double t1, double speed);
/// Current time in seconds (unwarped, i.e. always between startTime and stopTime)
/// This value is not accurate, it's useful for progress bars and indicators, but nothing else.
double MixGetCurrentTime();
@ -175,6 +178,7 @@ class AUDACITY_DLL_API Mixer {
samplePtr *mTemp;
float *mFloatBuffer;
double mRate;
double mSpeed;
bool mHighQuality;
};