mirror of
https://github.com/cookiengineer/audacity
synced 2025-05-03 17:19:43 +02:00
Merge branch 'scrubbing' -- preliminaries only, not the complete feature
This commit is contained in:
commit
f49a94755d
698
src/AudioIO.cpp
698
src/AudioIO.cpp
@ -79,6 +79,7 @@
|
||||
the speed control. In a separate algorithm, the audio callback updates
|
||||
mTime by (frames / samplerate) * factor, where factor reflects the
|
||||
speed at mTime. This effectively integrates speed to get position.
|
||||
Negative speeds are allowed too, for instance in scrubbing.
|
||||
|
||||
\par Midi Time
|
||||
MIDI is not warped according to the speed control. This might be
|
||||
@ -340,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,
|
||||
@ -552,7 +843,7 @@ AudioIO::AudioIO()
|
||||
mLastRecordingOffset = 0.0;
|
||||
mNumCaptureChannels = 0;
|
||||
mPaused = false;
|
||||
mPlayLooped = false;
|
||||
mPlayMode = PLAY_STRAIGHT;
|
||||
|
||||
mListener = NULL;
|
||||
mUpdateMeters = false;
|
||||
@ -615,6 +906,12 @@ AudioIO::AudioIO()
|
||||
#endif
|
||||
|
||||
mLastPlaybackTimeMillis = 0;
|
||||
|
||||
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
|
||||
mScrubQueue = NULL;
|
||||
mScrubDuration = 0;
|
||||
mSilentScrub = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
AudioIO::~AudioIO()
|
||||
@ -647,6 +944,10 @@ AudioIO::~AudioIO()
|
||||
DeleteSamples(mSilentBuf);
|
||||
|
||||
delete mThread;
|
||||
|
||||
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
|
||||
delete mScrubQueue;
|
||||
#endif
|
||||
}
|
||||
|
||||
void AudioIO::SetMixer(int inputSource)
|
||||
@ -1148,13 +1449,8 @@ int AudioIO::StartStream(WaveTrackArray playbackTracks,
|
||||
#ifdef EXPERIMENTAL_MIDI_OUT
|
||||
NoteTrackArray midiPlaybackTracks,
|
||||
#endif
|
||||
TimeTrack *timeTrack, double sampleRate,
|
||||
double t0, double t1,
|
||||
AudioIOListener* listener,
|
||||
bool playLooped /* = false */,
|
||||
double cutPreviewGapStart /* = 0.0 */,
|
||||
double cutPreviewGapLen, /* = 0.0 */
|
||||
const double *pStartTime /* = 0 */)
|
||||
double sampleRate, double t0, double t1,
|
||||
const AudioIOStartStreamOptions &options)
|
||||
{
|
||||
if( IsBusy() )
|
||||
return 0;
|
||||
@ -1194,8 +1490,8 @@ int AudioIO::StartStream(WaveTrackArray playbackTracks,
|
||||
}
|
||||
mSilenceLevel = (silenceLevelDB + dBRange)/(double)dBRange; // meter goes -dBRange dB -> 0dB
|
||||
|
||||
mTimeTrack = timeTrack;
|
||||
mListener = listener;
|
||||
mTimeTrack = options.timeTrack;
|
||||
mListener = options.listener;
|
||||
mRate = sampleRate;
|
||||
mT0 = t0;
|
||||
mT1 = t1;
|
||||
@ -1207,21 +1503,59 @@ int AudioIO::StartStream(WaveTrackArray playbackTracks,
|
||||
#ifdef EXPERIMENTAL_MIDI_OUT
|
||||
mMidiPlaybackTracks = midiPlaybackTracks;
|
||||
#endif
|
||||
mPlayLooped = playLooped;
|
||||
mCutPreviewGapStart = cutPreviewGapStart;
|
||||
mCutPreviewGapLen = cutPreviewGapLen;
|
||||
mPlayMode = options.playLooped ? PLAY_LOOPED : PLAY_STRAIGHT;
|
||||
mCutPreviewGapStart = options.cutPreviewGapStart;
|
||||
mCutPreviewGapLen = options.cutPreviewGapLen;
|
||||
mPlaybackBuffers = NULL;
|
||||
mPlaybackMixers = NULL;
|
||||
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)
|
||||
mWarpedLength = mTimeTrack->ComputeWarpedLength(mT0, mT1);
|
||||
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
|
||||
if (scrubbing)
|
||||
mWarpedLength = 0.0;
|
||||
else
|
||||
mWarpedLength = mT1 - mT0;
|
||||
#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
|
||||
@ -1230,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());
|
||||
@ -1307,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,13 +1674,20 @@ int AudioIO::StartStream(WaveTrackArray playbackTracks,
|
||||
memset(mPlaybackBuffers, 0, sizeof(RingBuffer*)*mPlaybackTracks.GetCount());
|
||||
memset(mPlaybackMixers, 0, sizeof(Mixer*)*mPlaybackTracks.GetCount());
|
||||
|
||||
for( unsigned int i = 0; i < mPlaybackTracks.GetCount(); i++ )
|
||||
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++)
|
||||
{
|
||||
mPlaybackBuffers[i] = new RingBuffer(floatSample, playbackBufferSize);
|
||||
|
||||
// MB: use normal time for the end time, not warped time!
|
||||
mPlaybackMixers[i] = new Mixer(1, &mPlaybackTracks[i],
|
||||
mTimeTrack, mT0, mT1, 1,
|
||||
warpOptions,
|
||||
mT0, mT1, 1,
|
||||
playbackMixBufferSize, false,
|
||||
mRate, floatSample, false);
|
||||
mPlaybackMixers[i]->ApplyTrackGains(false);
|
||||
@ -1376,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;
|
||||
@ -1411,19 +1766,19 @@ int AudioIO::StartStream(WaveTrackArray playbackTracks,
|
||||
AILASetStartTime();
|
||||
#endif
|
||||
|
||||
if (pStartTime)
|
||||
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
|
||||
delete mScrubQueue;
|
||||
if (scrubbing)
|
||||
{
|
||||
// Calculate the new time position
|
||||
mTime = std::max(mT0, std::min(mT1, *pStartTime));
|
||||
// Reset mixer positions for all playback tracks
|
||||
unsigned numMixers = mPlaybackTracks.GetCount();
|
||||
for (unsigned ii = 0; ii < numMixers; ++ii)
|
||||
mPlaybackMixers[ii]->Reposition(mTime);
|
||||
if(mTimeTrack)
|
||||
mWarpedTime = mTimeTrack->ComputeWarpedLength(mT0, mTime);
|
||||
else
|
||||
mWarpedTime = mTime - mT0;
|
||||
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
|
||||
@ -1542,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
|
||||
@ -1954,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)
|
||||
@ -1978,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)
|
||||
@ -2014,6 +2403,15 @@ bool AudioIO::IsMonitoring()
|
||||
return ( mPortStreamV19 && mStreamToken==0 );
|
||||
}
|
||||
|
||||
double AudioIO::LimitStreamTime(double absoluteTime) const
|
||||
{
|
||||
// Allows for forward or backward play
|
||||
if (ReversedTime())
|
||||
return std::max(mT1, std::min(mT0, absoluteTime));
|
||||
else
|
||||
return std::max(mT0, std::min(mT1, absoluteTime));
|
||||
}
|
||||
|
||||
double AudioIO::NormalizeStreamTime(double absoluteTime) const
|
||||
{
|
||||
// dmazzoni: This function is needed for two reasons:
|
||||
@ -2028,13 +2426,12 @@ double AudioIO::NormalizeStreamTime(double absoluteTime) const
|
||||
// mode. In this case, we should jump over a defined "gap" in the
|
||||
// audio.
|
||||
|
||||
// msmeyer: Just to be sure, the returned stream time should
|
||||
// never be smaller than the actual start time.
|
||||
if (absoluteTime < mT0)
|
||||
absoluteTime = mT0;
|
||||
|
||||
if (absoluteTime > mT1)
|
||||
absoluteTime = mT1;
|
||||
#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)
|
||||
{
|
||||
@ -2818,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
|
||||
@ -2834,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
|
||||
@ -2872,41 +3273,100 @@ 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);
|
||||
}
|
||||
|
||||
//if looping and processed is less than the full chunk/block/buffer that gets pulled from
|
||||
//other longer tracks, then we still need to advance the ring buffers or
|
||||
//we'll trip up on ourselves when we start them back up again.
|
||||
//if not looping we never start them up again, so its okay to not do anything
|
||||
if(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
|
||||
|
||||
@ -3559,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;
|
||||
@ -3576,17 +4042,20 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer,
|
||||
|
||||
// Calculate the new time position
|
||||
gAudioIO->mTime += gAudioIO->mSeek;
|
||||
if (gAudioIO->mTime < gAudioIO->mT0)
|
||||
gAudioIO->mTime = gAudioIO->mT0;
|
||||
else if (gAudioIO->mTime > gAudioIO->mT1)
|
||||
gAudioIO->mTime = gAudioIO->mT1;
|
||||
gAudioIO->mTime = gAudioIO->LimitStreamTime(gAudioIO->mTime);
|
||||
gAudioIO->mSeek = 0.0;
|
||||
|
||||
// Reset mixer positions and flush buffers for all tracks
|
||||
if(gAudioIO->mTimeTrack)
|
||||
gAudioIO->mWarpedTime = gAudioIO->mTimeTrack->ComputeWarpedLength(gAudioIO->mT0, gAudioIO->mTime);
|
||||
// Following gives negative when mT0 > mTime
|
||||
gAudioIO->mWarpedTime =
|
||||
gAudioIO->mTimeTrack->ComputeWarpedLength
|
||||
(gAudioIO->mT0, gAudioIO->mTime);
|
||||
else
|
||||
gAudioIO->mWarpedTime = gAudioIO->mTime - gAudioIO->mT0;
|
||||
gAudioIO->mWarpedTime = abs(gAudioIO->mWarpedTime);
|
||||
|
||||
// Reset mixer positions and flush buffers for all tracks
|
||||
for (i = 0; i < (unsigned int)numPlaybackTracks; i++)
|
||||
{
|
||||
gAudioIO->mPlaybackMixers[i]->Reposition(gAudioIO->mTime);
|
||||
@ -3631,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];
|
||||
@ -3667,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.
|
||||
}
|
||||
@ -3678,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)
|
||||
@ -3718,12 +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
|
||||
if (len == 0 && gAudioIO->mTime >= gAudioIO->mT1 &&
|
||||
!gAudioIO->mPlayLooped)
|
||||
{
|
||||
callbackReturn = paComplete;
|
||||
// PRL: or scrubbing.
|
||||
if (len == 0 &&
|
||||
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;
|
||||
|
||||
@ -3772,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();
|
||||
@ -3866,21 +4350,35 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer,
|
||||
}
|
||||
}
|
||||
|
||||
// Update the current time position
|
||||
if (gAudioIO->mTimeTrack) {
|
||||
// MB: this is why SolveWarpedLength is needed :)
|
||||
gAudioIO->mTime = gAudioIO->mTimeTrack->SolveWarpedLength(gAudioIO->mTime, framesPerBuffer / gAudioIO->mRate);
|
||||
} else {
|
||||
gAudioIO->mTime += framesPerBuffer / gAudioIO->mRate;
|
||||
// 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())
|
||||
delta *= -1.0;
|
||||
if (gAudioIO->mTimeTrack)
|
||||
// MB: this is why SolveWarpedLength is needed :)
|
||||
gAudioIO->mTime =
|
||||
gAudioIO->mTimeTrack->SolveWarpedLength(gAudioIO->mTime, delta);
|
||||
else
|
||||
gAudioIO->mTime += delta;
|
||||
}
|
||||
|
||||
// Wrap to start if looping
|
||||
while (gAudioIO->mPlayLooped && gAudioIO->mTime >= gAudioIO->mT1)
|
||||
if (gAudioIO->mPlayMode == AudioIO::PLAY_LOOPED)
|
||||
{
|
||||
// LL: This is not exactly right, but I'm at my wits end trying to
|
||||
// figure it out. Feel free to fix it. :-)
|
||||
// MB: it's much easier than you think, mTime isn't warped at all!
|
||||
gAudioIO->mTime -= gAudioIO->mT1 - gAudioIO->mT0;
|
||||
while (gAudioIO->ReversedTime()
|
||||
? gAudioIO->mTime <= gAudioIO->mT1
|
||||
: gAudioIO->mTime >= gAudioIO->mT1)
|
||||
{
|
||||
// LL: This is not exactly right, but I'm at my wits end trying to
|
||||
// figure it out. Feel free to fix it. :-)
|
||||
// MB: it's much easier than you think, mTime isn't warped at all!
|
||||
gAudioIO->mTime -= gAudioIO->mT1 - gAudioIO->mT0;
|
||||
}
|
||||
}
|
||||
|
||||
// Record the reported latency from PortAudio.
|
||||
|
124
src/AudioIO.h
124
src/AudioIO.h
@ -42,6 +42,7 @@ class Resample;
|
||||
class TimeTrack;
|
||||
class AudioThread;
|
||||
class Meter;
|
||||
class SelectedRegion;
|
||||
class TimeTrack;
|
||||
class wxDialog;
|
||||
|
||||
@ -74,6 +75,49 @@ DECLARE_EXPORTED_EVENT_TYPE(AUDACITY_DLL_API, EVT_AUDIOIO_PLAYBACK, -1);
|
||||
DECLARE_EXPORTED_EVENT_TYPE(AUDACITY_DLL_API, EVT_AUDIOIO_CAPTURE, -1);
|
||||
DECLARE_EXPORTED_EVENT_TYPE(AUDACITY_DLL_API, EVT_AUDIOIO_MONITOR, -1);
|
||||
|
||||
// To avoid growing the argument list of StartStream, add fields here
|
||||
struct AudioIOStartStreamOptions
|
||||
{
|
||||
AudioIOStartStreamOptions()
|
||||
: timeTrack(NULL)
|
||||
, listener(NULL)
|
||||
, 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;
|
||||
AudioIOListener* listener;
|
||||
bool playLooped;
|
||||
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 {
|
||||
|
||||
public:
|
||||
@ -103,15 +147,9 @@ class AUDACITY_DLL_API AudioIO {
|
||||
#ifdef EXPERIMENTAL_MIDI_OUT
|
||||
NoteTrackArray midiTracks,
|
||||
#endif
|
||||
TimeTrack *timeTrack, double sampleRate,
|
||||
double t0, double t1,
|
||||
AudioIOListener* listener,
|
||||
bool playLooped = false,
|
||||
double cutPreviewGapStart = 0.0,
|
||||
double cutPreviewGapLen = 0.0,
|
||||
// May be other than t0,
|
||||
// but will be constrained between t0 and t1
|
||||
const double *pStartTime = 0);
|
||||
double sampleRate, double t0, double t1,
|
||||
const AudioIOStartStreamOptions &options =
|
||||
AudioIOStartStreamOptions());
|
||||
|
||||
/** \brief Stop recording, playback or input monitoring.
|
||||
*
|
||||
@ -123,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.
|
||||
*
|
||||
@ -281,9 +350,8 @@ class AUDACITY_DLL_API AudioIO {
|
||||
*/
|
||||
static int GetOptimalSupportedSampleRate();
|
||||
|
||||
/** \brief The time the stream has been playing for
|
||||
/** \brief During playback, the (unwarped) track time most recently played
|
||||
*
|
||||
* This is given in seconds based on starting at t0
|
||||
* When playing looped, this will start from t0 again,
|
||||
* too. So the returned time should be always between
|
||||
* t0 and t1
|
||||
@ -375,10 +443,10 @@ private:
|
||||
#endif
|
||||
|
||||
/** \brief Get the number of audio samples free in all of the playback
|
||||
* buffers.
|
||||
*
|
||||
* Returns the smallest of the buffer free space values in the event that
|
||||
* they are different. */
|
||||
* buffers.
|
||||
*
|
||||
* Returns the smallest of the buffer free space values in the event that
|
||||
* they are different. */
|
||||
int GetCommonlyAvailPlayback();
|
||||
|
||||
/** \brief Get the number of audio samples ready in all of the recording
|
||||
@ -422,6 +490,12 @@ private:
|
||||
/** \brief How many sample rates to try */
|
||||
static const int NumRatesToTry;
|
||||
|
||||
bool ReversedTime() const
|
||||
{
|
||||
return mT1 < mT0;
|
||||
}
|
||||
double LimitStreamTime(double absoluteTime) const;
|
||||
|
||||
double NormalizeStreamTime(double absoluteTime) const;
|
||||
|
||||
/** \brief Clean up after StartStream if it fails.
|
||||
@ -506,7 +580,7 @@ private:
|
||||
double mSeek;
|
||||
double mPlaybackRingBufferSecs;
|
||||
double mCaptureRingBufferSecs;
|
||||
double mMaxPlaybackSecsToCopy;
|
||||
long mPlaybackSamplesToCopy;
|
||||
double mMinCaptureSecsToCopy;
|
||||
bool mPaused;
|
||||
PaStream *mPortStreamV19;
|
||||
@ -535,7 +609,7 @@ private:
|
||||
Meter *mInputMeter;
|
||||
Meter *mOutputMeter;
|
||||
bool mUpdateMeters;
|
||||
bool mUpdatingMeters;
|
||||
volatile bool mUpdatingMeters;
|
||||
|
||||
#if USE_PORTMIXER
|
||||
PxMixer *mPortMixer;
|
||||
@ -553,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;
|
||||
|
||||
@ -612,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
|
||||
|
@ -1430,31 +1430,42 @@ double Envelope::SolveIntegralOfInverse( double t0, double area )
|
||||
{
|
||||
if(area == 0.0)
|
||||
return t0;
|
||||
if(area < 0.0)
|
||||
{
|
||||
fprintf( stderr, "SolveIntegralOfInverse called with negative area, this is not supported!\n" );
|
||||
return t0;
|
||||
}
|
||||
|
||||
unsigned int count = mEnv.Count();
|
||||
if(count == 0) // 'empty' envelope
|
||||
return t0 + area * mDefaultValue;
|
||||
|
||||
double lastT, lastVal;
|
||||
unsigned int i; // this is the next point to check
|
||||
int i; // this is the next point to check
|
||||
if(t0 < mEnv[0]->GetT()) // t0 preceding the first point
|
||||
{
|
||||
i = 1;
|
||||
lastT = mEnv[0]->GetT();
|
||||
lastVal = mEnv[0]->GetVal();
|
||||
double added = (lastT - t0) / lastVal;
|
||||
if(added >= area)
|
||||
if (area < 0) {
|
||||
return t0 + area * mEnv[0]->GetVal();
|
||||
area -= added;
|
||||
}
|
||||
else {
|
||||
i = 1;
|
||||
lastT = mEnv[0]->GetT();
|
||||
lastVal = mEnv[0]->GetVal();
|
||||
double added = (lastT - t0) / lastVal;
|
||||
if(added >= area)
|
||||
return t0 + area * mEnv[0]->GetVal();
|
||||
area -= added;
|
||||
}
|
||||
}
|
||||
else if(t0 >= mEnv[count - 1]->GetT()) // t0 following the last point
|
||||
{
|
||||
return t0 + area * mEnv[count - 1]->GetVal();
|
||||
if (area < 0) {
|
||||
i = count - 2;
|
||||
lastT = mEnv[count - 1]->GetT();
|
||||
lastVal = mEnv[count - 1]->GetVal();
|
||||
double added = (lastT - t0) / lastVal; // negative
|
||||
if(added <= area)
|
||||
return t0 + area * mEnv[count - 1]->GetVal();
|
||||
area -= added;
|
||||
}
|
||||
else {
|
||||
return t0 + area * mEnv[count - 1]->GetVal();
|
||||
}
|
||||
}
|
||||
else // t0 enclosed by points
|
||||
{
|
||||
@ -1463,25 +1474,52 @@ double Envelope::SolveIntegralOfInverse( double t0, double area )
|
||||
BinarySearchForTime(lo, hi, t0);
|
||||
lastVal = InterpolatePoints(mEnv[lo]->GetVal(), mEnv[hi]->GetVal(), (t0 - mEnv[lo]->GetT()) / (mEnv[hi]->GetT() - mEnv[lo]->GetT()), mDB);
|
||||
lastT = t0;
|
||||
i = hi; // the point immediately after t0.
|
||||
if (area < 0)
|
||||
i = lo;
|
||||
else
|
||||
i = hi; // the point immediately after t0.
|
||||
}
|
||||
|
||||
// loop through the rest of the envelope points until we get to t1
|
||||
while (1)
|
||||
{
|
||||
if(i >= count) // the requested range extends beyond the last point
|
||||
if (area < 0) {
|
||||
// loop BACKWARDS through the rest of the envelope points until we get to t1
|
||||
// (which is less than t0)
|
||||
while (1)
|
||||
{
|
||||
return lastT + area * lastVal;
|
||||
if(i < 0) // the requested range extends beyond the leftmost point
|
||||
{
|
||||
return lastT + area * lastVal;
|
||||
}
|
||||
else
|
||||
{
|
||||
double added =
|
||||
-IntegrateInverseInterpolated(mEnv[i]->GetVal(), lastVal, lastT - mEnv[i]->GetT(), mDB);
|
||||
if(added <= area)
|
||||
return lastT - SolveIntegrateInverseInterpolated(lastVal, mEnv[i]->GetVal(), lastT - mEnv[i]->GetT(), -area, mDB);
|
||||
area -= added;
|
||||
lastT = mEnv[i]->GetT();
|
||||
lastVal = mEnv[i]->GetVal();
|
||||
--i;
|
||||
}
|
||||
}
|
||||
else
|
||||
}
|
||||
else {
|
||||
// loop through the rest of the envelope points until we get to t1
|
||||
while (1)
|
||||
{
|
||||
double added = IntegrateInverseInterpolated(lastVal, mEnv[i]->GetVal(), mEnv[i]->GetT() - lastT, mDB);
|
||||
if(added >= area)
|
||||
return lastT + SolveIntegrateInverseInterpolated(lastVal, mEnv[i]->GetVal(), mEnv[i]->GetT() - lastT, area, mDB);
|
||||
area -= added;
|
||||
lastT = mEnv[i]->GetT();
|
||||
lastVal = mEnv[i]->GetVal();
|
||||
i++;
|
||||
if(i >= count) // the requested range extends beyond the last point
|
||||
{
|
||||
return lastT + area * lastVal;
|
||||
}
|
||||
else
|
||||
{
|
||||
double added = IntegrateInverseInterpolated(lastVal, mEnv[i]->GetVal(), mEnv[i]->GetT() - lastT, mDB);
|
||||
if(added >= area)
|
||||
return lastT + SolveIntegrateInverseInterpolated(lastVal, mEnv[i]->GetVal(), mEnv[i]->GetT() - lastT, area, mDB);
|
||||
area -= added;
|
||||
lastT = mEnv[i]->GetT();
|
||||
lastVal = mEnv[i]->GetVal();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -99,8 +99,8 @@ class Envelope : public XMLTagHandler {
|
||||
void Flatten(double value);
|
||||
int GetDragPoint(void) {return mDragPoint;}
|
||||
|
||||
double GetMinValue() { return mMinValue; }
|
||||
double GetMaxValue() { return mMaxValue; }
|
||||
double GetMinValue() const { return mMinValue; }
|
||||
double GetMaxValue() const { return mMaxValue; }
|
||||
void SetRange(double minValue, double maxValue);
|
||||
|
||||
double ClampValue(double value) { return std::max(mMinValue, std::min(mMaxValue, value)); }
|
||||
|
@ -107,9 +107,6 @@
|
||||
// Paul Licameli (PRL) 5 Oct 2014
|
||||
#define EXPERIMENTAL_SPECTRAL_EDITING
|
||||
|
||||
// Paul Licameli (PRL) 29 Nov 2014
|
||||
// #define EXPERIMENTAL_SCRUBBING
|
||||
|
||||
// Paul Licameli (PRL) 29 Nov 2014
|
||||
// #define EXPERIMENTAL_IMPROVED_SEEKING
|
||||
|
||||
@ -169,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
|
||||
|
@ -2013,7 +2013,8 @@ void AudacityProject::OnPlayOneSecond()
|
||||
|
||||
double pos = mTrackPanel->GetMostRecentXPos();
|
||||
mLastPlayMode = oneSecondPlay;
|
||||
GetControlToolBar()->PlayPlayRegion(pos - 0.5, pos + 0.5);
|
||||
GetControlToolBar()->PlayPlayRegion
|
||||
(SelectedRegion(pos - 0.5, pos + 0.5), GetDefaultPlayOptions());
|
||||
}
|
||||
|
||||
|
||||
@ -2056,7 +2057,8 @@ void AudacityProject::OnPlayToSelection()
|
||||
// only when playing a short region, less than or equal to a second.
|
||||
// mLastPlayMode = ((t1-t0) > 1.0) ? normalPlay : oneSecondPlay;
|
||||
|
||||
GetControlToolBar()->PlayPlayRegion(t0, t1);
|
||||
GetControlToolBar()->PlayPlayRegion
|
||||
(SelectedRegion(t0, t1), GetDefaultPlayOptions());
|
||||
}
|
||||
|
||||
// The next 4 functions provide a limited version of the
|
||||
@ -2073,7 +2075,7 @@ void AudacityProject::OnPlayBeforeSelectionStart()
|
||||
|
||||
mLastPlayMode = oneSecondPlay; // this disables auto scrolling, as in OnPlayToSelection()
|
||||
|
||||
GetControlToolBar()->PlayPlayRegion(t0 - beforeLen, t0);
|
||||
GetControlToolBar()->PlayPlayRegion(SelectedRegion(t0 - beforeLen, t0), GetDefaultPlayOptions());
|
||||
}
|
||||
|
||||
void AudacityProject::OnPlayAfterSelectionStart()
|
||||
@ -2089,9 +2091,9 @@ void AudacityProject::OnPlayAfterSelectionStart()
|
||||
mLastPlayMode = oneSecondPlay; // this disables auto scrolling, as in OnPlayToSelection()
|
||||
|
||||
if ( t1 - t0 > 0.0 && t1 - t0 < afterLen )
|
||||
GetControlToolBar()->PlayPlayRegion(t0, t1);
|
||||
GetControlToolBar()->PlayPlayRegion(SelectedRegion(t0, t1), GetDefaultPlayOptions());
|
||||
else
|
||||
GetControlToolBar()->PlayPlayRegion(t0, t0 + afterLen);
|
||||
GetControlToolBar()->PlayPlayRegion(SelectedRegion(t0, t0 + afterLen), GetDefaultPlayOptions());
|
||||
}
|
||||
|
||||
void AudacityProject::OnPlayBeforeSelectionEnd()
|
||||
@ -2107,9 +2109,9 @@ void AudacityProject::OnPlayBeforeSelectionEnd()
|
||||
mLastPlayMode = oneSecondPlay; // this disables auto scrolling, as in OnPlayToSelection()
|
||||
|
||||
if ( t1 - t0 > 0.0 && t1 - t0 < beforeLen )
|
||||
GetControlToolBar()->PlayPlayRegion(t0, t1);
|
||||
GetControlToolBar()->PlayPlayRegion(SelectedRegion(t0, t1), GetDefaultPlayOptions());
|
||||
else
|
||||
GetControlToolBar()->PlayPlayRegion(t1 - beforeLen, t1);
|
||||
GetControlToolBar()->PlayPlayRegion(SelectedRegion(t1 - beforeLen, t1), GetDefaultPlayOptions());
|
||||
}
|
||||
|
||||
|
||||
@ -2124,7 +2126,7 @@ void AudacityProject::OnPlayAfterSelectionEnd()
|
||||
|
||||
mLastPlayMode = oneSecondPlay; // this disables auto scrolling, as in OnPlayToSelection()
|
||||
|
||||
GetControlToolBar()->PlayPlayRegion(t1, t1 + afterLen);
|
||||
GetControlToolBar()->PlayPlayRegion(SelectedRegion(t1, t1 + afterLen), GetDefaultPlayOptions());
|
||||
}
|
||||
|
||||
void AudacityProject::OnPlayLooped()
|
||||
|
258
src/Mix.cpp
258
src/Mix.cpp
@ -161,7 +161,8 @@ bool MixAndRender(TrackList *tracks, TrackFactory *trackFactory,
|
||||
endTime = mixEndTime;
|
||||
}
|
||||
|
||||
Mixer *mixer = new Mixer(numWaves, waveArray, tracks->GetTimeTrack(),
|
||||
Mixer *mixer = new Mixer(numWaves, waveArray,
|
||||
Mixer::WarpOptions(tracks->GetTimeTrack()),
|
||||
startTime, endTime, mono ? 1 : 2, maxBlockLen, false,
|
||||
rate, format);
|
||||
|
||||
@ -226,8 +227,28 @@ bool MixAndRender(TrackList *tracks, TrackFactory *trackFactory,
|
||||
return (updateResult == eProgressSuccess || updateResult == eProgressStopped);
|
||||
}
|
||||
|
||||
Mixer::WarpOptions::WarpOptions(double min, double max)
|
||||
: timeTrack(0), minSpeed(min), maxSpeed(max)
|
||||
{
|
||||
if (minSpeed < 0)
|
||||
{
|
||||
wxASSERT(false);
|
||||
minSpeed = 0;
|
||||
}
|
||||
if (maxSpeed < 0)
|
||||
{
|
||||
wxASSERT(false);
|
||||
maxSpeed = 0;
|
||||
}
|
||||
if (minSpeed > maxSpeed)
|
||||
{
|
||||
wxASSERT(false);
|
||||
std::swap(minSpeed, maxSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
Mixer::Mixer(int numInputTracks, WaveTrack **inputTracks,
|
||||
TimeTrack *timeTrack,
|
||||
const WarpOptions &warpOptions,
|
||||
double startTime, double stopTime,
|
||||
int numOutChannels, int outBufferSize, bool outInterleaved,
|
||||
double outRate, sampleFormat outFormat,
|
||||
@ -238,12 +259,15 @@ Mixer::Mixer(int numInputTracks, WaveTrack **inputTracks,
|
||||
mHighQuality = highQuality;
|
||||
mNumInputTracks = numInputTracks;
|
||||
mInputTrack = new WaveTrack*[mNumInputTracks];
|
||||
|
||||
// mSamplePos holds for each track the next sample position not
|
||||
// yet processed.
|
||||
mSamplePos = new sampleCount[mNumInputTracks];
|
||||
for(i=0; i<mNumInputTracks; i++) {
|
||||
mInputTrack[i] = inputTracks[i];
|
||||
mSamplePos[i] = inputTracks[i]->TimeToLongSamples(startTime);
|
||||
}
|
||||
mTimeTrack = timeTrack;
|
||||
mTimeTrack = warpOptions.timeTrack;
|
||||
mT0 = startTime;
|
||||
mT1 = stopTime;
|
||||
mTime = startTime;
|
||||
@ -251,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];
|
||||
@ -277,23 +302,45 @@ Mixer::Mixer(int numInputTracks, WaveTrack **inputTracks,
|
||||
}
|
||||
mFloatBuffer = new float[mInterleavedBufferSize];
|
||||
|
||||
// This is the number of samples grabbed in one go from a track
|
||||
// and placed in a queue, when mixing with resampling.
|
||||
// (Should we use WaveTrack::GetBestBlockSize instead?)
|
||||
mQueueMaxLen = 65536;
|
||||
|
||||
// But cut the queue into blocks of this finer size
|
||||
// for variable rate resampling. Each block is resampled at some
|
||||
// constant rate.
|
||||
mProcessLen = 1024;
|
||||
|
||||
// Position in each queue of the start of the next block to resample.
|
||||
mQueueStart = new int[mNumInputTracks];
|
||||
|
||||
// For each queue, the number of available samples after the queue start.
|
||||
mQueueLen = new int[mNumInputTracks];
|
||||
mSampleQueue = new float *[mNumInputTracks];
|
||||
mResample = new Resample*[mNumInputTracks];
|
||||
for(i=0; i<mNumInputTracks; i++) {
|
||||
double factor = (mRate / mInputTrack[i]->GetRate());
|
||||
if (timeTrack) {
|
||||
double minFactor, maxFactor;
|
||||
if (mTimeTrack) {
|
||||
// variable rate resampling
|
||||
mResample[i] = new Resample(mHighQuality,
|
||||
factor / timeTrack->GetRangeUpper(),
|
||||
factor / timeTrack->GetRangeLower());
|
||||
} else {
|
||||
mResample[i] = new Resample(mHighQuality, factor, factor); // constant rate resampling
|
||||
mbVariableRates = true;
|
||||
minFactor = factor / mTimeTrack->GetRangeUpper();
|
||||
maxFactor = factor / mTimeTrack->GetRangeLower();
|
||||
}
|
||||
else if (warpOptions.minSpeed > 0.0 && warpOptions.maxSpeed > 0.0) {
|
||||
// variable rate resampling
|
||||
mbVariableRates = true;
|
||||
minFactor = factor / warpOptions.maxSpeed;
|
||||
maxFactor = factor / warpOptions.minSpeed;
|
||||
}
|
||||
else {
|
||||
// constant rate resampling
|
||||
mbVariableRates = false;
|
||||
minFactor = maxFactor = factor;
|
||||
}
|
||||
|
||||
mResample[i] = new Resample(mHighQuality, minFactor, maxFactor);
|
||||
mSampleQueue[i] = new float[mQueueMaxLen];
|
||||
mQueueStart[i] = 0;
|
||||
mQueueLen[i] = 0;
|
||||
@ -377,10 +424,9 @@ sampleCount Mixer::MixVariableRates(int *channelFlags, WaveTrack *track,
|
||||
int *queueStart, int *queueLen,
|
||||
Resample * pResample)
|
||||
{
|
||||
double trackRate = track->GetRate();
|
||||
double initialWarp = mRate / trackRate;
|
||||
double tstep = 1.0 / trackRate;
|
||||
double t = (*pos - *queueLen) / trackRate;
|
||||
const double trackRate = track->GetRate();
|
||||
const double initialWarp = mRate / mSpeed / trackRate;
|
||||
const double tstep = 1.0 / trackRate;
|
||||
int sampleSize = SAMPLE_SIZE(floatSample);
|
||||
|
||||
sampleCount out = 0;
|
||||
@ -397,45 +443,64 @@ sampleCount Mixer::MixVariableRates(int *channelFlags, WaveTrack *track,
|
||||
*/
|
||||
|
||||
// Find the last sample
|
||||
sampleCount endPos;
|
||||
double endTime = track->GetEndTime();
|
||||
if (endTime > mT1) {
|
||||
endPos = track->TimeToLongSamples(mT1);
|
||||
}
|
||||
else {
|
||||
endPos = track->TimeToLongSamples(endTime);
|
||||
}
|
||||
double startTime = track->GetStartTime();
|
||||
const sampleCount endPos =
|
||||
track->TimeToLongSamples(std::max(startTime, std::min(endTime, mT1)));
|
||||
const sampleCount startPos =
|
||||
track->TimeToLongSamples(std::max(startTime, std::min(endTime, mT0)));
|
||||
const bool backwards = (endPos < startPos);
|
||||
// Find the time corresponding to the start of the queue, for use with time track
|
||||
double t = (*pos + (backwards ? *queueLen : - *queueLen)) / trackRate;
|
||||
|
||||
while (out < mMaxOut) {
|
||||
if (*queueLen < mProcessLen) {
|
||||
// Shift pending portion to start of the buffer
|
||||
memmove(queue, &queue[*queueStart], (*queueLen) * sampleSize);
|
||||
*queueStart = 0;
|
||||
|
||||
int getLen = mQueueMaxLen - *queueLen;
|
||||
int getLen =
|
||||
std::min((backwards ? *pos - endPos : endPos - *pos),
|
||||
sampleCount(mQueueMaxLen - *queueLen));
|
||||
|
||||
// Constrain
|
||||
if (*pos + getLen > endPos) {
|
||||
getLen = endPos - *pos;
|
||||
}
|
||||
|
||||
// Nothing to do if past end of track
|
||||
// Nothing to do if past end of play interval
|
||||
if (getLen > 0) {
|
||||
track->Get((samplePtr)&queue[*queueLen],
|
||||
floatSample,
|
||||
*pos,
|
||||
getLen);
|
||||
if (backwards) {
|
||||
track->Get((samplePtr)&queue[*queueLen],
|
||||
floatSample,
|
||||
*pos - (getLen - 1),
|
||||
getLen);
|
||||
|
||||
track->GetEnvelopeValues(mEnvValues,
|
||||
getLen,
|
||||
(*pos) / trackRate,
|
||||
tstep);
|
||||
track->GetEnvelopeValues(mEnvValues,
|
||||
getLen,
|
||||
(*pos - (getLen- 1)) / trackRate,
|
||||
tstep);
|
||||
|
||||
*pos -= getLen;
|
||||
}
|
||||
else {
|
||||
track->Get((samplePtr)&queue[*queueLen],
|
||||
floatSample,
|
||||
*pos,
|
||||
getLen);
|
||||
|
||||
track->GetEnvelopeValues(mEnvValues,
|
||||
getLen,
|
||||
(*pos) / trackRate,
|
||||
tstep);
|
||||
|
||||
*pos += getLen;
|
||||
}
|
||||
|
||||
for (int i = 0; i < getLen; i++) {
|
||||
queue[(*queueLen) + i] *= mEnvValues[i];
|
||||
}
|
||||
|
||||
if (backwards)
|
||||
ReverseSamples((samplePtr)&queue[0], floatSample,
|
||||
*queueStart, getLen);
|
||||
|
||||
*queueLen += getLen;
|
||||
*pos += getLen;
|
||||
}
|
||||
}
|
||||
|
||||
@ -452,8 +517,13 @@ sampleCount Mixer::MixVariableRates(int *channelFlags, WaveTrack *track,
|
||||
// as a result of this the warp factor may be slightly wrong, so AudioIO will stop too soon
|
||||
// or too late (resulting in missing sound or inserted silence). This can't be fixed
|
||||
// without changing the way the resampler works, because the number of input samples that will be used
|
||||
// is unpredictable. Maybe it can be compensated lated though.
|
||||
factor *= mTimeTrack->ComputeWarpFactor(t, t + (double)thisProcessLen / trackRate);
|
||||
// is unpredictable. Maybe it can be compensated later though.
|
||||
if (backwards)
|
||||
factor *= mTimeTrack->ComputeWarpFactor
|
||||
(t - (double)thisProcessLen / trackRate + tstep, t + tstep);
|
||||
else
|
||||
factor *= mTimeTrack->ComputeWarpFactor
|
||||
(t, t + (double)thisProcessLen / trackRate);
|
||||
}
|
||||
|
||||
int input_used;
|
||||
@ -472,7 +542,7 @@ sampleCount Mixer::MixVariableRates(int *channelFlags, WaveTrack *track,
|
||||
*queueStart += input_used;
|
||||
*queueLen -= input_used;
|
||||
out += outgen;
|
||||
t += (input_used / trackRate);
|
||||
t += ((backwards ? -input_used : input_used) / trackRate);
|
||||
|
||||
if (last) {
|
||||
break;
|
||||
@ -504,24 +574,46 @@ sampleCount Mixer::MixSameRate(int *channelFlags, WaveTrack *track,
|
||||
{
|
||||
int slen = mMaxOut;
|
||||
int c;
|
||||
double t = *pos / track->GetRate();
|
||||
double trackEndTime = track->GetEndTime();
|
||||
double tEnd = trackEndTime > mT1 ? mT1 : trackEndTime;
|
||||
const double t = *pos / track->GetRate();
|
||||
const double trackEndTime = track->GetEndTime();
|
||||
const double trackStartTime = track->GetStartTime();
|
||||
const double tEnd = std::max(trackStartTime, std::min(trackEndTime, mT1));
|
||||
const double tStart = std::max(trackStartTime, std::min(trackEndTime, mT0));
|
||||
const bool backwards = (tEnd < tStart);
|
||||
|
||||
//don't process if we're at the end of the selection or track.
|
||||
if (t>=tEnd)
|
||||
if ((backwards ? t <= tEnd : t >= tEnd))
|
||||
return 0;
|
||||
//if we're about to approach the end of the track or selection, figure out how much we need to grab
|
||||
if (t + slen/track->GetRate() > tEnd)
|
||||
slen = (int)((tEnd - t) * track->GetRate() + 0.5);
|
||||
if (backwards) {
|
||||
if (t - slen/track->GetRate() < tEnd)
|
||||
slen = (int)((t - tEnd) * track->GetRate() + 0.5);
|
||||
}
|
||||
else {
|
||||
if (t + slen/track->GetRate() > tEnd)
|
||||
slen = (int)((tEnd - t) * track->GetRate() + 0.5);
|
||||
}
|
||||
|
||||
if (slen > mMaxOut)
|
||||
slen = mMaxOut;
|
||||
|
||||
track->Get((samplePtr)mFloatBuffer, floatSample, *pos, slen);
|
||||
track->GetEnvelopeValues(mEnvValues, slen, t, 1.0 / mRate);
|
||||
for(int i=0; i<slen; i++)
|
||||
mFloatBuffer[i] *= mEnvValues[i]; // Track gain control will go here?
|
||||
if (backwards) {
|
||||
track->Get((samplePtr)mFloatBuffer, floatSample, *pos - (slen - 1), slen);
|
||||
track->GetEnvelopeValues(mEnvValues, slen, t - (slen - 1) / mRate, 1.0 / mRate);
|
||||
for(int i=0; i<slen; i++)
|
||||
mFloatBuffer[i] *= mEnvValues[i]; // Track gain control will go here?
|
||||
ReverseSamples((samplePtr)mFloatBuffer, floatSample, 0, slen);
|
||||
|
||||
*pos -= slen;
|
||||
}
|
||||
else {
|
||||
track->Get((samplePtr)mFloatBuffer, floatSample, *pos, slen);
|
||||
track->GetEnvelopeValues(mEnvValues, slen, t, 1.0 / mRate);
|
||||
for(int i=0; i<slen; i++)
|
||||
mFloatBuffer[i] *= mEnvValues[i]; // Track gain control will go here?
|
||||
|
||||
*pos += slen;
|
||||
}
|
||||
|
||||
for(c=0; c<mNumChannels; c++)
|
||||
if (mApplyTrackGains)
|
||||
@ -532,8 +624,6 @@ sampleCount Mixer::MixSameRate(int *channelFlags, WaveTrack *track,
|
||||
MixBuffers(mNumChannels, channelFlags, mGains,
|
||||
(samplePtr)mFloatBuffer, mTemp, slen, mInterleaved);
|
||||
|
||||
*pos += slen;
|
||||
|
||||
return slen;
|
||||
}
|
||||
|
||||
@ -545,7 +635,6 @@ sampleCount Mixer::Process(sampleCount maxToProcess)
|
||||
// return 0;
|
||||
|
||||
int i, j;
|
||||
sampleCount out;
|
||||
sampleCount maxOut = 0;
|
||||
int *channelFlags = new int[mNumChannels];
|
||||
|
||||
@ -580,42 +669,41 @@ sampleCount Mixer::Process(sampleCount maxToProcess)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mTimeTrack || track->GetRate() != mRate)
|
||||
out = MixVariableRates(channelFlags, track,
|
||||
&mSamplePos[i], mSampleQueue[i],
|
||||
&mQueueStart[i], &mQueueLen[i], mResample[i]);
|
||||
if (mbVariableRates || track->GetRate() != mRate)
|
||||
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(t > mTime)
|
||||
if (mT0 > mT1)
|
||||
mTime = std::max(t, mT1);
|
||||
else
|
||||
mTime = std::min(t, mT1);
|
||||
|
||||
}
|
||||
if(mInterleaved) {
|
||||
for(int c=0; c<mNumChannels; c++) {
|
||||
CopySamples(mTemp[0] + (c * SAMPLE_SIZE(floatSample)),
|
||||
floatSample,
|
||||
mBuffer[0] + (c * SAMPLE_SIZE(mFormat)),
|
||||
mFormat,
|
||||
maxOut,
|
||||
mHighQuality,
|
||||
mNumChannels,
|
||||
mNumChannels);
|
||||
floatSample,
|
||||
mBuffer[0] + (c * SAMPLE_SIZE(mFormat)),
|
||||
mFormat,
|
||||
maxOut,
|
||||
mHighQuality,
|
||||
mNumChannels,
|
||||
mNumChannels);
|
||||
}
|
||||
}
|
||||
else {
|
||||
for(int c=0; c<mNumBuffers; c++) {
|
||||
CopySamples(mTemp[c],
|
||||
floatSample,
|
||||
mBuffer[c],
|
||||
mFormat,
|
||||
maxOut,
|
||||
mHighQuality);
|
||||
CopySamples(mTemp[c],
|
||||
floatSample,
|
||||
mBuffer[c],
|
||||
mFormat,
|
||||
maxOut,
|
||||
mHighQuality);
|
||||
}
|
||||
}
|
||||
// MB: this doesn't take warping into account, replaced with code based on mSamplePos
|
||||
@ -661,10 +749,11 @@ void Mixer::Reposition(double t)
|
||||
int i;
|
||||
|
||||
mTime = t;
|
||||
if( mTime < mT0 )
|
||||
mTime = mT0;
|
||||
if( mTime > mT1 )
|
||||
mTime = mT1;
|
||||
const bool backwards = (mT1 < mT0);
|
||||
if (backwards)
|
||||
mTime = std::max(mT1, (std::min(mT0, mTime)));
|
||||
else
|
||||
mTime = std::max(mT0, (std::min(mT1, mTime)));
|
||||
|
||||
for(i=0; i<mNumInputTracks; i++) {
|
||||
mSamplePos[i] = mInputTrack[i]->TimeToLongSamples(mTime);
|
||||
@ -673,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;
|
||||
|
26
src/Mix.h
26
src/Mix.h
@ -67,12 +67,29 @@ class AUDACITY_DLL_API MixerSpec
|
||||
|
||||
class AUDACITY_DLL_API Mixer {
|
||||
public:
|
||||
//
|
||||
|
||||
// An argument to Mixer's constructor
|
||||
class WarpOptions
|
||||
{
|
||||
public:
|
||||
explicit WarpOptions(TimeTrack *t)
|
||||
: timeTrack(t), minSpeed(0.0), maxSpeed(0.0)
|
||||
{}
|
||||
|
||||
WarpOptions(double min, double max);
|
||||
|
||||
private:
|
||||
friend class Mixer;
|
||||
TimeTrack *timeTrack;
|
||||
double minSpeed, maxSpeed;
|
||||
};
|
||||
|
||||
//
|
||||
// Constructor / Destructor
|
||||
//
|
||||
|
||||
Mixer(int numInputTracks, WaveTrack **inputTracks,
|
||||
TimeTrack *timeTrack,
|
||||
const WarpOptions &warpOptions,
|
||||
double startTime, double stopTime,
|
||||
int numOutChannels, int outBufferSize, bool outInterleaved,
|
||||
double outRate, sampleFormat outFormat,
|
||||
@ -104,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();
|
||||
@ -129,6 +149,7 @@ class AUDACITY_DLL_API Mixer {
|
||||
// Input
|
||||
int mNumInputTracks;
|
||||
WaveTrack **mInputTrack;
|
||||
bool mbVariableRates;
|
||||
TimeTrack *mTimeTrack;
|
||||
sampleCount *mSamplePos;
|
||||
bool mApplyTrackGains;
|
||||
@ -157,6 +178,7 @@ class AUDACITY_DLL_API Mixer {
|
||||
samplePtr *mTemp;
|
||||
float *mFloatBuffer;
|
||||
double mRate;
|
||||
double mSpeed;
|
||||
bool mHighQuality;
|
||||
};
|
||||
|
||||
|
@ -1037,6 +1037,14 @@ AudacityProject::~AudacityProject()
|
||||
wxGetApp().GetRecentFiles()->RemoveMenu(mRecentFilesMenu);
|
||||
}
|
||||
|
||||
AudioIOStartStreamOptions AudacityProject::GetDefaultPlayOptions()
|
||||
{
|
||||
AudioIOStartStreamOptions options;
|
||||
options.timeTrack = GetTracks()->GetTimeTrack();
|
||||
options.listener = this;
|
||||
return options;
|
||||
}
|
||||
|
||||
void AudacityProject::UpdatePrefsVariables()
|
||||
{
|
||||
gPrefs->Read(wxT("/AudioFiles/ShowId3Dialog"), &mShowId3Dialog, true);
|
||||
|
@ -82,6 +82,7 @@ class LyricsWindow;
|
||||
class MixerBoard;
|
||||
class MixerBoardFrame;
|
||||
|
||||
struct AudioIOStartStreamOptions;
|
||||
|
||||
AudacityProject *CreateNewAudacityProject();
|
||||
AUDACITY_DLL_API AudacityProject *GetActiveProject();
|
||||
@ -135,6 +136,8 @@ class AUDACITY_DLL_API AudacityProject: public wxFrame,
|
||||
const wxPoint & pos, const wxSize & size);
|
||||
virtual ~AudacityProject();
|
||||
|
||||
AudioIOStartStreamOptions GetDefaultPlayOptions();
|
||||
|
||||
TrackList *GetTracks() { return mTracks; }
|
||||
UndoManager *GetUndoManager() { return &mUndoManager; }
|
||||
|
||||
|
@ -92,6 +92,24 @@ void ClearSamples(samplePtr src, sampleFormat format,
|
||||
memset(src + start*size, 0, len*size);
|
||||
}
|
||||
|
||||
void ReverseSamples(samplePtr src, sampleFormat format,
|
||||
int start, int len)
|
||||
{
|
||||
int size = SAMPLE_SIZE(format);
|
||||
samplePtr first = src + start * size;
|
||||
samplePtr last = src + (start + len - 1) * size;
|
||||
enum { fixedSize = SAMPLE_SIZE(floatSample) };
|
||||
wxASSERT(size <= fixedSize);
|
||||
char temp[fixedSize];
|
||||
while (first < last) {
|
||||
memcpy(temp, first, size);
|
||||
memcpy(first, last, size);
|
||||
memcpy(last, temp, size);
|
||||
first += size;
|
||||
last -= size;
|
||||
}
|
||||
}
|
||||
|
||||
void CopySamples(samplePtr src, sampleFormat srcFormat,
|
||||
samplePtr dst, sampleFormat dstFormat,
|
||||
unsigned int len,
|
||||
|
@ -70,6 +70,9 @@ void CopySamplesNoDither(samplePtr src, sampleFormat srcFormat,
|
||||
void ClearSamples(samplePtr buffer, sampleFormat format,
|
||||
int start, int len);
|
||||
|
||||
void ReverseSamples(samplePtr buffer, sampleFormat format,
|
||||
int start, int len);
|
||||
|
||||
//
|
||||
// This must be called on startup and everytime new ditherers
|
||||
// are set in preferences.
|
||||
|
@ -1248,7 +1248,7 @@ bool Sequence::Set(samplePtr buffer, sampleFormat format,
|
||||
}
|
||||
|
||||
bool Sequence::GetWaveDisplay(float *min, float *max, float *rms,int* bl,
|
||||
int len, sampleCount *where,
|
||||
int len, const sampleCount *where,
|
||||
double samplesPerPixel)
|
||||
{
|
||||
sampleCount s0 = where[0];
|
||||
|
@ -80,7 +80,7 @@ class Sequence: public XMLTagHandler {
|
||||
sampleCount start, sampleCount len);
|
||||
|
||||
bool GetWaveDisplay(float *min, float *max, float *rms,int* bl,
|
||||
int len, sampleCount *where,
|
||||
int len, const sampleCount *where,
|
||||
double samplesPerPixel);
|
||||
|
||||
bool Copy(sampleCount s0, sampleCount s1, Sequence **dest);
|
||||
|
@ -585,12 +585,6 @@ TrackPanel::TrackPanel(wxWindow * parent, wxWindowID id,
|
||||
mSelStartValid = false;
|
||||
mSelStart = 0;
|
||||
|
||||
#ifdef EXPERIMENTAL_SCRUBBING
|
||||
mScrubbing = false;
|
||||
mLastScrubTime = 0;
|
||||
mLastScrubPosition = 0;
|
||||
#endif
|
||||
|
||||
mInitialTrackSelection = new std::vector<bool>;
|
||||
}
|
||||
|
||||
@ -979,7 +973,7 @@ void TrackPanel::OnTimer()
|
||||
AudacityProject *p = GetProject();
|
||||
|
||||
if ((p->GetAudioIOToken() > 0) &&
|
||||
gAudioIO->IsStreamActive(p->GetAudioIOToken()))
|
||||
gAudioIO->IsStreamActive(p->GetAudioIOToken()))
|
||||
{
|
||||
// Update lyrics display.
|
||||
LyricsWindow* pLyricsWindow = p->GetLyricsWindow();
|
||||
@ -997,39 +991,13 @@ void TrackPanel::OnTimer()
|
||||
// audacityAudioCallback where it calls gAudioIO->mOutputMeter->UpdateDisplay().
|
||||
MixerBoard* pMixerBoard = this->GetMixerBoard();
|
||||
if (pMixerBoard &&
|
||||
(p->GetAudioIOToken() > 0) &&
|
||||
gAudioIO->IsStreamActive(p->GetAudioIOToken()))
|
||||
(p->GetAudioIOToken() > 0) &&
|
||||
gAudioIO->IsStreamActive(p->GetAudioIOToken()))
|
||||
{
|
||||
pMixerBoard->UpdateMeters(gAudioIO->GetStreamTime(),
|
||||
(p->mLastPlayMode == loopedPlay));
|
||||
(p->mLastPlayMode == loopedPlay));
|
||||
}
|
||||
|
||||
#ifdef EXPERIMENTAL_SCRUBBING
|
||||
if (mScrubbing
|
||||
&&
|
||||
gAudioIO->IsStreamActive(GetProject()->GetAudioIOToken()))
|
||||
{
|
||||
if (gAudioIO->GetLastPlaybackTime() < mLastScrubTime) {
|
||||
// Allow some audio catch up
|
||||
}
|
||||
else {
|
||||
wxMouseState state(::wxGetMouseState());
|
||||
wxCoord xx = state.GetX();
|
||||
ScreenToClient(&xx, NULL);
|
||||
double leadPosition = PositionToTime(xx, GetLeftOffset());
|
||||
if (mLastScrubPosition != leadPosition) {
|
||||
wxLongLong clockTime = ::wxGetLocalTimeMillis();
|
||||
double lagPosition = gAudioIO->GetStreamTime();
|
||||
|
||||
gAudioIO->SeekStream(leadPosition - lagPosition);
|
||||
|
||||
mLastScrubPosition = leadPosition;
|
||||
mLastScrubTime = clockTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Check whether we were playing or recording, but the stream has stopped.
|
||||
if (p->GetAudioIOToken()>0 &&
|
||||
!gAudioIO->IsStreamActive(p->GetAudioIOToken()))
|
||||
@ -1411,27 +1379,29 @@ void TrackPanel::OnPaint(wxPaintEvent & /* event */)
|
||||
mRefreshBacking = false;
|
||||
|
||||
// Redraw the backing bitmap
|
||||
DrawTracks( &mBackingDC );
|
||||
DrawTracks(&mBackingDC);
|
||||
|
||||
// Copy it to the display
|
||||
dc->Blit( 0, 0, mBacking->GetWidth(), mBacking->GetHeight(), &mBackingDC, 0, 0 );
|
||||
dc->Blit(0, 0, mBacking->GetWidth(), mBacking->GetHeight(), &mBackingDC, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Copy full, possibly clipped, damage rectange
|
||||
dc->Blit( box.x, box.y, box.width, box.height, &mBackingDC, box.x, box.y );
|
||||
dc->Blit(box.x, box.y, box.width, box.height, &mBackingDC, box.x, box.y);
|
||||
}
|
||||
|
||||
// Done with the clipped DC
|
||||
delete dc;
|
||||
|
||||
// Drawing now goes directly to the client area
|
||||
wxClientDC cdc( this );
|
||||
wxClientDC cdc(this);
|
||||
|
||||
// Update the indicator in case it was damaged if this project is playing
|
||||
|
||||
// PRL: mIndicatorShowing never becomes true!
|
||||
AudacityProject* p = GetProject();
|
||||
if (!gAudioIO->IsPaused() &&
|
||||
( mIndicatorShowing || gAudioIO->IsStreamActive(p->GetAudioIOToken())))
|
||||
(mIndicatorShowing || gAudioIO->IsStreamActive(p->GetAudioIOToken())))
|
||||
{
|
||||
// We just want to repair, not update the old, so set the second param to true.
|
||||
// This is important because this onPaint could be for just some of the tracks.
|
||||
@ -1439,8 +1409,8 @@ void TrackPanel::OnPaint(wxPaintEvent & /* event */)
|
||||
}
|
||||
|
||||
// Draw the cursor
|
||||
if( mViewInfo->selectedRegion.isPoint())
|
||||
DoDrawCursor( cdc );
|
||||
if (mViewInfo->selectedRegion.isPoint())
|
||||
DoDrawCursor(cdc);
|
||||
|
||||
#if DEBUG_DRAW_TIMING
|
||||
sw.Pause();
|
||||
@ -2054,52 +2024,6 @@ void TrackPanel::HandleSelect(wxMouseEvent & event)
|
||||
}
|
||||
|
||||
} else if (event.LeftUp() || event.RightUp()) {
|
||||
#ifdef EXPERIMENTAL_SCRUBBING
|
||||
if(mScrubbing) {
|
||||
if (gAudioIO->IsBusy()) {
|
||||
AudacityProject *p = GetActiveProject();
|
||||
if (p) {
|
||||
ControlToolBar * ctb = p->GetControlToolBar();
|
||||
ctb->StopPlaying();
|
||||
}
|
||||
}
|
||||
|
||||
if (mAdjustSelectionEdges) {
|
||||
if (event.ShiftDown()) {
|
||||
// Adjust time selection as if shift-left click at end
|
||||
const double selend = PositionToTime(event.m_x, GetLeftOffset());
|
||||
SelectionBoundary boundary = ChooseTimeBoundary(selend, false);
|
||||
switch (boundary)
|
||||
{
|
||||
case SBLeft:
|
||||
mViewInfo->selectedRegion.setT0(selend);
|
||||
break;
|
||||
case SBRight:
|
||||
mViewInfo->selectedRegion.setT1(selend);
|
||||
break;
|
||||
default:
|
||||
wxASSERT(false);
|
||||
}
|
||||
UpdateSelectionDisplay();
|
||||
}
|
||||
else {
|
||||
// Adjust time selection as if left click
|
||||
StartSelection(event.m_x, r.x);
|
||||
DisplaySelection();
|
||||
}
|
||||
}
|
||||
|
||||
mScrubbing = false;
|
||||
}
|
||||
else if (event.CmdDown()) {
|
||||
// A control-click will set just the indicator to the clicked spot,
|
||||
// and turn playback on -- but delayed until button up,
|
||||
// and only if no intervening drag
|
||||
StartOrJumpPlayback(event);
|
||||
}
|
||||
// Don't return yet
|
||||
#endif
|
||||
|
||||
if (mSnapManager) {
|
||||
delete mSnapManager;
|
||||
mSnapManager = NULL;
|
||||
@ -2197,7 +2121,8 @@ void TrackPanel::StartOrJumpPlayback(wxMouseEvent &event)
|
||||
//the clicked point
|
||||
ControlToolBar * ctb = p->GetControlToolBar();
|
||||
//ctb->SetPlay(true);// Not needed as done in PlayPlayRegion
|
||||
ctb->PlayPlayRegion(clicktime, endtime,false) ;
|
||||
ctb->PlayPlayRegion
|
||||
(SelectedRegion(clicktime, endtime), p->GetDefaultPlayOptions());
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -2208,37 +2133,12 @@ void TrackPanel::StartOrJumpPlayback(wxMouseEvent &event)
|
||||
//require a new method in ControlToolBar: SetPause();
|
||||
ControlToolBar * ctb = p->GetControlToolBar();
|
||||
ctb->StopPlaying();
|
||||
ctb->PlayPlayRegion(clicktime,endtime,false) ;
|
||||
ctb->PlayPlayRegion(SelectedRegion(clicktime, endtime), p->GetDefaultPlayOptions());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#ifdef EXPERIMENTAL_SCRUBBING
|
||||
void TrackPanel::StartScrubbing(double position)
|
||||
{
|
||||
AudacityProject *p = GetActiveProject();
|
||||
if (p &&
|
||||
// Should I make a bigger tolerance than zero?
|
||||
mLastScrubPosition != position) {
|
||||
ControlToolBar * ctb = p->GetControlToolBar();
|
||||
bool busy = gAudioIO->IsBusy();
|
||||
double maxTime = p->GetTracks()->GetEndTime();
|
||||
|
||||
if (busy)
|
||||
ctb->StopPlaying();
|
||||
|
||||
ctb->PlayPlayRegion(0, maxTime, false, false,
|
||||
0,
|
||||
&position);
|
||||
mScrubbing = true;
|
||||
mLastScrubPosition = position;
|
||||
mLastScrubTime = ::wxGetLocalTimeMillis();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
/// This method gets called when we're handling selection
|
||||
/// and the mouse was just clicked.
|
||||
void TrackPanel::SelectionHandleClick(wxMouseEvent & event,
|
||||
@ -2297,11 +2197,6 @@ void TrackPanel::SelectionHandleClick(wxMouseEvent & event,
|
||||
|
||||
if (event.ShiftDown()
|
||||
|
||||
#ifdef EXPERIMENTAL_SCRUBBING
|
||||
// Ctrl prevails over Shift with scrubbing enabled
|
||||
&& !event.CmdDown()
|
||||
#endif
|
||||
|
||||
#ifdef USE_MIDI
|
||||
&& !stretch
|
||||
#endif
|
||||
@ -2369,17 +2264,10 @@ void TrackPanel::SelectionHandleClick(wxMouseEvent & event,
|
||||
&& !stretch
|
||||
#endif
|
||||
) {
|
||||
#ifdef EXPERIMENTAL_SCRUBBING
|
||||
// With scrubbing enabled, playback happens on button up, not down,
|
||||
// and only if we do not start a scrub in the interim.
|
||||
mScrubbing = false;
|
||||
mLastScrubPosition = PositionToTime(event.m_x, GetLeftOffset());
|
||||
#else
|
||||
StartOrJumpPlayback(event);
|
||||
|
||||
// Not starting a drag
|
||||
SetCapturedTrack(NULL, IsUncaptured);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@ -3130,15 +3018,7 @@ void TrackPanel::SelectionHandleDrag(wxMouseEvent & event, Track *clickedTrack)
|
||||
return;
|
||||
|
||||
if (event.CmdDown()) {
|
||||
#ifdef EXPERIMENTAL_SCRUBBING
|
||||
if (!mScrubbing) {
|
||||
double position = PositionToTime(event.m_x, GetLeftOffset());
|
||||
StartScrubbing(position);
|
||||
}
|
||||
else
|
||||
#else
|
||||
// Ctrl-drag has no meaning, fuhggeddaboudit
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -313,9 +313,6 @@ class AUDACITY_DLL_API TrackPanel:public wxPanel {
|
||||
virtual void HandleSelect(wxMouseEvent & event);
|
||||
virtual void SelectionHandleDrag(wxMouseEvent &event, Track *pTrack);
|
||||
void StartOrJumpPlayback(wxMouseEvent &event);
|
||||
#ifdef EXPERIMENTAL_SCRUBBING
|
||||
void StartScrubbing(double position);
|
||||
#endif
|
||||
virtual void SelectionHandleClick(wxMouseEvent &event,
|
||||
Track* pTrack, wxRect r);
|
||||
virtual void StartSelection (int mouseXCoordinate, int trackLeftEdge);
|
||||
@ -764,12 +761,6 @@ protected:
|
||||
int mMoveUpThreshold;
|
||||
int mMoveDownThreshold;
|
||||
|
||||
#ifdef EXPERIMENTAL_SCRUBBING
|
||||
bool mScrubbing;
|
||||
wxLongLong mLastScrubTime; // milliseconds
|
||||
double mLastScrubPosition;
|
||||
#endif
|
||||
|
||||
wxCursor *mArrowCursor;
|
||||
wxCursor *mPencilCursor;
|
||||
wxCursor *mSelectCursor;
|
||||
|
@ -2265,7 +2265,7 @@ void Effect::Preview(bool dryOnly)
|
||||
#ifdef EXPERIMENTAL_MIDI_OUT
|
||||
empty,
|
||||
#endif
|
||||
NULL, rate, t0, t1, NULL);
|
||||
rate, t0, t1);
|
||||
|
||||
if (token) {
|
||||
int previewing = eProgressSuccess;
|
||||
@ -2959,7 +2959,9 @@ void EffectUIHost::OnPlay(wxCommandEvent & WXUNUSED(evt))
|
||||
mPlayPos = mRegion.t1();
|
||||
}
|
||||
|
||||
mProject->GetControlToolBar()->PlayPlayRegion(mPlayPos, mRegion.t1());
|
||||
mProject->GetControlToolBar()->PlayPlayRegion
|
||||
(SelectedRegion(mPlayPos, mRegion.t1()),
|
||||
mProject->GetDefaultPlayOptions());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,7 +277,7 @@ Mixer* ExportPlugin::CreateMixer(int numInputTracks, WaveTrack **inputTracks,
|
||||
{
|
||||
// MB: the stop time should not be warped, this was a bug.
|
||||
return new Mixer(numInputTracks, inputTracks,
|
||||
timeTrack,
|
||||
Mixer::WarpOptions(timeTrack),
|
||||
startTime, stopTime,
|
||||
numOutChannels, outBufferSize, outInterleaved,
|
||||
outRate, outFormat,
|
||||
|
@ -467,34 +467,46 @@ bool ControlToolBar::IsRecordDown()
|
||||
{
|
||||
return mRecord->IsDown();
|
||||
}
|
||||
void ControlToolBar::PlayPlayRegion(double t0, double t1,
|
||||
bool looped /* = false */,
|
||||
bool cutpreview /* = false */,
|
||||
TimeTrack *timetrack /* = NULL */,
|
||||
const double *pStartTime /* = NULL */)
|
||||
|
||||
int ControlToolBar::PlayPlayRegion(const SelectedRegion &selectedRegion,
|
||||
const AudioIOStartStreamOptions &options,
|
||||
bool cutpreview, /* = false */
|
||||
bool backwards /* = false */)
|
||||
{
|
||||
// Uncomment this for laughs!
|
||||
// backwards = true;
|
||||
|
||||
double t0 = selectedRegion.t0();
|
||||
double t1 = selectedRegion.t1();
|
||||
// SelectedRegion guarantees t0 <= t1, so we need another boolean argument
|
||||
// to indicate backwards play.
|
||||
const bool looped = options.playLooped;
|
||||
|
||||
if (backwards)
|
||||
std::swap(t0, t1);
|
||||
|
||||
SetPlay(true, looped, cutpreview);
|
||||
|
||||
if (gAudioIO->IsBusy()) {
|
||||
SetPlay(false);
|
||||
return;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (cutpreview && t0==t1) {
|
||||
SetPlay(false);
|
||||
return; /* msmeyer: makes no sense */
|
||||
return -1; /* msmeyer: makes no sense */
|
||||
}
|
||||
|
||||
AudacityProject *p = GetActiveProject();
|
||||
if (!p) {
|
||||
SetPlay(false);
|
||||
return; // Should never happen, but...
|
||||
return -1; // Should never happen, but...
|
||||
}
|
||||
|
||||
TrackList *t = p->GetTracks();
|
||||
if (!t) {
|
||||
mPlay->PopUp();
|
||||
return; // Should never happen, but...
|
||||
return -1; // Should never happen, but...
|
||||
}
|
||||
|
||||
bool hasaudio = false;
|
||||
@ -512,7 +524,7 @@ void ControlToolBar::PlayPlayRegion(double t0, double t1,
|
||||
|
||||
if (!hasaudio) {
|
||||
SetPlay(false);
|
||||
return; // No need to continue without audio tracks
|
||||
return -1; // No need to continue without audio tracks
|
||||
}
|
||||
|
||||
double maxofmins,minofmaxs;
|
||||
@ -547,7 +559,9 @@ void ControlToolBar::PlayPlayRegion(double t0, double t1,
|
||||
t1 = t->GetEndTime();
|
||||
}
|
||||
else {
|
||||
// always t0 < t1 right?
|
||||
// maybe t1 < t0, with backwards scrubbing for instance
|
||||
if (backwards)
|
||||
std::swap(t0, t1);
|
||||
|
||||
// the set intersection between the play region and the
|
||||
// valid range maximum of lower bounds
|
||||
@ -565,61 +579,71 @@ void ControlToolBar::PlayPlayRegion(double t0, double t1,
|
||||
// we test if the intersection has no volume
|
||||
if (minofmaxs <= maxofmins) {
|
||||
// no volume; play nothing
|
||||
return;
|
||||
return -1;
|
||||
}
|
||||
else {
|
||||
t0 = maxofmins;
|
||||
t1 = minofmaxs;
|
||||
}
|
||||
|
||||
if (backwards)
|
||||
std::swap(t0, t1);
|
||||
}
|
||||
|
||||
// Can't play before 0...either shifted or latencey corrected tracks
|
||||
if (t0 < 0.0) {
|
||||
// Can't play before 0...either shifted or latency corrected tracks
|
||||
if (t0 < 0.0)
|
||||
t0 = 0.0;
|
||||
}
|
||||
if (t1 < 0.0)
|
||||
t1 = 0.0;
|
||||
|
||||
int token = -1;
|
||||
bool success = false;
|
||||
if (t1 > t0) {
|
||||
int token;
|
||||
if (t1 != t0) {
|
||||
if (cutpreview) {
|
||||
const double tless = std::min(t0, t1);
|
||||
const double tgreater = std::max(t0, t1);
|
||||
double beforeLen, afterLen;
|
||||
gPrefs->Read(wxT("/AudioIO/CutPreviewBeforeLen"), &beforeLen, 2.0);
|
||||
gPrefs->Read(wxT("/AudioIO/CutPreviewAfterLen"), &afterLen, 1.0);
|
||||
double tcp0 = t0-beforeLen;
|
||||
double tcp1 = (t1+afterLen) - (t1-t0);
|
||||
SetupCutPreviewTracks(tcp0, t0, t1, tcp1);
|
||||
double tcp0 = tless-beforeLen;
|
||||
double diff = tgreater - tless;
|
||||
double tcp1 = (tgreater+afterLen) - diff;
|
||||
SetupCutPreviewTracks(tcp0, tless, tgreater, tcp1);
|
||||
if (backwards)
|
||||
std::swap(tcp0, tcp1);
|
||||
if (mCutPreviewTracks)
|
||||
{
|
||||
AudioIOStartStreamOptions myOptions = options;
|
||||
myOptions.cutPreviewGapStart = t0;
|
||||
myOptions.cutPreviewGapLen = t1 - t0;
|
||||
token = gAudioIO->StartStream(
|
||||
mCutPreviewTracks->GetWaveTrackArray(false),
|
||||
WaveTrackArray(),
|
||||
#ifdef EXPERIMENTAL_MIDI_OUT
|
||||
NoteTrackArray(),
|
||||
#endif
|
||||
timetrack, p->GetRate(), tcp0, tcp1, p, false,
|
||||
t0, t1-t0,
|
||||
pStartTime);
|
||||
p->GetRate(), tcp0, tcp1, myOptions);
|
||||
} else
|
||||
{
|
||||
// Cannot create cut preview tracks, clean up and exit
|
||||
SetPlay(false);
|
||||
SetStop(false);
|
||||
SetRecord(false);
|
||||
return;
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
// Lifted the following into AudacityProject::GetDefaultPlayOptions()
|
||||
/*
|
||||
if (!timetrack) {
|
||||
timetrack = t->GetTimeTrack();
|
||||
}
|
||||
*/
|
||||
token = gAudioIO->StartStream(t->GetWaveTrackArray(false),
|
||||
WaveTrackArray(),
|
||||
#ifdef EXPERIMENTAL_MIDI_OUT
|
||||
t->GetNoteTrackArray(false),
|
||||
#endif
|
||||
timetrack,
|
||||
p->GetRate(), t0, t1, p, looped,
|
||||
0, 0,
|
||||
pStartTime);
|
||||
p->GetRate(), t0, t1, options);
|
||||
}
|
||||
if (token != 0) {
|
||||
success = true;
|
||||
@ -648,7 +672,10 @@ void ControlToolBar::PlayPlayRegion(double t0, double t1,
|
||||
SetPlay(false);
|
||||
SetStop(false);
|
||||
SetRecord(false);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
void ControlToolBar::PlayCurrentRegion(bool looped /* = false */,
|
||||
@ -666,9 +693,12 @@ void ControlToolBar::PlayCurrentRegion(bool looped /* = false */,
|
||||
double playRegionStart, playRegionEnd;
|
||||
p->GetPlayRegion(&playRegionStart, &playRegionEnd);
|
||||
|
||||
PlayPlayRegion(playRegionStart,
|
||||
playRegionEnd,
|
||||
looped, cutpreview);
|
||||
AudioIOStartStreamOptions options(p->GetDefaultPlayOptions());
|
||||
options.playLooped = looped;
|
||||
if (cutpreview)
|
||||
options.timeTrack = NULL;
|
||||
PlayPlayRegion(SelectedRegion(playRegionStart, playRegionEnd),
|
||||
options, cutpreview);
|
||||
}
|
||||
}
|
||||
|
||||
@ -898,14 +928,14 @@ void ControlToolBar::OnRecord(wxCommandEvent &evt)
|
||||
#ifdef AUTOMATED_INPUT_LEVEL_ADJUSTMENT
|
||||
gAudioIO->AILAInitialize();
|
||||
#endif
|
||||
|
||||
|
||||
AudioIOStartStreamOptions options(p->GetDefaultPlayOptions());
|
||||
int token = gAudioIO->StartStream(playbackTracks,
|
||||
newRecordingTracks,
|
||||
#ifdef EXPERIMENTAL_MIDI_OUT
|
||||
midiTracks,
|
||||
#endif
|
||||
t->GetTimeTrack(),
|
||||
p->GetRate(), t0, t1, p);
|
||||
p->GetRate(), t0, t1, options);
|
||||
|
||||
bool success = (token != 0);
|
||||
|
||||
|
@ -30,6 +30,9 @@ class AudacityProject;
|
||||
class TrackList;
|
||||
class TimeTrack;
|
||||
|
||||
struct AudioIOStartStreamOptions;
|
||||
class SelectedRegion;
|
||||
|
||||
// In the GUI, ControlToolBar appears as the "Transport Toolbar". "Control Toolbar" is historic.
|
||||
class ControlToolBar:public ToolBar {
|
||||
|
||||
@ -64,13 +67,10 @@ class ControlToolBar:public ToolBar {
|
||||
// play from current cursor.
|
||||
void PlayCurrentRegion(bool looped = false, bool cutpreview = false);
|
||||
// Play the region [t0,t1]
|
||||
void PlayPlayRegion(double t0, double t1,
|
||||
bool looped = false,
|
||||
bool cutpreview = false,
|
||||
TimeTrack *timetrack = NULL,
|
||||
// May be other than t0,
|
||||
// but will be constrained between t0 and t1
|
||||
const double *pStartTime = NULL);
|
||||
// Return the Audio IO token or -1 for failure
|
||||
int PlayPlayRegion(const SelectedRegion &selectedRegion,
|
||||
const AudioIOStartStreamOptions &options,
|
||||
bool cutpreview = false, bool backwards = false);
|
||||
void PlayDefault();
|
||||
|
||||
// Stop playing
|
||||
|
@ -439,11 +439,13 @@ void TranscriptionToolBar::PlayAtSpeed(bool looped, bool cutPreview)
|
||||
#ifdef EXPERIMENTAL_MIDI_OUT
|
||||
gAudioIO->SetMidiPlaySpeed(mPlaySpeed);
|
||||
#endif
|
||||
p->GetControlToolBar()->PlayPlayRegion(playRegionStart,
|
||||
playRegionEnd,
|
||||
looped,
|
||||
cutPreview,
|
||||
mTimeTrack);
|
||||
AudioIOStartStreamOptions options(p->GetDefaultPlayOptions());
|
||||
options.playLooped = looped;
|
||||
options.timeTrack = mTimeTrack;
|
||||
p->GetControlToolBar()->PlayPlayRegion
|
||||
(SelectedRegion(playRegionStart, playRegionEnd),
|
||||
options,
|
||||
cutPreview);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user