diff --git a/src/AudioIO.cpp b/src/AudioIO.cpp index 756c4e398..4320e964c 100644 --- a/src/AudioIO.cpp +++ b/src/AudioIO.cpp @@ -1650,6 +1650,8 @@ int AudioIO::StartStream(const ConstWaveTrackArray &playbackTracks, double t0, double t1, const AudioIOStartStreamOptions &options) { + auto cleanup = finally ( [this] { ClearRecordingException(); } ); + if( IsBusy() ) return 0; @@ -2250,6 +2252,8 @@ void AudioIO::SetMeters() void AudioIO::StopStream() { + auto cleanup = finally ( [this] { ClearRecordingException(); } ); + if( mPortStreamV19 == NULL #ifdef EXPERIMENTAL_MIDI_OUT && mMidiStream == NULL @@ -3420,6 +3424,25 @@ void AudioIO::FillBuffers() { unsigned int i; + auto delayedHandler = [this] ( AudacityException * pException ) { + // In the main thread, stop recording + // This is one place where the application handles disk + // exhaustion exceptions from wave track operations, without rolling + // back to the last pushed undo state. Instead, partial recording + // results are pushed as a NEW undo state. For this reason, as + // commented elsewhere, we want an exception safety guarantee for + // the output wave tracks, after the failed append operation, that + // the tracks remain as they were after the previous successful + // (block-level) appends. + + // Note that the Flush in StopStream() may throw another exception, + // but StopStream() contains that exception, and the logic in + // AudacityException::DelayedHandlerAction prevents redundant message + // boxes. + StopStream(); + DefaultDelayedHandlerAction{}( pException ); + }; + if (mPlaybackTracks.size() > 0) { // Though extremely unlikely, it is possible that some buffers @@ -3595,78 +3618,97 @@ void AudioIO::FillBuffers() } } // end of playback buffering - if (mCaptureTracks.size() > 0) // start record buffering - { - auto commonlyAvail = GetCommonlyAvailCapture(); + if (!mRecordingException && + mCaptureTracks.size() > 0) + GuardedCall( [&] { + // start record buffering + auto commonlyAvail = GetCommonlyAvailCapture(); - // - // Determine how much this will add to captured tracks - // - double deltat = commonlyAvail / mRate; + // + // Determine how much this will add to captured tracks + // + double deltat = commonlyAvail / mRate; - if (mAudioThreadShouldCallFillBuffersOnce || - deltat >= mMinCaptureSecsToCopy) - { - // Append captured samples to the end of the WaveTracks. - // The WaveTracks have their own buffering for efficiency. - AutoSaveFile blockFileLog; - auto numChannels = mCaptureTracks.size(); - - for( i = 0; (int)i < numChannels; i++ ) + if (mAudioThreadShouldCallFillBuffersOnce || + deltat >= mMinCaptureSecsToCopy) { - auto avail = commonlyAvail; - sampleFormat trackFormat = mCaptureTracks[i]->GetSampleFormat(); + // Append captured samples to the end of the WaveTracks. + // The WaveTracks have their own buffering for efficiency. + AutoSaveFile blockFileLog; + auto numChannels = mCaptureTracks.size(); - AutoSaveFile appendLog; - - if( mFactor == 1.0 ) + for( i = 0; (int)i < numChannels; i++ ) { - SampleBuffer temp(avail, trackFormat); - const auto got = + auto avail = commonlyAvail; + sampleFormat trackFormat = mCaptureTracks[i]->GetSampleFormat(); + + AutoSaveFile appendLog; + + if( mFactor == 1.0 ) + { + SampleBuffer temp(avail, trackFormat); + const auto got = mCaptureBuffers[i]->Get(temp.ptr(), trackFormat, avail); - // wxASSERT(got == avail); - // but we can't assert in this thread - wxUnusedVar(got); - mCaptureTracks[i]-> Append(temp.ptr(), trackFormat, avail, 1, - &appendLog); - } - else - { - size_t size = lrint(avail * mFactor); - SampleBuffer temp1(avail, floatSample); - SampleBuffer temp2(size, floatSample); - const auto got = + // wxASSERT(got == avail); + // but we can't assert in this thread + wxUnusedVar(got); + // see comment in second handler about guarantee + mCaptureTracks[i]-> Append(temp.ptr(), trackFormat, avail, 1, + &appendLog); + } + else + { + size_t size = lrint(avail * mFactor); + SampleBuffer temp1(avail, floatSample); + SampleBuffer temp2(size, floatSample); + const auto got = mCaptureBuffers[i]->Get(temp1.ptr(), floatSample, avail); - // wxASSERT(got == avail); - // but we can't assert in this thread - wxUnusedVar(got); - /* we are re-sampling on the fly. The last resampling call - * must flush any samples left in the rate conversion buffer - * so that they get recorded - */ - const auto results = + // wxASSERT(got == avail); + // but we can't assert in this thread + wxUnusedVar(got); + /* we are re-sampling on the fly. The last resampling call + * must flush any samples left in the rate conversion buffer + * so that they get recorded + */ + const auto results = mResample[i]->Process(mFactor, (float *)temp1.ptr(), avail, - !IsStreamActive(), (float *)temp2.ptr(), size); - size = results.second; - mCaptureTracks[i]-> Append(temp2.ptr(), floatSample, size, 1, - &appendLog); + !IsStreamActive(), (float *)temp2.ptr(), size); + size = results.second; + // see comment in second handler about guarantee + mCaptureTracks[i]-> Append(temp2.ptr(), floatSample, size, 1, + &appendLog); + } + + if (!appendLog.IsEmpty()) + { + blockFileLog.StartTag(wxT("recordingrecovery")); + blockFileLog.WriteAttr(wxT("id"), mCaptureTracks[i]->GetAutoSaveIdent()); + blockFileLog.WriteAttr(wxT("channel"), (int)i); + blockFileLog.WriteAttr(wxT("numchannels"), numChannels); + blockFileLog.WriteSubTree(appendLog); + blockFileLog.EndTag(wxT("recordingrecovery")); + } } - if (!appendLog.IsEmpty()) - { - blockFileLog.StartTag(wxT("recordingrecovery")); - blockFileLog.WriteAttr(wxT("id"), mCaptureTracks[i]->GetAutoSaveIdent()); - blockFileLog.WriteAttr(wxT("channel"), (int)i); - blockFileLog.WriteAttr(wxT("numchannels"), numChannels); - blockFileLog.WriteSubTree(appendLog); - blockFileLog.EndTag(wxT("recordingrecovery")); - } + if (mListener && !blockFileLog.IsEmpty()) + mListener->OnAudioIONewBlockFiles(blockFileLog); } - - if (mListener && !blockFileLog.IsEmpty()) - mListener->OnAudioIONewBlockFiles(blockFileLog); - } - } // end of record buffering + // end of record buffering + }, + // handler + [this] ( AudacityException *pException ) { + if ( pException ) { + // So that we don't attempt to fill the recording buffer again + // before the main thread stops recording + SetRecordingException(); + return ; + } + else + // Don't want to intercept other exceptions (?) + throw; + }, + delayedHandler + ); } void AudioIO::SetListener(AudioIOListener* listener) diff --git a/src/AudioIO.h b/src/AudioIO.h index 4180bceef..e77cd3559 100644 --- a/src/AudioIO.h +++ b/src/AudioIO.h @@ -19,6 +19,7 @@ #include "MemoryX.h" #include +#include #ifdef USE_MIDI @@ -692,6 +693,14 @@ private: bool mSilentScrub; sampleCount mScrubDuration; #endif + + // A flag tested and set in one thread, cleared in another. Perhaps + // this guarantee of atomicity is more cautious than necessary. + wxAtomicInt mRecordingException {}; + void SetRecordingException() + { wxAtomicInc( mRecordingException ); } + void ClearRecordingException() + { if (mRecordingException) wxAtomicDec( mRecordingException ); } }; #endif