/********************************************************************** Audacity: A Digital Audio Editor ProjectAudioManager.cpp Paul Licameli split from ProjectManager.cpp **********************************************************************/ #include "ProjectAudioManager.h" #include #include #include "AudioIO.h" #include "CommonCommandFlags.h" #include "LabelTrack.h" #include "Menus.h" #include "Project.h" #include "ProjectAudioIO.h" #include "ProjectFileIO.h" #include "ProjectHistory.h" #include "ProjectSettings.h" #include "ProjectStatus.h" #include "TimeTrack.h" #include "TrackPanelAx.h" #include "UndoManager.h" #include "ViewInfo.h" #include "WaveTrack.h" #include "toolbars/ToolManager.h" #include "prefs/TracksPrefs.h" #include "tracks/ui/Scrubbing.h" #include "tracks/ui/TrackView.h" #include "widgets/ErrorDialog.h" #include "widgets/MeterPanelBase.h" #include "widgets/Warning.h" #include "widgets/AudacityMessageBox.h" static AudacityProject::AttachedObjects::RegisteredFactory sProjectAudioManagerKey { []( AudacityProject &project ) { return std::make_shared< ProjectAudioManager >( project ); } }; ProjectAudioManager &ProjectAudioManager::Get( AudacityProject &project ) { return project.AttachedObjects::Get< ProjectAudioManager >( sProjectAudioManagerKey ); } const ProjectAudioManager &ProjectAudioManager::Get( const AudacityProject &project ) { return Get( const_cast< AudacityProject & >( project ) ); } ProjectAudioManager::ProjectAudioManager( AudacityProject &project ) : mProject{ project } { static ProjectStatus::RegisteredStatusWidthFunction registerStatusWidthFunction{ StatusWidthFunction }; project.Bind( EVT_CHECKPOINT_FAILURE, &ProjectAudioManager::OnCheckpointFailure, this ); } ProjectAudioManager::~ProjectAudioManager() = default; static TranslatableString FormatRate( int rate ) { if (rate > 0) { return XO("Actual Rate: %d").Format( rate ); } else // clear the status field return {}; } auto ProjectAudioManager::StatusWidthFunction( const AudacityProject &project, StatusBarField field ) -> ProjectStatus::StatusWidthResult { if ( field == rateStatusBarField ) { auto &audioManager = ProjectAudioManager::Get( project ); int rate = audioManager.mDisplayedRate; return { { { FormatRate( rate ) } }, 50 }; } return {}; } /*! @excsafety{Strong} -- For state of mCutPreviewTracks */ int ProjectAudioManager::PlayPlayRegion(const SelectedRegion &selectedRegion, const AudioIOStartStreamOptions &options, PlayMode mode, bool backwards, /* = false */ bool playWhiteSpace /* = false */) { auto &projectAudioManager = *this; bool canStop = projectAudioManager.CanStopAudioStream(); if ( !canStop ) return -1; bool useMidi = true; // Remove these lines to experiment with scrubbing/seeking of note tracks if (options.pScrubbingOptions) useMidi = 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); projectAudioManager.SetLooping( mode == PlayMode::loopedPlay ); projectAudioManager.SetCutting( mode == PlayMode::cutPreviewPlay ); bool success = false; auto gAudioIO = AudioIO::Get(); if (gAudioIO->IsBusy()) return -1; const bool cutpreview = mode == PlayMode::cutPreviewPlay; if (cutpreview && t0==t1) return -1; /* msmeyer: makes no sense */ AudacityProject *p = &mProject; auto &tracks = TrackList::Get( *p ); mLastPlayMode = mode; bool hasaudio; if (useMidi) hasaudio = ! tracks.Any().empty(); else hasaudio = ! tracks.Any().empty(); double latestEnd = (playWhiteSpace)? t1 : tracks.GetEndTime(); if (!hasaudio) return -1; // No need to continue without audio tracks #if defined(EXPERIMENTAL_SEEK_BEHIND_CURSOR) double init_seek = 0.0; #endif double loop_offset = 0.0; if (t1 == t0) { if (looped) { const auto &selectedRegion = ViewInfo::Get( *p ).selectedRegion; // play selection if there is one, otherwise // set start of play region to project start, // and loop the project from current play position. if ((t0 > selectedRegion.t0()) && (t0 < selectedRegion.t1())) { t0 = selectedRegion.t0(); t1 = selectedRegion.t1(); } else { // loop the entire project // Bug2347, loop playback from cursor position instead of project start loop_offset = t0 - tracks.GetStartTime(); t0 = tracks.GetStartTime(); t1 = tracks.GetEndTime(); } } else { // move t0 to valid range if (t0 < 0) { t0 = tracks.GetStartTime(); } else if (t0 > tracks.GetEndTime()) { t0 = tracks.GetEndTime(); } #if defined(EXPERIMENTAL_SEEK_BEHIND_CURSOR) else { init_seek = t0; //AC: init_seek is where playback will 'start' t0 = tracks.GetStartTime(); } #endif } t1 = tracks.GetEndTime(); } else { // maybe t1 < t0, with backwards scrubbing for instance if (backwards) std::swap(t0, t1); t0 = std::max(0.0, std::min(t0, latestEnd)); t1 = std::max(0.0, std::min(t1, latestEnd)); if (backwards) std::swap(t0, t1); } int token = -1; 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 = 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( GetAllPlaybackTracks(*mCutPreviewTracks, false, useMidi), tcp0, tcp1, myOptions); } else // Cannot create cut preview tracks, clean up and exit return -1; } else { token = gAudioIO->StartStream( GetAllPlaybackTracks( tracks, false, useMidi ), t0, t1, options); } if (token != 0) { success = true; ProjectAudioIO::Get(*p).SetAudioIOToken(token); if (loop_offset != 0.0) { // Bug 2347 gAudioIO->SeekStream(loop_offset); } #if defined(EXPERIMENTAL_SEEK_BEHIND_CURSOR ) //AC: If init_seek was set, now's the time to make it happen. gAudioIO->SeekStream(init_seek); #endif } else { // Bug1627 (part of it): // infinite error spew when trying to start scrub: // Problem was that the error dialog yields to events, // causing recursion to this function in the scrub timer // handler! Easy fix, just delay the user alert instead. auto &window = GetProjectFrame( mProject ); window.CallAfter( [&]{ // Show error message if stream could not be opened ShowExceptionDialog(&window, XO("Error"), XO("Error opening sound device.\nTry changing the audio host, playback device and the project sample rate."), wxT("Error_opening_sound_device")); }); } } if (!success) return -1; return token; } void ProjectAudioManager::PlayCurrentRegion(bool looped /* = false */, bool cutpreview /* = false */) { auto &projectAudioManager = *this; bool canStop = projectAudioManager.CanStopAudioStream(); if ( !canStop ) return; AudacityProject *p = &mProject; { const auto &playRegion = ViewInfo::Get( *p ).playRegion; auto options = DefaultPlayOptions( *p ); options.playLooped = looped; if (cutpreview) options.envelope = nullptr; auto mode = cutpreview ? PlayMode::cutPreviewPlay : options.playLooped ? PlayMode::loopedPlay : PlayMode::normalPlay; PlayPlayRegion(SelectedRegion(playRegion.GetStart(), playRegion.GetEnd()), options, mode); } } void ProjectAudioManager::Stop(bool stopStream /* = true*/) { AudacityProject *project = &mProject; auto &projectAudioManager = *this; bool canStop = projectAudioManager.CanStopAudioStream(); if ( !canStop ) return; if(project) { // Let scrubbing code do some appearance change auto &scrubber = Scrubber::Get( *project ); scrubber.StopScrubbing(); } auto gAudioIO = AudioIO::Get(); auto cleanup = finally( [&]{ projectAudioManager.SetStopping( false ); } ); if (stopStream && gAudioIO->IsBusy()) { // flag that we are stopping projectAudioManager.SetStopping( true ); // Allow UI to update for that while( wxTheApp->ProcessIdle() ) ; } if(stopStream) gAudioIO->StopStream(); projectAudioManager.SetLooping( false ); projectAudioManager.SetCutting( false ); #ifdef EXPERIMENTAL_AUTOMATED_INPUT_LEVEL_ADJUSTMENT gAudioIO->AILADisable(); #endif projectAudioManager.SetPaused( false ); //Make sure you tell gAudioIO to unpause gAudioIO->SetPaused( false ); ClearCutPreviewTracks(); // So that we continue monitoring after playing or recording. // also clean the MeterQueues if( project ) { auto &projectAudioIO = ProjectAudioIO::Get( *project ); auto meter = projectAudioIO.GetPlaybackMeter(); if( meter ) { meter->Clear(); } meter = projectAudioIO.GetCaptureMeter(); if( meter ) { meter->Clear(); } } const auto toolbar = ToolManager::Get( *project ).GetToolBar(ScrubbingBarID); if (toolbar) toolbar->EnableDisableButtons(); } void ProjectAudioManager::Pause() { auto &projectAudioManager = *this; bool canStop = projectAudioManager.CanStopAudioStream(); if ( !canStop ) { auto gAudioIO = AudioIO::Get(); gAudioIO->SetPaused(!gAudioIO->IsPaused()); } else { OnPause(); } } WaveTrackArray ProjectAudioManager::ChooseExistingRecordingTracks( AudacityProject &proj, bool selectedOnly, double targetRate) { auto p = &proj; size_t recordingChannels = std::max(0, AudioIORecordChannels.Read()); bool strictRules = (recordingChannels <= 2); // Iterate over all wave tracks, or over selected wave tracks only. // If target rate was specified, ignore all tracks with other rates. // // In the usual cases of one or two recording channels, seek a first-fit // unbroken sub-sequence for which the total number of channels matches the // required number exactly. Never drop inputs or fill only some channels // of a track. // // In case of more than two recording channels, choose tracks only among the // selected. Simply take the earliest wave tracks, until the number of // channels is enough. If there are fewer channels than inputs, but at least // one channel, then some of the input channels will be dropped. // // Resulting tracks may be non-consecutive within the list of all tracks // (there may be non-wave tracks between, or non-selected tracks when // considering selected tracks only.) if (!strictRules && !selectedOnly) return {}; auto &trackList = TrackList::Get( *p ); std::vector channelCounts; WaveTrackArray candidates; const auto range = trackList.Leaders(); for ( auto candidate : selectedOnly ? range + &Track::IsSelected : range ) { if (targetRate != RATE_NOT_SELECTED && candidate->GetRate() != targetRate) continue; // count channels in this track const auto channels = TrackList::Channels( candidate ); unsigned nChannels = channels.size(); if (strictRules && nChannels > recordingChannels) { // The recording would under-fill this track's channels // Can't use any partial accumulated results // either. Keep looking. candidates.clear(); channelCounts.clear(); continue; } else { // Might use this but may have to discard some of the accumulated while(strictRules && nChannels + candidates.size() > recordingChannels) { auto nOldChannels = channelCounts[0]; wxASSERT(nOldChannels > 0); channelCounts.erase(channelCounts.begin()); candidates.erase(candidates.begin(), candidates.begin() + nOldChannels); } channelCounts.push_back(nChannels); for ( auto channel : channels ) { candidates.push_back(channel->SharedPointer()); if(candidates.size() == recordingChannels) // Done! return candidates; } } } if (!strictRules && !candidates.empty()) // good enough return candidates; // If the loop didn't exit early, we could not find enough channels return {}; } /*! @excsafety{Strong} -- For state of current project's tracks */ void ProjectAudioManager::OnRecord(bool altAppearance) { bool bPreferNewTrack; gPrefs->Read("/GUI/PreferNewTrackRecord", &bPreferNewTrack, false); const bool appendRecord = (altAppearance == bPreferNewTrack); // Code from CommandHandler start... AudacityProject *p = &mProject; if (p) { const auto &selectedRegion = ViewInfo::Get( *p ).selectedRegion; double t0 = selectedRegion.t0(); double t1 = selectedRegion.t1(); // When no time selection, recording duration is 'unlimited'. if (t1 == t0) t1 = DBL_MAX; auto options = DefaultPlayOptions(*p); WaveTrackArray existingTracks; // Checking the selected tracks: counting them and // making sure they all have the same rate const auto selectedTracks{ GetPropertiesOfSelected(*p) }; const int rateOfSelected{ selectedTracks.rateOfSelected }; const int numberOfSelected{ selectedTracks.numberOfSelected }; const bool allSameRate{ selectedTracks.allSameRate }; if (!allSameRate) { AudacityMessageBox(XO("The tracks selected " "for recording must all have the same sampling rate"), XO("Mismatched Sampling Rates"), wxICON_ERROR | wxCENTRE); return; } if (appendRecord) { const auto trackRange = TrackList::Get( *p ).Any< const WaveTrack >(); // Try to find wave tracks to record into. (If any are selected, // try to choose only from them; else if wave tracks exist, may record into any.) existingTracks = ChooseExistingRecordingTracks(*p, true, rateOfSelected); if (!existingTracks.empty()) { t0 = std::max(t0, (trackRange + &Track::IsSelected).max(&Track::GetEndTime)); } else { if (numberOfSelected > 0 && rateOfSelected != options.rate) { AudacityMessageBox(XO( "Too few tracks are selected for recording at this sample rate.\n" "(Audacity requires two channels at the same sample rate for\n" "each stereo track)"), XO("Too Few Compatible Tracks Selected"), wxICON_ERROR | wxCENTRE); return; } existingTracks = ChooseExistingRecordingTracks(*p, false, options.rate); t0 = std::max( t0, trackRange.max( &Track::GetEndTime ) ); // If suitable tracks still not found, will record into NEW ones, // but the choice of t0 does not depend on that. } // Whether we decided on NEW tracks or not: if (t1 <= selectedRegion.t0() && selectedRegion.t1() > selectedRegion.t0()) { t1 = selectedRegion.t1(); // record within the selection } else { t1 = DBL_MAX; // record for a long, long time } } TransportTracks transportTracks; if (UseDuplex()) { // Remove recording tracks from the list of tracks for duplex ("overdub") // playback. /* TODO: set up stereo tracks if that is how the user has set up * their preferences, and choose sample format based on prefs */ transportTracks = GetAllPlaybackTracks(TrackList::Get( *p ), false, true); for (const auto &wt : existingTracks) { auto end = transportTracks.playbackTracks.end(); auto it = std::find(transportTracks.playbackTracks.begin(), end, wt); if (it != end) transportTracks.playbackTracks.erase(it); } } transportTracks.captureTracks = existingTracks; if (rateOfSelected != RATE_NOT_SELECTED) options.rate = rateOfSelected; DoRecord(*p, transportTracks, t0, t1, altAppearance, options); } } bool ProjectAudioManager::UseDuplex() { bool duplex; gPrefs->Read(wxT("/AudioIO/Duplex"), &duplex, #ifdef EXPERIMENTAL_DA false #else true #endif ); return duplex; } bool ProjectAudioManager::DoRecord(AudacityProject &project, const TransportTracks &tracks, double t0, double t1, bool altAppearance, const AudioIOStartStreamOptions &options) { auto &projectAudioManager = *this; CommandFlag flags = AlwaysEnabledFlag; // 0 means recalc flags. // NB: The call may have the side effect of changing flags. bool allowed = MenuManager::Get(project).TryToMakeActionAllowed( flags, AudioIONotBusyFlag() | CanStopAudioStreamFlag()); if (!allowed) return false; // ...end of code from CommandHandler. auto gAudioIO = AudioIO::Get(); if (gAudioIO->IsBusy()) return false; projectAudioManager.SetAppending( !altAppearance ); bool success = false; auto transportTracks = tracks; // Will replace any given capture tracks with temporaries transportTracks.captureTracks.clear(); const auto p = &project; bool appendRecord = !tracks.captureTracks.empty(); { if (appendRecord) { // Append recording: // Pad selected/all wave tracks to make them all the same length for (const auto &wt : tracks.captureTracks) { auto endTime = wt->GetEndTime(); // If the track was chosen for recording and playback both, // remember the original in preroll tracks, before making the // pending replacement. bool prerollTrack = make_iterator_range(transportTracks.playbackTracks).contains(wt); if (prerollTrack) transportTracks.prerollTracks.push_back(wt); // A function that copies all the non-sample data between // wave tracks; in case the track recorded to changes scale // type (for instance), during the recording. auto updater = [](Track &d, const Track &s){ auto &dst = static_cast(d); auto &src = static_cast(s); dst.Reinit(src); }; // Get a copy of the track to be appended, to be pushed into // undo history only later. auto pending = std::static_pointer_cast( TrackList::Get( *p ).RegisterPendingChangedTrack( updater, wt.get() ) ); // End of current track is before or at recording start time. // Less than or equal, not just less than, to ensure a clip boundary. // when append recording. if (endTime <= t0) { // Pad the recording track with silence, up to the // maximum time. auto newTrack = pending->EmptyCopy(); newTrack->InsertSilence(0.0, t0 - endTime); newTrack->Flush(); pending->Clear(endTime, t0); pending->Paste(endTime, newTrack.get()); } transportTracks.captureTracks.push_back(pending); } TrackList::Get( *p ).UpdatePendingTracks(); } if( transportTracks.captureTracks.empty() ) { // recording to NEW track(s). bool recordingNameCustom, useTrackNumber, useDateStamp, useTimeStamp; wxString defaultTrackName, defaultRecordingTrackName; // Count the tracks. auto &trackList = TrackList::Get( *p ); auto numTracks = trackList.Leaders< const WaveTrack >().size(); auto recordingChannels = std::max(1, AudioIORecordChannels.Read()); gPrefs->Read(wxT("/GUI/TrackNames/RecordingNameCustom"), &recordingNameCustom, false); gPrefs->Read(wxT("/GUI/TrackNames/TrackNumber"), &useTrackNumber, false); gPrefs->Read(wxT("/GUI/TrackNames/DateStamp"), &useDateStamp, false); gPrefs->Read(wxT("/GUI/TrackNames/TimeStamp"), &useTimeStamp, false); defaultTrackName = TracksPrefs::GetDefaultAudioTrackNamePreference(); gPrefs->Read(wxT("/GUI/TrackNames/RecodingTrackName"), &defaultRecordingTrackName, defaultTrackName); wxString baseTrackName = recordingNameCustom? defaultRecordingTrackName : defaultTrackName; Track *first {}; for (int c = 0; c < recordingChannels; c++) { auto newTrack = WaveTrackFactory::Get( *p ).NewWaveTrack(); if (!first) first = newTrack.get(); // Quantize bounds to the rate of the new track. if (c == 0) { if (t0 < DBL_MAX) t0 = newTrack->LongSamplesToTime(newTrack->TimeToLongSamples(t0)); if (t1 < DBL_MAX) t1 = newTrack->LongSamplesToTime(newTrack->TimeToLongSamples(t1)); } newTrack->SetOffset(t0); wxString nameSuffix = wxString(wxT("")); if (useTrackNumber) { nameSuffix += wxString::Format(wxT("%d"), 1 + (int) numTracks + c); } if (useDateStamp) { if (!nameSuffix.empty()) { nameSuffix += wxT("_"); } nameSuffix += wxDateTime::Now().FormatISODate(); } if (useTimeStamp) { if (!nameSuffix.empty()) { nameSuffix += wxT("_"); } nameSuffix += wxDateTime::Now().FormatISOTime(); } // ISO standard would be nice, but ":" is unsafe for file name. nameSuffix.Replace(wxT(":"), wxT("-")); if (baseTrackName.empty()) { newTrack->SetName(nameSuffix); } else if (nameSuffix.empty()) { newTrack->SetName(baseTrackName); } else { newTrack->SetName(baseTrackName + wxT("_") + nameSuffix); } TrackList::Get( *p ).RegisterPendingNewTrack( newTrack ); if ((recordingChannels > 2) && !(ProjectSettings::Get(*p).GetTracksFitVerticallyZoomed())) { TrackView::Get( *newTrack ).SetMinimized(true); } transportTracks.captureTracks.push_back(newTrack); } TrackList::Get( *p ).GroupChannels(*first, recordingChannels); // Bug 1548. First of new tracks needs the focus. TrackFocus::Get(*p).Set(first); if (TrackList::Get(*p).back()) TrackList::Get(*p).back()->EnsureVisible(); } //Automated Input Level Adjustment Initialization #ifdef EXPERIMENTAL_AUTOMATED_INPUT_LEVEL_ADJUSTMENT gAudioIO->AILAInitialize(); #endif int token = gAudioIO->StartStream(transportTracks, t0, t1, options); success = (token != 0); if (success) { ProjectAudioIO::Get( *p ).SetAudioIOToken(token); } else { CancelRecording(); // Show error message if stream could not be opened auto msg = XO("Error opening recording device.\nError code: %s") .Format( gAudioIO->LastPaErrorString() ); ShowExceptionDialog(&GetProjectFrame( mProject ), XO("Error"), msg, wxT("Error_opening_sound_device")); } } return success; } void ProjectAudioManager::OnPause() { auto &projectAudioManager = *this; bool canStop = projectAudioManager.CanStopAudioStream(); if ( !canStop ) { return; } bool paused = !projectAudioManager.Paused(); projectAudioManager.SetPaused( paused ); auto gAudioIO = AudioIO::Get(); #ifdef EXPERIMENTAL_SCRUBBING_SUPPORT auto project = &mProject; auto &scrubber = Scrubber::Get( *project ); // Bug 1494 - Pausing a seek or scrub should just STOP as // it is confusing to be in a paused scrub state. bool bStopInstead = paused && gAudioIO->IsScrubbing() && !scrubber.IsSpeedPlaying() && !scrubber.IsKeyboardScrubbing(); if (bStopInstead) { Stop(); return; } if (gAudioIO->IsScrubbing()) scrubber.Pause(paused); else #endif { gAudioIO->SetPaused(paused); } } /*! @excsafety{Strong} -- For state of mCutPreviewTracks*/ void ProjectAudioManager::SetupCutPreviewTracks(double WXUNUSED(playStart), double cutStart, double cutEnd, double WXUNUSED(playEnd)) { ClearCutPreviewTracks(); AudacityProject *p = &mProject; { auto trackRange = TrackList::Get( *p ).Selected< const PlayableTrack >(); if( !trackRange.empty() ) { auto cutPreviewTracks = TrackList::Create( nullptr ); for (const auto track1 : trackRange) { // Duplicate and change tracks // Clear has a very small chance of throwing auto newTrack = track1->Duplicate(); newTrack->Clear(cutStart, cutEnd); cutPreviewTracks->Add( newTrack ); } // use No-throw-guarantee: mCutPreviewTracks = cutPreviewTracks; } } } void ProjectAudioManager::ClearCutPreviewTracks() { if (mCutPreviewTracks) mCutPreviewTracks->Clear(); mCutPreviewTracks.reset(); } void ProjectAudioManager::CancelRecording() { const auto project = &mProject; TrackList::Get( *project ).ClearPendingTracks(); } void ProjectAudioManager::OnAudioIORate(int rate) { auto &project = mProject; mDisplayedRate = rate; auto display = FormatRate( rate ); ProjectStatus::Get( project ).Set( display, rateStatusBarField ); } void ProjectAudioManager::OnAudioIOStartRecording() { // Auto-save was done here before, but it is unnecessary, provided there // are sufficient autosaves when pushing or modifying undo states. } // This is called after recording has stopped and all tracks have flushed. void ProjectAudioManager::OnAudioIOStopRecording() { auto &project = mProject; auto &projectAudioIO = ProjectAudioIO::Get( project ); auto &projectFileIO = ProjectFileIO::Get( project ); auto &window = GetProjectFrame( project ); // Only push state if we were capturing and not monitoring if (projectAudioIO.GetAudioIOToken() > 0) { auto &history = ProjectHistory::Get( project ); if (IsTimerRecordCancelled()) { // discard recording history.RollbackState(); // Reset timer record ResetTimerRecordCancelled(); } else { // Add to history // We want this to have No-fail-guarantee if we get here from exception // handling of recording, and that means we rely on the last autosave // successfully committed to the database, not risking a failure history.PushState(XO("Recorded Audio"), XO("Record"), UndoPush::NOAUTOSAVE); // Now, we may add a label track to give information about // dropouts. We allow failure of this. auto &tracks = TrackList::Get( project ); auto gAudioIO = AudioIO::Get(); auto &intervals = gAudioIO->LostCaptureIntervals(); if (intervals.size()) { // Make a track with labels for recording errors auto uTrack = std::make_shared(); auto pTrack = uTrack.get(); tracks.Add( uTrack ); /* i18n-hint: A name given to a track, appearing as its menu button. The translation should be short or else it will not display well. At most, about 11 Latin characters. Dropout is a loss of a short sequence of audio sample data from the recording */ pTrack->SetName(_("Dropouts")); long counter = 1; for (auto &interval : intervals) pTrack->AddLabel( SelectedRegion{ interval.first, interval.first + interval.second }, wxString::Format(wxT("%ld"), counter++)); history.ModifyState( true ); // this might fail and throw // CallAfter so that we avoid any problems of yielding // to the event loop while still inside the timer callback, // entering StopStream() recursively wxTheApp->CallAfter( [&] { ShowWarningDialog(&window, wxT("DropoutDetected"), XO("\ Recorded audio was lost at the labeled locations. Possible causes:\n\ \n\ Other applications are competing with Audacity for processor time\n\ \n\ You are saving directly to a slow external storage device\n\ " ), false, XXO("Turn off dropout detection")); }); } } } } void ProjectAudioManager::OnAudioIONewBlocks(const WaveTrackArray *tracks) { auto &project = mProject; auto &projectFileIO = ProjectFileIO::Get( project ); projectFileIO.AutoSave(true); } void ProjectAudioManager::OnCommitRecording() { const auto project = &mProject; TrackList::Get( *project ).ApplyPendingTracks(); } void ProjectAudioManager::OnSoundActivationThreshold() { auto &project = mProject; auto gAudioIO = AudioIO::Get(); if ( gAudioIO && &project == gAudioIO->GetOwningProject() ) { wxTheApp->CallAfter( [this]{ Pause(); } ); } } void ProjectAudioManager::OnCheckpointFailure(wxCommandEvent &evt) { evt.Skip(); Stop(); } bool ProjectAudioManager::Playing() const { auto gAudioIO = AudioIO::Get(); return gAudioIO->IsBusy() && CanStopAudioStream() && // ... and not merely monitoring !gAudioIO->IsMonitoring() && // ... and not punch-and-roll recording gAudioIO->GetNumCaptureChannels() == 0; } bool ProjectAudioManager::Recording() const { auto gAudioIO = AudioIO::Get(); return gAudioIO->IsBusy() && CanStopAudioStream() && gAudioIO->GetNumCaptureChannels() > 0; } bool ProjectAudioManager::CanStopAudioStream() const { auto gAudioIO = AudioIO::Get(); return (!gAudioIO->IsStreamActive() || gAudioIO->IsMonitoring() || gAudioIO->GetOwningProject() == &mProject ); } const ReservedCommandFlag& CanStopAudioStreamFlag(){ static ReservedCommandFlag flag{ [](const AudacityProject &project){ auto &projectAudioManager = ProjectAudioManager::Get( project ); bool canStop = projectAudioManager.CanStopAudioStream(); return canStop; } }; return flag; } AudioIOStartStreamOptions DefaultPlayOptions( AudacityProject &project ) { auto &projectAudioIO = ProjectAudioIO::Get( project ); AudioIOStartStreamOptions options { &project, ProjectSettings::Get( project ).GetRate() }; options.captureMeter = projectAudioIO.GetCaptureMeter(); options.playbackMeter = projectAudioIO.GetPlaybackMeter(); auto timeTrack = *TrackList::Get( project ).Any().begin(); options.envelope = timeTrack ? timeTrack->GetEnvelope() : nullptr; options.listener = ProjectAudioManager::Get( project ).shared_from_this(); return options; } AudioIOStartStreamOptions DefaultSpeedPlayOptions( AudacityProject &project ) { auto &projectAudioIO = ProjectAudioIO::Get( project ); auto gAudioIO = AudioIO::Get(); auto PlayAtSpeedRate = gAudioIO->GetBestRate( false, //not capturing true, //is playing ProjectSettings::Get( project ).GetRate() //suggested rate ); AudioIOStartStreamOptions options{ &project, PlayAtSpeedRate }; options.captureMeter = projectAudioIO.GetCaptureMeter(); options.playbackMeter = projectAudioIO.GetPlaybackMeter(); auto timeTrack = *TrackList::Get( project ).Any().begin(); options.envelope = timeTrack ? timeTrack->GetEnvelope() : nullptr; options.listener = ProjectAudioManager::Get( project ).shared_from_this(); return options; } #ifdef EXPERIMENTAL_MIDI_OUT #include "NoteTrack.h" #endif TransportTracks ProjectAudioManager::GetAllPlaybackTracks( TrackList &trackList, bool selectedOnly, bool useMidi) { TransportTracks result; { auto range = trackList.Any< WaveTrack >() + (selectedOnly ? &Track::IsSelected : &Track::Any ); for (auto pTrack: range) result.playbackTracks.push_back( pTrack->SharedPointer< WaveTrack >() ); } #ifdef EXPERIMENTAL_MIDI_OUT if (useMidi) { auto range = trackList.Any< const NoteTrack >() + (selectedOnly ? &Track::IsSelected : &Track::Any ); for (auto pTrack: range) result.midiTracks.push_back( pTrack->SharedPointer< const NoteTrack >() ); } #else WXUNUSED(useMidi); #endif return result; } // Stop playing or recording, if paused. void ProjectAudioManager::StopIfPaused() { if( AudioIOBase::Get()->IsPaused() ) Stop(); } bool ProjectAudioManager::DoPlayStopSelect( bool click, bool shift ) { auto &project = mProject; auto &scrubber = Scrubber::Get( project ); auto token = ProjectAudioIO::Get( project ).GetAudioIOToken(); auto &viewInfo = ViewInfo::Get( project ); auto &selection = viewInfo.selectedRegion; auto gAudioIO = AudioIO::Get(); //If busy, stop playing, make sure everything is unpaused. if (scrubber.HasMark() || gAudioIO->IsStreamActive(token)) { // change the selection auto time = gAudioIO->GetStreamTime(); // Test WasSpeedPlaying(), not IsSpeedPlaying() // as we could be stopped now. Similarly WasKeyboardScrubbing(). if (click && (scrubber.WasSpeedPlaying() || scrubber.WasKeyboardScrubbing())) { ;// don't change the selection. } else if (shift && click) { // Change the region selection, as if by shift-click at the play head auto t0 = selection.t0(), t1 = selection.t1(); if (time < t0) // Grow selection t0 = time; else if (time > t1) // Grow selection t1 = time; else { // Shrink selection, changing the nearer boundary if (fabs(t0 - time) < fabs(t1 - time)) t0 = time; else t1 = time; } selection.setTimes(t0, t1); } else if (click){ // avoid a point at negative time. time = wxMax( time, 0 ); // Set a point selection, as if by a click at the play head selection.setTimes(time, time); } else // How stop and set cursor always worked // -- change t0, collapsing to point only if t1 was greater selection.setT0(time, false); ProjectHistory::Get( project ).ModifyState(false); // without bWantsAutoSave return true; } return false; } // The code for "OnPlayStopSelect" is simply the code of "OnPlayStop" and // "OnStopSelect" merged. void ProjectAudioManager::DoPlayStopSelect() { auto gAudioIO = AudioIO::Get(); if (DoPlayStopSelect(false, false)) Stop(); else if (!gAudioIO->IsBusy()) { //Otherwise, start playing (assuming audio I/O isn't busy) // Will automatically set mLastPlayMode PlayCurrentRegion(false); } } #include "CommonCommandFlags.h" static RegisteredMenuItemEnabler stopIfPaused{{ []{ return PausedFlag(); }, []{ return AudioIONotBusyFlag(); }, []( const AudacityProject &project ){ return MenuManager::Get( project ).mStopIfWasPaused; }, []( AudacityProject &project, CommandFlag ){ if ( MenuManager::Get( project ).mStopIfWasPaused ) ProjectAudioManager::Get( project ).StopIfPaused(); } }}; // GetSelectedProperties collects information about // currently selected audio tracks PropertiesOfSelected GetPropertiesOfSelected(const AudacityProject &proj) { double rateOfSelection{ RATE_NOT_SELECTED }; PropertiesOfSelected result; result.allSameRate = true; const auto selectedTracks{ TrackList::Get(proj).Selected< const WaveTrack >() }; for (const auto & track : selectedTracks) { if (rateOfSelection != RATE_NOT_SELECTED && track->GetRate() != rateOfSelection) result.allSameRate = false; else if (rateOfSelection == RATE_NOT_SELECTED) rateOfSelection = track->GetRate(); } result.numberOfSelected = selectedTracks.size(); result.rateOfSelected = rateOfSelection; return result; }