mirror of
https://github.com/cookiengineer/audacity
synced 2025-08-01 08:29:27 +02:00
Support for scrubbing in playback engine, but unused
This commit is contained in:
parent
5abfd25a34
commit
d988c3329f
566
src/AudioIO.cpp
566
src/AudioIO.cpp
@ -341,6 +341,296 @@ wxArrayLong AudioIO::mCachedSampleRates;
|
|||||||
double AudioIO::mCachedBestRateIn = 0.0;
|
double AudioIO::mCachedBestRateIn = 0.0;
|
||||||
double AudioIO::mCachedBestRateOut;
|
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[] = {
|
const int AudioIO::StandardRates[] = {
|
||||||
8000,
|
8000,
|
||||||
11025,
|
11025,
|
||||||
@ -553,7 +843,7 @@ AudioIO::AudioIO()
|
|||||||
mLastRecordingOffset = 0.0;
|
mLastRecordingOffset = 0.0;
|
||||||
mNumCaptureChannels = 0;
|
mNumCaptureChannels = 0;
|
||||||
mPaused = false;
|
mPaused = false;
|
||||||
mPlayLooped = false;
|
mPlayMode = PLAY_STRAIGHT;
|
||||||
|
|
||||||
mListener = NULL;
|
mListener = NULL;
|
||||||
mUpdateMeters = false;
|
mUpdateMeters = false;
|
||||||
@ -616,6 +906,12 @@ AudioIO::AudioIO()
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
mLastPlaybackTimeMillis = 0;
|
mLastPlaybackTimeMillis = 0;
|
||||||
|
|
||||||
|
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
|
||||||
|
mScrubQueue = NULL;
|
||||||
|
mScrubDuration = 0;
|
||||||
|
mSilentScrub = false;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioIO::~AudioIO()
|
AudioIO::~AudioIO()
|
||||||
@ -648,6 +944,10 @@ AudioIO::~AudioIO()
|
|||||||
DeleteSamples(mSilentBuf);
|
DeleteSamples(mSilentBuf);
|
||||||
|
|
||||||
delete mThread;
|
delete mThread;
|
||||||
|
|
||||||
|
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
|
||||||
|
delete mScrubQueue;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioIO::SetMixer(int inputSource)
|
void AudioIO::SetMixer(int inputSource)
|
||||||
@ -1203,7 +1503,7 @@ int AudioIO::StartStream(WaveTrackArray playbackTracks,
|
|||||||
#ifdef EXPERIMENTAL_MIDI_OUT
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
||||||
mMidiPlaybackTracks = midiPlaybackTracks;
|
mMidiPlaybackTracks = midiPlaybackTracks;
|
||||||
#endif
|
#endif
|
||||||
mPlayLooped = options.playLooped;
|
mPlayMode = options.playLooped ? PLAY_LOOPED : PLAY_STRAIGHT;
|
||||||
mCutPreviewGapStart = options.cutPreviewGapStart;
|
mCutPreviewGapStart = options.cutPreviewGapStart;
|
||||||
mCutPreviewGapLen = options.cutPreviewGapLen;
|
mCutPreviewGapLen = options.cutPreviewGapLen;
|
||||||
mPlaybackBuffers = NULL;
|
mPlaybackBuffers = NULL;
|
||||||
@ -1211,9 +1511,43 @@ int AudioIO::StartStream(WaveTrackArray playbackTracks,
|
|||||||
mCaptureBuffers = NULL;
|
mCaptureBuffers = NULL;
|
||||||
mResample = 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
|
// 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
|
// (ignoring accumulated rounding errors during playback) which fixes the 'missing sound at the end' bug
|
||||||
mWarpedTime = 0.0;
|
mWarpedTime = 0.0;
|
||||||
|
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
|
||||||
|
if (scrubbing)
|
||||||
|
mWarpedLength = 0.0;
|
||||||
|
else
|
||||||
|
#endif
|
||||||
|
{
|
||||||
if (mTimeTrack)
|
if (mTimeTrack)
|
||||||
// Following gives negative when mT0 > mT1
|
// Following gives negative when mT0 > mT1
|
||||||
mWarpedLength = mTimeTrack->ComputeWarpedLength(mT0, mT1);
|
mWarpedLength = mTimeTrack->ComputeWarpedLength(mT0, mT1);
|
||||||
@ -1221,6 +1555,7 @@ int AudioIO::StartStream(WaveTrackArray playbackTracks,
|
|||||||
mWarpedLength = mT1 - mT0;
|
mWarpedLength = mT1 - mT0;
|
||||||
// PRL allow backwards play
|
// PRL allow backwards play
|
||||||
mWarpedLength = abs(mWarpedLength);
|
mWarpedLength = abs(mWarpedLength);
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// The RingBuffer sizes, and the max amount of the buffer to
|
// The RingBuffer sizes, and the max amount of the buffer to
|
||||||
@ -1229,8 +1564,22 @@ int AudioIO::StartStream(WaveTrackArray playbackTracks,
|
|||||||
// killing performance.
|
// 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;
|
mPlaybackRingBufferSecs = 10.0;
|
||||||
mMaxPlaybackSecsToCopy = 4.0;
|
|
||||||
|
|
||||||
mCaptureRingBufferSecs = 4.5 + 0.5 * std::min(size_t(16), mCaptureTracks.GetCount());
|
mCaptureRingBufferSecs = 4.5 + 0.5 * std::min(size_t(16), mCaptureTracks.GetCount());
|
||||||
mMinCaptureSecsToCopy = 0.2 + 0.2 * 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
|
// Allocate output buffers. For every output track we allocate
|
||||||
// a ring buffer of five seconds
|
// a ring buffer of five seconds
|
||||||
sampleCount playbackBufferSize =
|
sampleCount playbackBufferSize =
|
||||||
(sampleCount)(mRate * mPlaybackRingBufferSecs + 0.5f);
|
(sampleCount)lrint(mRate * mPlaybackRingBufferSecs);
|
||||||
sampleCount playbackMixBufferSize =
|
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.
|
// In the extraordinarily rare case that we can't even afford 100 samples, just give up.
|
||||||
if(playbackBufferSize < 100 || playbackMixBufferSize < 100)
|
if(playbackBufferSize < 100 || playbackMixBufferSize < 100)
|
||||||
@ -1326,6 +1675,9 @@ int AudioIO::StartStream(WaveTrackArray playbackTracks,
|
|||||||
memset(mPlaybackMixers, 0, sizeof(Mixer*)*mPlaybackTracks.GetCount());
|
memset(mPlaybackMixers, 0, sizeof(Mixer*)*mPlaybackTracks.GetCount());
|
||||||
|
|
||||||
const Mixer::WarpOptions &warpOptions =
|
const Mixer::WarpOptions &warpOptions =
|
||||||
|
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
|
||||||
|
scrubbing ? Mixer::WarpOptions(GetMinScrubSpeed(), GetMaxScrubSpeed()) :
|
||||||
|
#endif
|
||||||
Mixer::WarpOptions(mTimeTrack);
|
Mixer::WarpOptions(mTimeTrack);
|
||||||
|
|
||||||
for (unsigned int i = 0; i < mPlaybackTracks.GetCount(); i++)
|
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.
|
// try deleting everything, halving our buffer size, and try again.
|
||||||
StartStreamCleanup(true);
|
StartStreamCleanup(true);
|
||||||
mPlaybackRingBufferSecs *= 0.5;
|
mPlaybackRingBufferSecs *= 0.5;
|
||||||
mMaxPlaybackSecsToCopy *= 0.5;
|
mPlaybackSamplesToCopy /= 2;
|
||||||
mCaptureRingBufferSecs *= 0.5;
|
mCaptureRingBufferSecs *= 0.5;
|
||||||
mMinCaptureSecsToCopy *= 0.5;
|
mMinCaptureSecsToCopy *= 0.5;
|
||||||
bDone = false;
|
bDone = false;
|
||||||
@ -1414,6 +1766,20 @@ int AudioIO::StartStream(WaveTrackArray playbackTracks,
|
|||||||
AILASetStartTime();
|
AILASetStartTime();
|
||||||
#endif
|
#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
|
// 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
|
// 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
|
// audio thread call FillBuffers here makes the code more predictable, since
|
||||||
@ -1531,6 +1897,14 @@ void AudioIO::StartStreamCleanup(bool bOnlyBuffers)
|
|||||||
mPortStreamV19 = NULL;
|
mPortStreamV19 = NULL;
|
||||||
mStreamToken = 0;
|
mStreamToken = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
|
||||||
|
if (mScrubQueue)
|
||||||
|
{
|
||||||
|
delete mScrubQueue;
|
||||||
|
mScrubQueue = 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef EXPERIMENTAL_MIDI_OUT
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
||||||
@ -1943,6 +2317,14 @@ void AudioIO::StopStream()
|
|||||||
|
|
||||||
mNumCaptureChannels = 0;
|
mNumCaptureChannels = 0;
|
||||||
mNumPlaybackChannels = 0;
|
mNumPlaybackChannels = 0;
|
||||||
|
|
||||||
|
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
|
||||||
|
if (mScrubQueue)
|
||||||
|
{
|
||||||
|
delete mScrubQueue;
|
||||||
|
mScrubQueue = 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioIO::SetPaused(bool state)
|
void AudioIO::SetPaused(bool state)
|
||||||
@ -1967,6 +2349,24 @@ bool AudioIO::IsPaused()
|
|||||||
return mPaused;
|
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()
|
bool AudioIO::IsBusy()
|
||||||
{
|
{
|
||||||
if (mStreamToken != 0)
|
if (mStreamToken != 0)
|
||||||
@ -2026,6 +2426,11 @@ double AudioIO::NormalizeStreamTime(double absoluteTime) const
|
|||||||
// mode. In this case, we should jump over a defined "gap" in the
|
// mode. In this case, we should jump over a defined "gap" in the
|
||||||
// audio.
|
// audio.
|
||||||
|
|
||||||
|
#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);
|
absoluteTime = LimitStreamTime(absoluteTime);
|
||||||
|
|
||||||
if (mCutPreviewGapLen > 0)
|
if (mCutPreviewGapLen > 0)
|
||||||
@ -2810,12 +3215,7 @@ void AudioIO::FillBuffers()
|
|||||||
// things simple, we only write as much data as is vacant in
|
// things simple, we only write as much data as is vacant in
|
||||||
// ALL buffers, and advance the global time by that much.
|
// ALL buffers, and advance the global time by that much.
|
||||||
// MB: subtract a few samples because the code below has rounding errors
|
// MB: subtract a few samples because the code below has rounding errors
|
||||||
int commonlyAvail = GetCommonlyAvailPlayback() - 10;
|
int available = GetCommonlyAvailPlayback() - 10;
|
||||||
|
|
||||||
//
|
|
||||||
// Determine how much this will globally advance playback time
|
|
||||||
//
|
|
||||||
double secsAvail = commonlyAvail / mRate;
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Don't fill the buffers at all unless we can do the
|
// 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
|
// The exception is if we're at the end of the selected
|
||||||
// region - then we should just fill the buffer.
|
// region - then we should just fill the buffer.
|
||||||
//
|
//
|
||||||
if (secsAvail >= mMaxPlaybackSecsToCopy ||
|
if (available >= mPlaybackSamplesToCopy ||
|
||||||
(!mPlayLooped && (secsAvail > 0 && mWarpedTime+secsAvail >= mWarpedLength)))
|
(mPlayMode == PLAY_STRAIGHT &&
|
||||||
|
available > 0 &&
|
||||||
|
mWarpedTime+(available/mRate) >= mWarpedLength))
|
||||||
{
|
{
|
||||||
// Limit maximum buffer size (increases performance)
|
// Limit maximum buffer size (increases performance)
|
||||||
if (secsAvail > mMaxPlaybackSecsToCopy)
|
if (available > mPlaybackSamplesToCopy)
|
||||||
secsAvail = mMaxPlaybackSecsToCopy;
|
available = mPlaybackSamplesToCopy;
|
||||||
|
|
||||||
double deltat; // this is warped time
|
|
||||||
|
|
||||||
// msmeyer: When playing a very short selection in looped
|
// msmeyer: When playing a very short selection in looped
|
||||||
// mode, the selection must be copied to the buffer multiple
|
// mode, the selection must be copied to the buffer multiple
|
||||||
// times, to ensure, that the buffer has a reasonable size
|
// times, to ensure, that the buffer has a reasonable size
|
||||||
// This is the purpose of this loop.
|
// This is the purpose of this loop.
|
||||||
|
// PRL: or, when scrubbing, we may get work repeatedly from the
|
||||||
|
// scrub queue.
|
||||||
|
bool done = false;
|
||||||
do {
|
do {
|
||||||
deltat = secsAvail;
|
// 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
|
||||||
|
{
|
||||||
|
double deltat = frames / mRate;
|
||||||
if (mWarpedTime + deltat > mWarpedLength)
|
if (mWarpedTime + deltat > mWarpedLength)
|
||||||
{
|
{
|
||||||
deltat = mWarpedLength - mWarpedTime;
|
frames = (mWarpedLength - mWarpedTime) * mRate;
|
||||||
mWarpedTime = mWarpedLength;
|
mWarpedTime = mWarpedLength;
|
||||||
if( deltat < 0.0 ) // this should never happen
|
if (frames < 0) // this should never happen
|
||||||
deltat = 0.0;
|
frames = 0;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
|
||||||
mWarpedTime += deltat;
|
mWarpedTime += deltat;
|
||||||
}
|
}
|
||||||
|
|
||||||
secsAvail -= deltat;
|
|
||||||
|
|
||||||
for( i = 0; i < mPlaybackTracks.GetCount(); i++ )
|
for( i = 0; i < mPlaybackTracks.GetCount(); i++ )
|
||||||
{
|
{
|
||||||
// The mixer here isn't actually mixing: it's just doing
|
// The mixer here isn't actually mixing: it's just doing
|
||||||
@ -2864,9 +3273,17 @@ void AudioIO::FillBuffers()
|
|||||||
samplePtr warpedSamples;
|
samplePtr warpedSamples;
|
||||||
//don't do anything if we have no length. In particular, Process() will fail an wxAssert
|
//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.
|
//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();
|
warpedSamples = mPlaybackMixers[i]->GetBuffer();
|
||||||
mPlaybackBuffers[i]->Put(warpedSamples, floatSample, processed);
|
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
|
//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.
|
//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 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
|
//delete old if necessary
|
||||||
if(mSilentBuf)
|
if(mSilentBuf)
|
||||||
DeleteSamples(mSilentBuf);
|
DeleteSamples(mSilentBuf);
|
||||||
mLastSilentBufSize=lrint(deltat * mRate);
|
mLastSilentBufSize = frames;
|
||||||
mSilentBuf = NewSamples(mLastSilentBufSize, floatSample);
|
mSilentBuf = NewSamples(mLastSilentBufSize, floatSample);
|
||||||
ClearSamples(mSilentBuf, floatSample, 0, mLastSilentBufSize);
|
ClearSamples(mSilentBuf, floatSample, 0, mLastSilentBufSize);
|
||||||
}
|
}
|
||||||
mPlaybackBuffers[i]->Put(mSilentBuf, floatSample, lrint(deltat * mRate) - processed);
|
mPlaybackBuffers[i]->Put(mSilentBuf, floatSample, frames - processed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
available -= frames;
|
||||||
|
wxASSERT(available >= 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
|
// msmeyer: If playing looped, check if we are at the end of the buffer
|
||||||
// and if yes, restart from the beginning.
|
// and if yes, restart from the beginning.
|
||||||
if (mPlayLooped && mWarpedTime >= mWarpedLength)
|
if (mWarpedTime >= mWarpedLength)
|
||||||
{
|
{
|
||||||
for (i = 0; i < mPlaybackTracks.GetCount(); i++)
|
for (i = 0; i < mPlaybackTracks.GetCount(); i++)
|
||||||
mPlaybackMixers[i]->Restart();
|
mPlaybackMixers[i]->Restart();
|
||||||
mWarpedTime = 0.0;
|
mWarpedTime = 0.0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} while (mPlayLooped && secsAvail > 0 && deltat > 0);
|
break;
|
||||||
|
default:
|
||||||
|
done = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (!done);
|
||||||
}
|
}
|
||||||
} // end of playback buffering
|
} // 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)
|
if (gAudioIO->mSeek)
|
||||||
{
|
{
|
||||||
int token = gAudioIO->mStreamToken;
|
int token = gAudioIO->mStreamToken;
|
||||||
@ -3627,6 +4100,7 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer,
|
|||||||
int group = 0;
|
int group = 0;
|
||||||
int chanCnt = 0;
|
int chanCnt = 0;
|
||||||
float rate = 0.0;
|
float rate = 0.0;
|
||||||
|
int maxLen = 0;
|
||||||
for (t = 0; t < numPlaybackTracks; t++)
|
for (t = 0; t < numPlaybackTracks; t++)
|
||||||
{
|
{
|
||||||
WaveTrack *vt = gAudioIO->mPlaybackTracks[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
|
// this is original code prior to r10680 -RBD
|
||||||
if (cut)
|
if (cut)
|
||||||
{
|
{
|
||||||
gAudioIO->mPlaybackBuffers[t]->Discard(framesPerBuffer);
|
len = gAudioIO->mPlaybackBuffers[t]->Discard(framesPerBuffer);
|
||||||
// keep going here.
|
// keep going here.
|
||||||
// we may still need to issue a paComplete.
|
// we may still need to issue a paComplete.
|
||||||
}
|
}
|
||||||
@ -3674,6 +4148,9 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer,
|
|||||||
(int)framesPerBuffer);
|
(int)framesPerBuffer);
|
||||||
chanCnt++;
|
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)
|
if (linkFlag)
|
||||||
@ -3714,8 +4191,9 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer,
|
|||||||
// the end, then we've actually finished playing the entire
|
// the end, then we've actually finished playing the entire
|
||||||
// selection.
|
// selection.
|
||||||
// msmeyer: We never finish if we are playing looped
|
// msmeyer: We never finish if we are playing looped
|
||||||
|
// PRL: or scrubbing.
|
||||||
if (len == 0 &&
|
if (len == 0 &&
|
||||||
!gAudioIO->mPlayLooped) {
|
gAudioIO->mPlayMode == AudioIO::PLAY_STRAIGHT) {
|
||||||
if ((gAudioIO->ReversedTime()
|
if ((gAudioIO->ReversedTime()
|
||||||
? gAudioIO->mTime <= gAudioIO->mT1
|
? gAudioIO->mTime <= gAudioIO->mT1
|
||||||
: gAudioIO->mTime >= gAudioIO->mT1))
|
: gAudioIO->mTime >= gAudioIO->mT1))
|
||||||
@ -3770,6 +4248,14 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer,
|
|||||||
chanCnt = 0;
|
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();
|
em.RealtimeProcessEnd();
|
||||||
|
|
||||||
gAudioIO->mLastPlaybackTimeMillis = ::wxGetLocalTimeMillis();
|
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;
|
double delta = framesPerBuffer / gAudioIO->mRate;
|
||||||
if (gAudioIO->ReversedTime())
|
if (gAudioIO->ReversedTime())
|
||||||
@ -3878,7 +4368,7 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wrap to start if looping
|
// Wrap to start if looping
|
||||||
if (gAudioIO->mPlayLooped)
|
if (gAudioIO->mPlayMode == AudioIO::PLAY_LOOPED)
|
||||||
{
|
{
|
||||||
while (gAudioIO->ReversedTime()
|
while (gAudioIO->ReversedTime()
|
||||||
? gAudioIO->mTime <= gAudioIO->mT1
|
? gAudioIO->mTime <= gAudioIO->mT1
|
||||||
|
@ -84,6 +84,12 @@ struct AudioIOStartStreamOptions
|
|||||||
, playLooped(false)
|
, playLooped(false)
|
||||||
, cutPreviewGapStart(0.0)
|
, cutPreviewGapStart(0.0)
|
||||||
, cutPreviewGapLen(0.0)
|
, cutPreviewGapLen(0.0)
|
||||||
|
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
|
||||||
|
, scrubDelay(0.0)
|
||||||
|
, maxScrubSpeed(1.0)
|
||||||
|
, minScrubStutter(0.0)
|
||||||
|
, scrubStartClockTimeMillis(-1)
|
||||||
|
#endif
|
||||||
{}
|
{}
|
||||||
|
|
||||||
TimeTrack *timeTrack;
|
TimeTrack *timeTrack;
|
||||||
@ -92,6 +98,24 @@ struct AudioIOStartStreamOptions
|
|||||||
double cutPreviewGapStart;
|
double cutPreviewGapStart;
|
||||||
double cutPreviewGapLen;
|
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 {
|
class AUDACITY_DLL_API AudioIO {
|
||||||
@ -137,6 +161,37 @@ class AUDACITY_DLL_API AudioIO {
|
|||||||
* by the specified amount from where it is now */
|
* by the specified amount from where it is now */
|
||||||
void SeekStream(double seconds) { mSeek = seconds; }
|
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,
|
/** \brief Returns true if audio i/o is busy starting, stopping, playing,
|
||||||
* or recording.
|
* or recording.
|
||||||
*
|
*
|
||||||
@ -525,7 +580,7 @@ private:
|
|||||||
double mSeek;
|
double mSeek;
|
||||||
double mPlaybackRingBufferSecs;
|
double mPlaybackRingBufferSecs;
|
||||||
double mCaptureRingBufferSecs;
|
double mCaptureRingBufferSecs;
|
||||||
double mMaxPlaybackSecsToCopy;
|
long mPlaybackSamplesToCopy;
|
||||||
double mMinCaptureSecsToCopy;
|
double mMinCaptureSecsToCopy;
|
||||||
bool mPaused;
|
bool mPaused;
|
||||||
PaStream *mPortStreamV19;
|
PaStream *mPortStreamV19;
|
||||||
@ -572,7 +627,13 @@ private:
|
|||||||
bool mInputMixerWorks;
|
bool mInputMixerWorks;
|
||||||
float mMixerOutputVol;
|
float mMixerOutputVol;
|
||||||
|
|
||||||
bool mPlayLooped;
|
enum {
|
||||||
|
PLAY_STRAIGHT,
|
||||||
|
PLAY_LOOPED,
|
||||||
|
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
|
||||||
|
PLAY_SCRUB,
|
||||||
|
#endif
|
||||||
|
} mPlayMode;
|
||||||
double mCutPreviewGapStart;
|
double mCutPreviewGapStart;
|
||||||
double mCutPreviewGapLen;
|
double mCutPreviewGapLen;
|
||||||
|
|
||||||
@ -631,6 +692,14 @@ private:
|
|||||||
// Serialize main thread and PortAudio thread's attempts to pause and change
|
// Serialize main thread and PortAudio thread's attempts to pause and change
|
||||||
// the state used by the third, Audio thread.
|
// the state used by the third, Audio thread.
|
||||||
wxMutex mSuspendAudioThread;
|
wxMutex mSuspendAudioThread;
|
||||||
|
|
||||||
|
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
|
||||||
|
struct ScrubQueue;
|
||||||
|
ScrubQueue *mScrubQueue;
|
||||||
|
|
||||||
|
bool mSilentScrub;
|
||||||
|
long mScrubDuration;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -166,4 +166,8 @@
|
|||||||
// Define to enable Nyquist audio clip boundary control (Steve Daulton Dec 2014)
|
// Define to enable Nyquist audio clip boundary control (Steve Daulton Dec 2014)
|
||||||
// #define EXPERIMENTAL_NYQUIST_SPLIT_CONTROL
|
// #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
|
#endif
|
||||||
|
25
src/Mix.cpp
25
src/Mix.cpp
@ -275,6 +275,7 @@ Mixer::Mixer(int numInputTracks, WaveTrack **inputTracks,
|
|||||||
mBufferSize = outBufferSize;
|
mBufferSize = outBufferSize;
|
||||||
mInterleaved = outInterleaved;
|
mInterleaved = outInterleaved;
|
||||||
mRate = outRate;
|
mRate = outRate;
|
||||||
|
mSpeed = 1.0;
|
||||||
mFormat = outFormat;
|
mFormat = outFormat;
|
||||||
mApplyTrackGains = true;
|
mApplyTrackGains = true;
|
||||||
mGains = new float[mNumChannels];
|
mGains = new float[mNumChannels];
|
||||||
@ -424,7 +425,7 @@ sampleCount Mixer::MixVariableRates(int *channelFlags, WaveTrack *track,
|
|||||||
Resample * pResample)
|
Resample * pResample)
|
||||||
{
|
{
|
||||||
const double trackRate = track->GetRate();
|
const double trackRate = track->GetRate();
|
||||||
const double initialWarp = mRate / trackRate;
|
const double initialWarp = mRate / mSpeed / trackRate;
|
||||||
const double tstep = 1.0 / trackRate;
|
const double tstep = 1.0 / trackRate;
|
||||||
int sampleSize = SAMPLE_SIZE(floatSample);
|
int sampleSize = SAMPLE_SIZE(floatSample);
|
||||||
|
|
||||||
@ -634,7 +635,6 @@ sampleCount Mixer::Process(sampleCount maxToProcess)
|
|||||||
// return 0;
|
// return 0;
|
||||||
|
|
||||||
int i, j;
|
int i, j;
|
||||||
sampleCount out;
|
|
||||||
sampleCount maxOut = 0;
|
sampleCount maxOut = 0;
|
||||||
int *channelFlags = new int[mNumChannels];
|
int *channelFlags = new int[mNumChannels];
|
||||||
|
|
||||||
@ -669,16 +669,14 @@ sampleCount Mixer::Process(sampleCount maxToProcess)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mbVariableRates || track->GetRate() != mRate)
|
if (mbVariableRates || track->GetRate() != mRate)
|
||||||
out = MixVariableRates(channelFlags, track,
|
maxOut = std::max(maxOut,
|
||||||
|
MixVariableRates(channelFlags, track,
|
||||||
&mSamplePos[i], mSampleQueue[i],
|
&mSamplePos[i], mSampleQueue[i],
|
||||||
&mQueueStart[i], &mQueueLen[i], mResample[i]);
|
&mQueueStart[i], &mQueueLen[i], mResample[i]));
|
||||||
else
|
else
|
||||||
out = MixSameRate(channelFlags, track, &mSamplePos[i]);
|
maxOut = std::max(maxOut,
|
||||||
|
MixSameRate(channelFlags, track, &mSamplePos[i]));
|
||||||
if (out > maxOut)
|
|
||||||
maxOut = out;
|
|
||||||
|
|
||||||
double t = (double)mSamplePos[i] / (double)track->GetRate();
|
double t = (double)mSamplePos[i] / (double)track->GetRate();
|
||||||
if (mT0 > mT1)
|
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 )
|
MixerSpec::MixerSpec( int numTracks, int maxNumChannels )
|
||||||
{
|
{
|
||||||
mNumTracks = mNumChannels = numTracks;
|
mNumTracks = mNumChannels = numTracks;
|
||||||
|
@ -121,6 +121,9 @@ class AUDACITY_DLL_API Mixer {
|
|||||||
/// Process() is called.
|
/// Process() is called.
|
||||||
void Reposition(double t);
|
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)
|
/// 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.
|
/// This value is not accurate, it's useful for progress bars and indicators, but nothing else.
|
||||||
double MixGetCurrentTime();
|
double MixGetCurrentTime();
|
||||||
@ -175,6 +178,7 @@ class AUDACITY_DLL_API Mixer {
|
|||||||
samplePtr *mTemp;
|
samplePtr *mTemp;
|
||||||
float *mFloatBuffer;
|
float *mFloatBuffer;
|
||||||
double mRate;
|
double mRate;
|
||||||
|
double mSpeed;
|
||||||
bool mHighQuality;
|
bool mHighQuality;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user