Support for scrubbing in playback engine, but unused
This commit is contained in:
parent
5abfd25a34
commit
d988c3329f
602
src/AudioIO.cpp
602
src/AudioIO.cpp
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
27
src/Mix.cpp
27
src/Mix.cpp
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue