diff --git a/src/AudioIO.cpp b/src/AudioIO.cpp index 2f37eb14a..8dbede16d 100644 --- a/src/AudioIO.cpp +++ b/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. diff --git a/src/AudioIO.h b/src/AudioIO.h index 6d5bd8db8..7c99038b9 100644 --- a/src/AudioIO.h +++ b/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 diff --git a/src/Envelope.cpp b/src/Envelope.cpp index a5658f047..b4892292e 100644 --- a/src/Envelope.cpp +++ b/src/Envelope.cpp @@ -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++; + } } } } diff --git a/src/Envelope.h b/src/Envelope.h index 1a33abc0f..481657bd4 100644 --- a/src/Envelope.h +++ b/src/Envelope.h @@ -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)); } diff --git a/src/Experimental.h b/src/Experimental.h index b86b7682d..3a1da9ab4 100644 --- a/src/Experimental.h +++ b/src/Experimental.h @@ -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 diff --git a/src/Menus.cpp b/src/Menus.cpp index e0e34119a..b247cf3a1 100644 --- a/src/Menus.cpp +++ b/src/Menus.cpp @@ -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() diff --git a/src/Mix.cpp b/src/Mix.cpp index a1cdc5853..2f88d048d 100644 --- a/src/Mix.cpp +++ b/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; iTimeToLongSamples(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; iGetRate()); - 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; iGet((samplePtr)mFloatBuffer, floatSample, *pos - (slen - 1), slen); + track->GetEnvelopeValues(mEnvValues, slen, t - (slen - 1) / mRate, 1.0 / mRate); + for(int i=0; iGet((samplePtr)mFloatBuffer, floatSample, *pos, slen); + track->GetEnvelopeValues(mEnvValues, slen, t, 1.0 / mRate); + for(int i=0; iGetRate() != 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 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; iTimeToLongSamples(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; diff --git a/src/Mix.h b/src/Mix.h index 90f859d67..b4274d589 100644 --- a/src/Mix.h +++ b/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; }; diff --git a/src/Project.cpp b/src/Project.cpp index 0218678a2..66278ab57 100644 --- a/src/Project.cpp +++ b/src/Project.cpp @@ -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); diff --git a/src/Project.h b/src/Project.h index 587564f01..b8166838d 100644 --- a/src/Project.h +++ b/src/Project.h @@ -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; } diff --git a/src/SampleFormat.cpp b/src/SampleFormat.cpp index 7ea934079..1ac38f81c 100644 --- a/src/SampleFormat.cpp +++ b/src/SampleFormat.cpp @@ -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, diff --git a/src/SampleFormat.h b/src/SampleFormat.h index d2b9e8e04..6d0dd9902 100644 --- a/src/SampleFormat.h +++ b/src/SampleFormat.h @@ -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. diff --git a/src/Sequence.cpp b/src/Sequence.cpp index a995911d2..293610de6 100644 --- a/src/Sequence.cpp +++ b/src/Sequence.cpp @@ -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]; diff --git a/src/Sequence.h b/src/Sequence.h index eca3cb3d9..8e73921d8 100644 --- a/src/Sequence.h +++ b/src/Sequence.h @@ -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); diff --git a/src/TrackPanel.cpp b/src/TrackPanel.cpp index 0d7743c9f..73c26548b 100644 --- a/src/TrackPanel.cpp +++ b/src/TrackPanel.cpp @@ -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; } @@ -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; } diff --git a/src/TrackPanel.h b/src/TrackPanel.h index a14f7d1c3..e21c38e0a 100644 --- a/src/TrackPanel.h +++ b/src/TrackPanel.h @@ -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; diff --git a/src/effects/Effect.cpp b/src/effects/Effect.cpp index 804455dfa..75bb92bb6 100644 --- a/src/effects/Effect.cpp +++ b/src/effects/Effect.cpp @@ -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()); } } diff --git a/src/export/Export.cpp b/src/export/Export.cpp index d974d9f23..0ac5c4e82 100644 --- a/src/export/Export.cpp +++ b/src/export/Export.cpp @@ -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, diff --git a/src/toolbars/ControlToolBar.cpp b/src/toolbars/ControlToolBar.cpp index 9f0b0c4f6..ec11310e9 100644 --- a/src/toolbars/ControlToolBar.cpp +++ b/src/toolbars/ControlToolBar.cpp @@ -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); diff --git a/src/toolbars/ControlToolBar.h b/src/toolbars/ControlToolBar.h index a2ee3935b..847752cf1 100644 --- a/src/toolbars/ControlToolBar.h +++ b/src/toolbars/ControlToolBar.h @@ -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 diff --git a/src/toolbars/TranscriptionToolBar.cpp b/src/toolbars/TranscriptionToolBar.cpp index 0e603bcdb..9e71f691c 100644 --- a/src/toolbars/TranscriptionToolBar.cpp +++ b/src/toolbars/TranscriptionToolBar.cpp @@ -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); } }