1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-07-27 06:07:59 +02:00

Gracefully handle disk exhaustion exceptions during recording

This commit is contained in:
Paul Licameli 2016-11-22 14:10:53 -05:00
parent f4e2fb5eac
commit eeb301e50d
2 changed files with 112 additions and 61 deletions

View File

@ -1650,6 +1650,8 @@ int AudioIO::StartStream(const ConstWaveTrackArray &playbackTracks,
double t0, double t1, double t0, double t1,
const AudioIOStartStreamOptions &options) const AudioIOStartStreamOptions &options)
{ {
auto cleanup = finally ( [this] { ClearRecordingException(); } );
if( IsBusy() ) if( IsBusy() )
return 0; return 0;
@ -2250,6 +2252,8 @@ void AudioIO::SetMeters()
void AudioIO::StopStream() void AudioIO::StopStream()
{ {
auto cleanup = finally ( [this] { ClearRecordingException(); } );
if( mPortStreamV19 == NULL if( mPortStreamV19 == NULL
#ifdef EXPERIMENTAL_MIDI_OUT #ifdef EXPERIMENTAL_MIDI_OUT
&& mMidiStream == NULL && mMidiStream == NULL
@ -3420,6 +3424,25 @@ void AudioIO::FillBuffers()
{ {
unsigned int i; 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) if (mPlaybackTracks.size() > 0)
{ {
// Though extremely unlikely, it is possible that some buffers // Though extremely unlikely, it is possible that some buffers
@ -3595,78 +3618,97 @@ void AudioIO::FillBuffers()
} }
} // end of playback buffering } // end of playback buffering
if (mCaptureTracks.size() > 0) // start record buffering if (!mRecordingException &&
{ mCaptureTracks.size() > 0)
auto commonlyAvail = GetCommonlyAvailCapture(); GuardedCall<void>( [&] {
// start record buffering
auto commonlyAvail = GetCommonlyAvailCapture();
// //
// Determine how much this will add to captured tracks // Determine how much this will add to captured tracks
// //
double deltat = commonlyAvail / mRate; double deltat = commonlyAvail / mRate;
if (mAudioThreadShouldCallFillBuffersOnce || if (mAudioThreadShouldCallFillBuffersOnce ||
deltat >= mMinCaptureSecsToCopy) 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++ )
{ {
auto avail = commonlyAvail; // Append captured samples to the end of the WaveTracks.
sampleFormat trackFormat = mCaptureTracks[i]->GetSampleFormat(); // The WaveTracks have their own buffering for efficiency.
AutoSaveFile blockFileLog;
auto numChannels = mCaptureTracks.size();
AutoSaveFile appendLog; for( i = 0; (int)i < numChannels; i++ )
if( mFactor == 1.0 )
{ {
SampleBuffer temp(avail, trackFormat); auto avail = commonlyAvail;
const auto got = 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); mCaptureBuffers[i]->Get(temp.ptr(), trackFormat, avail);
// wxASSERT(got == avail); // wxASSERT(got == avail);
// but we can't assert in this thread // but we can't assert in this thread
wxUnusedVar(got); wxUnusedVar(got);
mCaptureTracks[i]-> Append(temp.ptr(), trackFormat, avail, 1, // see comment in second handler about guarantee
&appendLog); mCaptureTracks[i]-> Append(temp.ptr(), trackFormat, avail, 1,
} &appendLog);
else }
{ else
size_t size = lrint(avail * mFactor); {
SampleBuffer temp1(avail, floatSample); size_t size = lrint(avail * mFactor);
SampleBuffer temp2(size, floatSample); SampleBuffer temp1(avail, floatSample);
const auto got = SampleBuffer temp2(size, floatSample);
const auto got =
mCaptureBuffers[i]->Get(temp1.ptr(), floatSample, avail); mCaptureBuffers[i]->Get(temp1.ptr(), floatSample, avail);
// wxASSERT(got == avail); // wxASSERT(got == avail);
// but we can't assert in this thread // but we can't assert in this thread
wxUnusedVar(got); wxUnusedVar(got);
/* we are re-sampling on the fly. The last resampling call /* we are re-sampling on the fly. The last resampling call
* must flush any samples left in the rate conversion buffer * must flush any samples left in the rate conversion buffer
* so that they get recorded * so that they get recorded
*/ */
const auto results = const auto results =
mResample[i]->Process(mFactor, (float *)temp1.ptr(), avail, mResample[i]->Process(mFactor, (float *)temp1.ptr(), avail,
!IsStreamActive(), (float *)temp2.ptr(), size); !IsStreamActive(), (float *)temp2.ptr(), size);
size = results.second; size = results.second;
mCaptureTracks[i]-> Append(temp2.ptr(), floatSample, size, 1, // see comment in second handler about guarantee
&appendLog); 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()) if (mListener && !blockFileLog.IsEmpty())
{ mListener->OnAudioIONewBlockFiles(blockFileLog);
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"));
}
} }
// end of record buffering
if (mListener && !blockFileLog.IsEmpty()) },
mListener->OnAudioIONewBlockFiles(blockFileLog); // handler
} [this] ( AudacityException *pException ) {
} // end of record buffering 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) void AudioIO::SetListener(AudioIOListener* listener)

View File

@ -19,6 +19,7 @@
#include "MemoryX.h" #include "MemoryX.h"
#include <vector> #include <vector>
#include <wx/atomic.h>
#ifdef USE_MIDI #ifdef USE_MIDI
@ -692,6 +693,14 @@ private:
bool mSilentScrub; bool mSilentScrub;
sampleCount mScrubDuration; sampleCount mScrubDuration;
#endif #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 #endif