From 51051ee933f8f4c80075d658955748e4455289ae Mon Sep 17 00:00:00 2001 From: Paul Licameli Date: Mon, 10 Jun 2019 15:42:38 -0400 Subject: [PATCH] Separate AudioIOBase from AudioIO --- src/AudacityApp.cpp | 4 +- src/AudioIO.cpp | 1362 +------------------------------------ src/AudioIO.h | 438 +----------- src/AudioIOBase.cpp | 1351 ++++++++++++++++++++++++++++++++++++ src/AudioIOBase.h | 468 ++++++++++++- src/tracks/ui/Scrubbing.h | 31 +- 6 files changed, 1861 insertions(+), 1793 deletions(-) diff --git a/src/AudacityApp.cpp b/src/AudacityApp.cpp index 2ee231faa..191d44ce8 100644 --- a/src/AudacityApp.cpp +++ b/src/AudacityApp.cpp @@ -1478,7 +1478,7 @@ bool AudacityApp::OnInit() // More initialization InitDitherers(); - InitAudioIO(); + AudioIO::Init(); #ifdef __WXMAC__ @@ -2041,7 +2041,7 @@ int AudacityApp::OnExit() DeinitFFT(); - DeinitAudioIO(); + AudioIO::Deinit(); // Terminate the PluginManager (must be done before deleting the locale) PluginManager::Get().Terminate(); diff --git a/src/AudioIO.cpp b/src/AudioIO.cpp index 98d18735a..1197871c4 100644 --- a/src/AudioIO.cpp +++ b/src/AudioIO.cpp @@ -447,8 +447,6 @@ time warp info and AudioIOListener and whether the playback is looped. #include #include #include -#include -#include #include "MissingAliasFileDialog.h" #include "Mix.h" @@ -458,7 +456,6 @@ time warp info and AudioIOListener and whether the playback is looped. #include "Prefs.h" #include "Project.h" #include "ProjectWindow.h" -#include "Envelope.h" #include "WaveTrack.h" #include "AutoRecovery.h" @@ -483,7 +480,7 @@ time warp info and AudioIOListener and whether the playback is looped. #endif #define ROUND(x) (int) ((x)+0.5) //#include - #include "../lib-src/portmidi/pm_common/portmidi.h" +// #include "../lib-src/portmidi/pm_common/portmidi.h" #include "../lib-src/portaudio-v19/src/common/pa_util.h" #include "NoteTrack.h" #endif @@ -496,8 +493,10 @@ time warp info and AudioIOListener and whether the playback is looped. using std::max; using std::min; -std::unique_ptr ugAudioIO; -AudioIO *gAudioIO{}; +AudioIO *AudioIO::Get() +{ + return static_cast< AudioIO* >( AudioIOBase::Get() ); +} wxDEFINE_EVENT(EVT_AUDIOIO_PLAYBACK, wxCommandEvent); wxDEFINE_EVENT(EVT_AUDIOIO_CAPTURE, wxCommandEvent); @@ -505,12 +504,6 @@ wxDEFINE_EVENT(EVT_AUDIOIO_MONITOR, wxCommandEvent); // static int AudioIoCallback::mNextStreamToken = 0; -int AudioIoCallback::mCachedPlaybackIndex = -1; -std::vector AudioIoCallback::mCachedPlaybackRates; -int AudioIoCallback::mCachedCaptureIndex = -1; -std::vector AudioIoCallback::mCachedCaptureRates; -std::vector AudioIoCallback::mCachedSampleRates; -double AudioIoCallback::mCachedBestRateIn = 0.0; double AudioIoCallback::mCachedBestRateOut; bool AudioIoCallback::mCachedBestRatePlaying; bool AudioIoCallback::mCachedBestRateCapturing; @@ -822,45 +815,6 @@ static double SystemTime(bool usingAlsa) } #endif -const int AudioIO::StandardRates[] = { - 8000, - 11025, - 16000, - 22050, - 32000, - 44100, - 48000, - 88200, - 96000, - 176400, - 192000, - 352800, - 384000 -}; -const int AudioIO::NumStandardRates = sizeof(AudioIO::StandardRates) / - sizeof(AudioIO::StandardRates[0]); -const int AudioIO::RatesToTry[] = { - 8000, - 9600, - 11025, - 12000, - 15000, - 16000, - 22050, - 24000, - 32000, - 44100, - 48000, - 88200, - 96000, - 176400, - 192000, - 352800, - 384000 -}; -const int AudioIO::NumRatesToTry = sizeof(AudioIO::RatesToTry) / - sizeof(AudioIO::RatesToTry[0]); - int audacityAudioCallback(const void *inputBuffer, void *outputBuffer, unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo *timeInfo, @@ -937,20 +891,19 @@ class MidiThread final : public AudioThread { // ////////////////////////////////////////////////////////////////////// -void InitAudioIO() +void AudioIO::Init() { ugAudioIO.reset(safenew AudioIO()); - gAudioIO = ugAudioIO.get(); - gAudioIO->mThread->Run(); + Get()->mThread->Run(); #ifdef EXPERIMENTAL_MIDI_OUT #ifdef USE_MIDI_THREAD - gAudioIO->mMidiThread->Run(); + Get()->mMidiThread->Run(); #endif #endif // Make sure device prefs are initialized if (gPrefs->Read(wxT("AudioIO/RecordingDevice"), wxT("")).empty()) { - int i = AudioIO::getRecordDevIndex(); + int i = getRecordDevIndex(); const PaDeviceInfo *info = Pa_GetDeviceInfo(i); if (info) { gPrefs->Write(wxT("/AudioIO/RecordingDevice"), DeviceName(info)); @@ -959,7 +912,7 @@ void InitAudioIO() } if (gPrefs->Read(wxT("AudioIO/PlaybackDevice"), wxT("")).empty()) { - int i = AudioIO::getPlayDevIndex(); + int i = getPlayDevIndex(); const PaDeviceInfo *info = Pa_GetDeviceInfo(i); if (info) { gPrefs->Write(wxT("/AudioIO/PlaybackDevice"), DeviceName(info)); @@ -970,29 +923,15 @@ void InitAudioIO() gPrefs->Flush(); } -void DeinitAudioIO() +void AudioIO::Deinit() { ugAudioIO.reset(); } -wxString DeviceName(const PaDeviceInfo* info) -{ - wxString infoName = wxSafeConvertMB2WX(info->name); - - return infoName; -} - -wxString HostName(const PaDeviceInfo* info) -{ - wxString hostapiName = wxSafeConvertMB2WX(Pa_GetHostApiInfo(info->hostApi)->name); - - return hostapiName; -} - bool AudioIO::ValidateDeviceNames(const wxString &play, const wxString &rec) { - const PaDeviceInfo *pInfo = Pa_GetDeviceInfo(AudioIO::getPlayDevIndex(play)); - const PaDeviceInfo *rInfo = Pa_GetDeviceInfo(AudioIO::getRecordDevIndex(rec)); + const PaDeviceInfo *pInfo = Pa_GetDeviceInfo(getPlayDevIndex(play)); + const PaDeviceInfo *rInfo = Pa_GetDeviceInfo(getRecordDevIndex(rec)); // Valid iff both defined and the same api. return pInfo != nullptr && rInfo != nullptr && pInfo->hostApi == rInfo->hostApi; @@ -1151,22 +1090,6 @@ AudioIO::~AudioIO() mThread->Delete(); mThread.reset(); - - gAudioIO = nullptr; -} - -AudioIO *AudioIO::Get() -{ - return gAudioIO; -} - -void AudioIO::SetMixer(int inputSource) -{ -#if defined(USE_PORTMIXER) - int oldRecordSource = Px_GetCurrentInputSource(mPortMixer); - if ( inputSource != oldRecordSource ) - Px_SetCurrentInputSource(mPortMixer, inputSource); -#endif } void AudioIO::SetMixer(int inputSource, float recordVolume, @@ -1181,7 +1104,7 @@ void AudioIO::SetMixer(int inputSource, float recordVolume, float oldRecordVolume = Px_GetInputVolume(mixer); float oldPlaybackVolume = Px_GetPCMOutputVolume(mixer); - SetMixer(inputSource); + AudioIoCallback::SetMixer(inputSource); if( oldRecordVolume != recordVolume ) Px_SetInputVolume(mixer, recordVolume); if( oldPlaybackVolume != playbackVolume ) @@ -1259,207 +1182,6 @@ wxArrayString AudioIO::GetInputSourceNames() #endif } -void AudioIO::HandleDeviceChange() -{ - // This should not happen, but it would screw things up if it did. - // Vaughan, 2010-10-08: But it *did* happen, due to a bug, and nobody - // caught it because this method just returned. Added wxASSERT(). - wxASSERT(!IsStreamActive()); - if (IsStreamActive()) - return; - - // get the selected record and playback devices - const int playDeviceNum = getPlayDevIndex(); - const int recDeviceNum = getRecordDevIndex(); - - // If no change needed, return - if (mCachedPlaybackIndex == playDeviceNum && - mCachedCaptureIndex == recDeviceNum) - return; - - // cache playback/capture rates - mCachedPlaybackRates = GetSupportedPlaybackRates(playDeviceNum); - mCachedCaptureRates = GetSupportedCaptureRates(recDeviceNum); - mCachedSampleRates = GetSupportedSampleRates(playDeviceNum, recDeviceNum); - mCachedPlaybackIndex = playDeviceNum; - mCachedCaptureIndex = recDeviceNum; - mCachedBestRateIn = 0.0; - -#if defined(USE_PORTMIXER) - - // if we have a PortMixer object, close it down - if (mPortMixer) { - #if __WXMAC__ - // on the Mac we must make sure that we restore the hardware playthrough - // state of the sound device to what it was before, because there isn't - // a UI for this (!) - if (Px_SupportsPlaythrough(mPortMixer) && mPreviousHWPlaythrough >= 0.0) - Px_SetPlaythrough(mPortMixer, mPreviousHWPlaythrough); - mPreviousHWPlaythrough = -1.0; - #endif - Px_CloseMixer(mPortMixer); - mPortMixer = NULL; - } - - // that might have given us no rates whatsoever, so we have to guess an - // answer to do the next bit - int numrates = mCachedSampleRates.size(); - int highestSampleRate; - if (numrates > 0) - { - highestSampleRate = mCachedSampleRates[numrates - 1]; - } - else - { // we don't actually have any rates that work for Rec and Play. Guess one - // to use for messing with the mixer, which doesn't actually do either - highestSampleRate = 44100; - // mCachedSampleRates is still empty, but it's not used again, so - // can ignore - } - mInputMixerWorks = false; - mEmulateMixerOutputVol = true; - mMixerOutputVol = 1.0; - - int error; - // This tries to open the device with the samplerate worked out above, which - // will be the highest available for play and record on the device, or - // 44.1kHz if the info cannot be fetched. - - PaStream *stream; - - PaStreamParameters playbackParameters; - - playbackParameters.device = playDeviceNum; - playbackParameters.sampleFormat = paFloat32; - playbackParameters.hostApiSpecificStreamInfo = NULL; - playbackParameters.channelCount = 1; - if (Pa_GetDeviceInfo(playDeviceNum)) - playbackParameters.suggestedLatency = - Pa_GetDeviceInfo(playDeviceNum)->defaultLowOutputLatency; - else - playbackParameters.suggestedLatency = DEFAULT_LATENCY_CORRECTION/1000.0; - - PaStreamParameters captureParameters; - - captureParameters.device = recDeviceNum; - captureParameters.sampleFormat = paFloat32;; - captureParameters.hostApiSpecificStreamInfo = NULL; - captureParameters.channelCount = 1; - if (Pa_GetDeviceInfo(recDeviceNum)) - captureParameters.suggestedLatency = - Pa_GetDeviceInfo(recDeviceNum)->defaultLowInputLatency; - else - captureParameters.suggestedLatency = DEFAULT_LATENCY_CORRECTION/1000.0; - - // try opening for record and playback - error = Pa_OpenStream(&stream, - &captureParameters, &playbackParameters, - highestSampleRate, paFramesPerBufferUnspecified, - paClipOff | paDitherOff, - audacityAudioCallback, NULL); - - if (!error) { - // Try portmixer for this stream - mPortMixer = Px_OpenMixer(stream, 0); - if (!mPortMixer) { - Pa_CloseStream(stream); - error = true; - } - } - - // if that failed, try just for record - if( error ) { - error = Pa_OpenStream(&stream, - &captureParameters, NULL, - highestSampleRate, paFramesPerBufferUnspecified, - paClipOff | paDitherOff, - audacityAudioCallback, NULL); - - if (!error) { - mPortMixer = Px_OpenMixer(stream, 0); - if (!mPortMixer) { - Pa_CloseStream(stream); - error = true; - } - } - } - - // finally, try just for playback - if ( error ) { - error = Pa_OpenStream(&stream, - NULL, &playbackParameters, - highestSampleRate, paFramesPerBufferUnspecified, - paClipOff | paDitherOff, - audacityAudioCallback, NULL); - - if (!error) { - mPortMixer = Px_OpenMixer(stream, 0); - if (!mPortMixer) { - Pa_CloseStream(stream); - error = true; - } - } - } - - // FIXME: TRAP_ERR errors in HandleDeviceChange not reported. - // if it's still not working, give up - if( error ) - return; - - // Set input source -#if USE_PORTMIXER - int sourceIndex; - if (gPrefs->Read(wxT("/AudioIO/RecordingSourceIndex"), &sourceIndex)) { - if (sourceIndex >= 0) { - //the current index of our source may be different because the stream - //is a combination of two devices, so update it. - sourceIndex = getRecordSourceIndex(mPortMixer); - if (sourceIndex >= 0) - SetMixer(sourceIndex); - } - } -#endif - - // Determine mixer capabilities - if it doesn't support control of output - // signal level, we emulate it (by multiplying this value by all outgoing - // samples) - - mMixerOutputVol = Px_GetPCMOutputVolume(mPortMixer); - mEmulateMixerOutputVol = false; - Px_SetPCMOutputVolume(mPortMixer, 0.0); - if (Px_GetPCMOutputVolume(mPortMixer) > 0.1) - mEmulateMixerOutputVol = true; - Px_SetPCMOutputVolume(mPortMixer, 0.2f); - if (Px_GetPCMOutputVolume(mPortMixer) < 0.1 || - Px_GetPCMOutputVolume(mPortMixer) > 0.3) - mEmulateMixerOutputVol = true; - Px_SetPCMOutputVolume(mPortMixer, mMixerOutputVol); - - float inputVol = Px_GetInputVolume(mPortMixer); - mInputMixerWorks = true; // assume it works unless proved wrong - Px_SetInputVolume(mPortMixer, 0.0); - if (Px_GetInputVolume(mPortMixer) > 0.1) - mInputMixerWorks = false; // can't set to zero - Px_SetInputVolume(mPortMixer, 0.2f); - if (Px_GetInputVolume(mPortMixer) < 0.1 || - Px_GetInputVolume(mPortMixer) > 0.3) - mInputMixerWorks = false; // can't set level accurately - Px_SetInputVolume(mPortMixer, inputVol); - - Pa_CloseStream(stream); - - - #if 0 - wxPrintf("PortMixer: Playback: %s Recording: %s\n", - mEmulateMixerOutputVol? "emulated": "native", - mInputMixerWorks? "hardware": "no control"); - #endif - - mMixerOutputVol = 1.0; - -#endif // USE_PORTMIXER -} - static PaSampleFormat AudacityToPortAudioSampleFormat(sampleFormat format) { switch(format) { @@ -1708,7 +1430,7 @@ void AudioIO::StartMonitoring( const AudioIOStartStreamOptions &options ) captureFormat); if (!success) { - wxString msg = wxString::Format(_("Error opening recording device.\nError code: %s"), gAudioIO->LastPaErrorString()); + wxString msg = wxString::Format(_("Error opening recording device.\nError code: %s"), Get()->LastPaErrorString()); ShowErrorDialog( ProjectWindow::Find( mOwningProject ), _("Error"), msg, wxT("Error_opening_sound_device")); return; @@ -2291,7 +2013,7 @@ void AudioIO::StartStreamCleanup(bool bOnlyBuffers) PmTimestamp MidiTime(void *WXUNUSED(info)) { - return gAudioIO->MidiTime(); + return AudioIO::Get()->MidiTime(); } // Set up state to iterate NoteTrack events in sequence. @@ -2392,34 +2114,6 @@ bool AudioIO::IsAvailable(AudacityProject *project) const return mOwningProject == NULL || mOwningProject == project; } -void AudioIO::SetCaptureMeter(AudacityProject *project, MeterPanel *meter) -{ - if (( mOwningProject ) && ( mOwningProject != project)) - return; - - if (meter) - { - mInputMeter = meter; - mInputMeter->Reset(mRate, true); - } - else - mInputMeter.Release(); -} - -void AudioIO::SetPlaybackMeter(AudacityProject *project, MeterPanel *meter) -{ - if (( mOwningProject ) && ( mOwningProject != project)) - return; - - if (meter) - { - mOutputMeter = meter; - mOutputMeter->Reset(mRate, true); - } - else - mOutputMeter.Release(); -} - void AudioIO::SetMeters() { if (mInputMeter) @@ -2731,11 +2425,6 @@ void AudioIO::SetPaused(bool state) mPaused = state; } -bool AudioIoCallback::IsPaused() const -{ - return mPaused; -} - #ifdef EXPERIMENTAL_SCRUBBING_SUPPORT void AudioIO::UpdateScrub (double endTimeOrSpeed, const ScrubbingOptions &options) @@ -2763,370 +2452,6 @@ double AudioIO::GetLastScrubTime() const #endif -bool AudioIO::IsBusy() const -{ - if (mStreamToken != 0) - return true; - - return false; -} - -bool AudioIO::IsStreamActive() const -{ - bool isActive = false; - // JKC: Not reporting any Pa error, but that looks OK. - if( mPortStreamV19 ) - isActive = (Pa_IsStreamActive( mPortStreamV19 ) > 0); - -#ifdef EXPERIMENTAL_MIDI_OUT - if( mMidiStreamActive && !mMidiOutputComplete ) - isActive = true; -#endif - return isActive; -} - -bool AudioIO::IsStreamActive(int token) const -{ - return (this->IsStreamActive() && this->IsAudioTokenActive(token)); -} - -bool AudioIO::IsAudioTokenActive(int token) const -{ - return ( token > 0 && token == mStreamToken ); -} - -bool AudioIO::IsMonitoring() const -{ - return ( mPortStreamV19 && mStreamToken==0 ); -} - -void AudioIO::PlaybackSchedule::Init( - const double t0, const double t1, - const AudioIOStartStreamOptions &options, - const RecordingSchedule *pRecordingSchedule ) -{ - if ( pRecordingSchedule ) - // It does not make sense to apply the time warp during overdub recording, - // which defeats the purpose of making the recording synchronized with - // the existing audio. (Unless we figured out the inverse warp of the - // captured samples in real time.) - // So just quietly ignore the time track. - mEnvelope = nullptr; - else - mEnvelope = options.envelope; - - mT0 = t0; - if (pRecordingSchedule) - mT0 -= pRecordingSchedule->mPreRoll; - - mT1 = t1; - if (pRecordingSchedule) - // adjust mT1 so that we don't give paComplete too soon to fill up the - // desired length of recording - mT1 -= pRecordingSchedule->mLatencyCorrection; - - // Main thread's initialization of mTime - SetTrackTime( mT0 ); - - mPlayMode = options.playLooped - ? PlaybackSchedule::PLAY_LOOPED - : PlaybackSchedule::PLAY_STRAIGHT; - mCutPreviewGapStart = options.cutPreviewGapStart; - mCutPreviewGapLen = options.cutPreviewGapLen; - -#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT - bool scrubbing = (options.pScrubbingOptions != nullptr); - - // Scrubbing is not compatible with looping or recording or a time track! - if (scrubbing) - { - const auto &scrubOptions = *options.pScrubbingOptions; - if (pRecordingSchedule || - Looping() || - mEnvelope || - scrubOptions.maxSpeed < ScrubbingOptions::MinAllowedScrubSpeed()) { - wxASSERT(false); - scrubbing = false; - } - else - mPlayMode = (scrubOptions.isPlayingAtSpeed) - ? PlaybackSchedule::PLAY_AT_SPEED - : PlaybackSchedule::PLAY_SCRUB; - } -#endif - - mWarpedTime = 0.0; -#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT - if (Scrubbing()) - mWarpedLength = 0.0f; - else -#endif - mWarpedLength = RealDuration(mT1); -} - -double AudioIO::PlaybackSchedule::LimitTrackTime() const -{ - // Track time readout for the main thread - // Allows for forward or backward play - return ClampTrackTime( GetTrackTime() ); -} - -double AudioIO::PlaybackSchedule::ClampTrackTime( double trackTime ) const -{ - if (ReversedTime()) - return std::max(mT1, std::min(mT0, trackTime)); - else - return std::max(mT0, std::min(mT1, trackTime)); -} - -double AudioIO::PlaybackSchedule::NormalizeTrackTime() const -{ - // Track time readout for the main thread - - // dmazzoni: This function is needed for two reasons: - // One is for looped-play mode - this function makes sure that the - // position indicator keeps wrapping around. The other reason is - // more subtle - it's because PortAudio can query the hardware for - // the current stream time, and this query is not always accurate. - // Sometimes it's a little behind or ahead, and so this function - // makes sure that at least we clip it to the selection. - // - // msmeyer: There is also the possibility that we are using "cut preview" - // mode. In this case, we should jump over a defined "gap" in the - // audio. - - double absoluteTime; - -#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 (Interactive()) - absoluteTime = GetTrackTime(); - else -#endif - absoluteTime = LimitTrackTime(); - - if (mCutPreviewGapLen > 0) - { - // msmeyer: We're in cut preview mode, so if we are on the right - // side of the gap, we jump over it. - if (absoluteTime > mCutPreviewGapStart) - absoluteTime += mCutPreviewGapLen; - } - - return absoluteTime; -} - -double AudioIO::GetStreamTime() -{ - // Track time readout for the main thread - - if( !IsStreamActive() ) - return BAD_STREAM_TIME; - - return mPlaybackSchedule.NormalizeTrackTime(); -} - - -std::vector AudioIO::GetSupportedPlaybackRates(int devIndex, double rate) -{ - if (devIndex == -1) - { // weren't given a device index, get the prefs / default one - devIndex = getPlayDevIndex(); - } - - // Check if we can use the cached rates - if (mCachedPlaybackIndex != -1 && devIndex == mCachedPlaybackIndex - && (rate == 0.0 || make_iterator_range(mCachedPlaybackRates).contains(rate))) - { - return mCachedPlaybackRates; - } - - std::vector supported; - int irate = (int)rate; - const PaDeviceInfo* devInfo = NULL; - int i; - - devInfo = Pa_GetDeviceInfo(devIndex); - - if (!devInfo) - { - wxLogDebug(wxT("GetSupportedPlaybackRates() Could not get device info!")); - return supported; - } - - // LLL: Remove when a proper method of determining actual supported - // DirectSound rate is devised. - const PaHostApiInfo* hostInfo = Pa_GetHostApiInfo(devInfo->hostApi); - bool isDirectSound = (hostInfo && hostInfo->type == paDirectSound); - - PaStreamParameters pars; - - pars.device = devIndex; - pars.channelCount = 1; - pars.sampleFormat = paFloat32; - pars.suggestedLatency = devInfo->defaultHighOutputLatency; - pars.hostApiSpecificStreamInfo = NULL; - - // JKC: PortAudio Errors handled OK here. No need to report them - for (i = 0; i < NumRatesToTry; i++) - { - // LLL: Remove when a proper method of determining actual supported - // DirectSound rate is devised. - if (!(isDirectSound && RatesToTry[i] > 200000)){ - if (Pa_IsFormatSupported(NULL, &pars, RatesToTry[i]) == 0) - supported.push_back(RatesToTry[i]); - Pa_Sleep( 10 );// There are ALSA drivers that don't like being probed - // too quickly. - } - } - - if (irate != 0 && !make_iterator_range(supported).contains(irate)) - { - // LLL: Remove when a proper method of determining actual supported - // DirectSound rate is devised. - if (!(isDirectSound && RatesToTry[i] > 200000)) - if (Pa_IsFormatSupported(NULL, &pars, irate) == 0) - supported.push_back(irate); - } - - return supported; -} - -std::vector AudioIO::GetSupportedCaptureRates(int devIndex, double rate) -{ - if (devIndex == -1) - { // not given a device, look up in prefs / default - devIndex = getRecordDevIndex(); - } - - // Check if we can use the cached rates - if (mCachedCaptureIndex != -1 && devIndex == mCachedCaptureIndex - && (rate == 0.0 || make_iterator_range(mCachedCaptureRates).contains(rate))) - { - return mCachedCaptureRates; - } - - std::vector supported; - int irate = (int)rate; - const PaDeviceInfo* devInfo = NULL; - int i; - - devInfo = Pa_GetDeviceInfo(devIndex); - - if (!devInfo) - { - wxLogDebug(wxT("GetSupportedCaptureRates() Could not get device info!")); - return supported; - } - - double latencyDuration = DEFAULT_LATENCY_DURATION; - long recordChannels = 1; - gPrefs->Read(wxT("/AudioIO/LatencyDuration"), &latencyDuration); - gPrefs->Read(wxT("/AudioIO/RecordChannels"), &recordChannels); - - // LLL: Remove when a proper method of determining actual supported - // DirectSound rate is devised. - const PaHostApiInfo* hostInfo = Pa_GetHostApiInfo(devInfo->hostApi); - bool isDirectSound = (hostInfo && hostInfo->type == paDirectSound); - - PaStreamParameters pars; - - pars.device = devIndex; - pars.channelCount = recordChannels; - pars.sampleFormat = paFloat32; - pars.suggestedLatency = latencyDuration / 1000.0; - pars.hostApiSpecificStreamInfo = NULL; - - for (i = 0; i < NumRatesToTry; i++) - { - // LLL: Remove when a proper method of determining actual supported - // DirectSound rate is devised. - if (!(isDirectSound && RatesToTry[i] > 200000)) - { - if (Pa_IsFormatSupported(&pars, NULL, RatesToTry[i]) == 0) - supported.push_back(RatesToTry[i]); - Pa_Sleep( 10 );// There are ALSA drivers that don't like being probed - // too quickly. - } - } - - if (irate != 0 && !make_iterator_range(supported).contains(irate)) - { - // LLL: Remove when a proper method of determining actual supported - // DirectSound rate is devised. - if (!(isDirectSound && RatesToTry[i] > 200000)) - if (Pa_IsFormatSupported(&pars, NULL, irate) == 0) - supported.push_back(irate); - } - - return supported; -} - -std::vector AudioIO::GetSupportedSampleRates(int playDevice, int recDevice, double rate) -{ - // Not given device indices, look up prefs - if (playDevice == -1) { - playDevice = getPlayDevIndex(); - } - if (recDevice == -1) { - recDevice = getRecordDevIndex(); - } - - // Check if we can use the cached rates - if (mCachedPlaybackIndex != -1 && mCachedCaptureIndex != -1 && - playDevice == mCachedPlaybackIndex && - recDevice == mCachedCaptureIndex && - (rate == 0.0 || make_iterator_range(mCachedSampleRates).contains(rate))) - { - return mCachedSampleRates; - } - - auto playback = GetSupportedPlaybackRates(playDevice, rate); - auto capture = GetSupportedCaptureRates(recDevice, rate); - int i; - - // Return only sample rates which are in both arrays - std::vector result; - - for (i = 0; i < (int)playback.size(); i++) - if (make_iterator_range(capture).contains(playback[i])) - result.push_back(playback[i]); - - // If this yields no results, use the default sample rates nevertheless -/* if (result.empty()) - { - for (i = 0; i < NumStandardRates; i++) - result.push_back(StandardRates[i]); - }*/ - - return result; -} - -/** \todo: should this take into account PortAudio's value for - * PaDeviceInfo::defaultSampleRate? In principal this should let us work out - * which rates are "real" and which resampled in the drivers, and so prefer - * the real rates. */ -int AudioIO::GetOptimalSupportedSampleRate() -{ - auto rates = GetSupportedSampleRates(); - - if (make_iterator_range(rates).contains(44100)) - return 44100; - - if (make_iterator_range(rates).contains(48000)) - return 48000; - - // if there are no supported rates, the next bit crashes. So check first, - // and give them a "sensible" value if there are no valid values. They - // will still get an error later, but with any luck may have changed - // something by then. It's no worse than having an invalid default rate - // stored in the preferences, which we don't check for - if (rates.empty()) return 44100; - - return rates.back(); -} - double AudioIO::GetBestRate(bool capturing, bool playing, double sampleRate) { // Check if we can use the cached value @@ -3214,7 +2539,9 @@ finished: AudioThread::ExitCode AudioThread::Entry() { - while( !TestDestroy() ) + AudioIO *gAudioIO; + while( !TestDestroy() && + nullptr != ( gAudioIO = AudioIO::Get() ) ) { using Clock = std::chrono::steady_clock; auto loopPassStart = Clock::now(); @@ -3247,7 +2574,9 @@ AudioThread::ExitCode AudioThread::Entry() #ifdef EXPERIMENTAL_MIDI_OUT MidiThread::ExitCode MidiThread::Entry() { - while( !TestDestroy() ) + AudioIO *gAudioIO; + while( !TestDestroy() && + nullptr != ( gAudioIO = AudioIO::Get() ) ) { // Set LoopActive outside the tests to avoid race condition gAudioIO->mMidiThreadFillBuffersLoopActive = true; @@ -3296,484 +2625,6 @@ size_t AudioIO::GetCommonlyAvailCapture() return commonlyAvail; } -#if USE_PORTMIXER -int AudioIO::getRecordSourceIndex(PxMixer *portMixer) -{ - int i; - wxString sourceName = gPrefs->Read(wxT("/AudioIO/RecordingSource"), wxT("")); - int numSources = Px_GetNumInputSources(portMixer); - for (i = 0; i < numSources; i++) { - if (sourceName == wxString(wxSafeConvertMB2WX(Px_GetInputSourceName(portMixer, i)))) - return i; - } - return -1; -} -#endif - -int AudioIO::getPlayDevIndex(const wxString &devNameArg) -{ - wxString devName(devNameArg); - // if we don't get given a device, look up the preferences - if (devName.empty()) - { - devName = gPrefs->Read(wxT("/AudioIO/PlaybackDevice"), wxT("")); - } - - wxString hostName = gPrefs->Read(wxT("/AudioIO/Host"), wxT("")); - PaHostApiIndex hostCnt = Pa_GetHostApiCount(); - PaHostApiIndex hostNum; - for (hostNum = 0; hostNum < hostCnt; hostNum++) - { - const PaHostApiInfo *hinfo = Pa_GetHostApiInfo(hostNum); - if (hinfo && wxString(wxSafeConvertMB2WX(hinfo->name)) == hostName) - { - for (PaDeviceIndex hostDevice = 0; hostDevice < hinfo->deviceCount; hostDevice++) - { - PaDeviceIndex deviceNum = Pa_HostApiDeviceIndexToDeviceIndex(hostNum, hostDevice); - - const PaDeviceInfo *dinfo = Pa_GetDeviceInfo(deviceNum); - if (dinfo && DeviceName(dinfo) == devName && dinfo->maxOutputChannels > 0 ) - { - // this device name matches the stored one, and works. - // So we say this is the answer and return it - return deviceNum; - } - } - - // The device wasn't found so use the default for this host. - // LL: At this point, preferences and active no longer match. - return hinfo->defaultOutputDevice; - } - } - - // The host wasn't found, so use the default output device. - // FIXME: TRAP_ERR PaErrorCode not handled well (this code is similar to input code - // and the input side has more comments.) - - PaDeviceIndex deviceNum = Pa_GetDefaultOutputDevice(); - - // Sometimes PortAudio returns -1 if it cannot find a suitable default - // device, so we just use the first one available - // - // LL: At this point, preferences and active no longer match - // - // And I can't imagine how far we'll get specifying an "invalid" index later - // on...are we certain "0" even exists? - if (deviceNum < 0) { - wxASSERT(false); - deviceNum = 0; - } - - return deviceNum; -} - -int AudioIO::getRecordDevIndex(const wxString &devNameArg) -{ - wxString devName(devNameArg); - // if we don't get given a device, look up the preferences - if (devName.empty()) - { - devName = gPrefs->Read(wxT("/AudioIO/RecordingDevice"), wxT("")); - } - - wxString hostName = gPrefs->Read(wxT("/AudioIO/Host"), wxT("")); - PaHostApiIndex hostCnt = Pa_GetHostApiCount(); - PaHostApiIndex hostNum; - for (hostNum = 0; hostNum < hostCnt; hostNum++) - { - const PaHostApiInfo *hinfo = Pa_GetHostApiInfo(hostNum); - if (hinfo && wxString(wxSafeConvertMB2WX(hinfo->name)) == hostName) - { - for (PaDeviceIndex hostDevice = 0; hostDevice < hinfo->deviceCount; hostDevice++) - { - PaDeviceIndex deviceNum = Pa_HostApiDeviceIndexToDeviceIndex(hostNum, hostDevice); - - const PaDeviceInfo *dinfo = Pa_GetDeviceInfo(deviceNum); - if (dinfo && DeviceName(dinfo) == devName && dinfo->maxInputChannels > 0 ) - { - // this device name matches the stored one, and works. - // So we say this is the answer and return it - return deviceNum; - } - } - - // The device wasn't found so use the default for this host. - // LL: At this point, preferences and active no longer match. - return hinfo->defaultInputDevice; - } - } - - // The host wasn't found, so use the default input device. - // FIXME: TRAP_ERR PaErrorCode not handled well in getRecordDevIndex() - PaDeviceIndex deviceNum = Pa_GetDefaultInputDevice(); - - // Sometimes PortAudio returns -1 if it cannot find a suitable default - // device, so we just use the first one available - // PortAudio has an error reporting function. We should log/report the error? - // - // LL: At this point, preferences and active no longer match - // - // And I can't imagine how far we'll get specifying an "invalid" index later - // on...are we certain "0" even exists? - if (deviceNum < 0) { - // JKC: This ASSERT will happen if you run with no config file - // This happens once. Config file will exist on the next run. - // TODO: Look into this a bit more. Could be relevant to blank Device Toolbar. - wxASSERT(false); - deviceNum = 0; - } - - return deviceNum; -} - -wxString AudioIO::GetDeviceInfo() -{ - wxStringOutputStream o; - wxTextOutputStream s(o, wxEOL_UNIX); - - if (IsStreamActive()) { - return _("Stream is active ... unable to gather information.\n"); - } - - - // FIXME: TRAP_ERR PaErrorCode not handled. 3 instances in GetDeviceInfo(). - int recDeviceNum = Pa_GetDefaultInputDevice(); - int playDeviceNum = Pa_GetDefaultOutputDevice(); - int cnt = Pa_GetDeviceCount(); - - // PRL: why only into the log? - wxLogDebug(wxT("Portaudio reports %d audio devices"),cnt); - - s << wxT("==============================\n"); - s << wxString::Format(_("Default recording device number: %d\n"), recDeviceNum); - s << wxString::Format(_("Default playback device number: %d\n"), playDeviceNum); - - wxString recDevice = gPrefs->Read(wxT("/AudioIO/RecordingDevice"), wxT("")); - wxString playDevice = gPrefs->Read(wxT("/AudioIO/PlaybackDevice"), wxT("")); - int j; - - // This gets info on all available audio devices (input and output) - if (cnt <= 0) { - s << _("No devices found\n"); - return o.GetString(); - } - - const PaDeviceInfo* info; - - for (j = 0; j < cnt; j++) { - s << wxT("==============================\n"); - - info = Pa_GetDeviceInfo(j); - if (!info) { - s << wxString::Format(_("Device info unavailable for: %d\n"), j); - continue; - } - - wxString name = DeviceName(info); - s << wxString::Format(_("Device ID: %d\n"), j); - s << wxString::Format(_("Device name: %s\n"), name); - s << wxString::Format(_("Host name: %s\n"), HostName(info)); - s << wxString::Format(_("Recording channels: %d\n"), info->maxInputChannels); - s << wxString::Format(_("Playback channels: %d\n"), info->maxOutputChannels); - s << wxString::Format(_("Low Recording Latency: %g\n"), info->defaultLowInputLatency); - s << wxString::Format(_("Low Playback Latency: %g\n"), info->defaultLowOutputLatency); - s << wxString::Format(_("High Recording Latency: %g\n"), info->defaultHighInputLatency); - s << wxString::Format(_("High Playback Latency: %g\n"), info->defaultHighOutputLatency); - - auto rates = GetSupportedPlaybackRates(j, 0.0); - - /* i18n-hint: Supported, meaning made available by the system */ - s << _("Supported Rates:\n"); - for (int k = 0; k < (int) rates.size(); k++) { - s << wxT(" ") << (int)rates[k] << wxT("\n"); - } - - if (name == playDevice && info->maxOutputChannels > 0) - playDeviceNum = j; - - if (name == recDevice && info->maxInputChannels > 0) - recDeviceNum = j; - - // Sometimes PortAudio returns -1 if it cannot find a suitable default - // device, so we just use the first one available - if (recDeviceNum < 0 && info->maxInputChannels > 0){ - recDeviceNum = j; - } - if (playDeviceNum < 0 && info->maxOutputChannels > 0){ - playDeviceNum = j; - } - } - - bool haveRecDevice = (recDeviceNum >= 0); - bool havePlayDevice = (playDeviceNum >= 0); - - s << wxT("==============================\n"); - if (haveRecDevice) - s << wxString::Format(_("Selected recording device: %d - %s\n"), recDeviceNum, recDevice); - else - s << wxString::Format(_("No recording device found for '%s'.\n"), recDevice); - - if (havePlayDevice) - s << wxString::Format(_("Selected playback device: %d - %s\n"), playDeviceNum, playDevice); - else - s << wxString::Format(_("No playback device found for '%s'.\n"), playDevice); - - std::vector supportedSampleRates; - - if (havePlayDevice && haveRecDevice) { - supportedSampleRates = GetSupportedSampleRates(playDeviceNum, recDeviceNum); - - s << _("Supported Rates:\n"); - for (int k = 0; k < (int) supportedSampleRates.size(); k++) { - s << wxT(" ") << (int)supportedSampleRates[k] << wxT("\n"); - } - } - else { - s << _("Cannot check mutual sample rates without both devices.\n"); - return o.GetString(); - } - -#if defined(USE_PORTMIXER) - if (supportedSampleRates.size() > 0) - { - int highestSampleRate = supportedSampleRates.back(); - bool EmulateMixerInputVol = true; - bool EmulateMixerOutputVol = true; - float MixerInputVol = 1.0; - float MixerOutputVol = 1.0; - - int error; - - PaStream *stream; - - PaStreamParameters playbackParameters; - - playbackParameters.device = playDeviceNum; - playbackParameters.sampleFormat = paFloat32; - playbackParameters.hostApiSpecificStreamInfo = NULL; - playbackParameters.channelCount = 1; - if (Pa_GetDeviceInfo(playDeviceNum)){ - playbackParameters.suggestedLatency = - Pa_GetDeviceInfo(playDeviceNum)->defaultLowOutputLatency; - } - else{ - playbackParameters.suggestedLatency = DEFAULT_LATENCY_CORRECTION/1000.0; - } - - PaStreamParameters captureParameters; - - captureParameters.device = recDeviceNum; - captureParameters.sampleFormat = paFloat32;; - captureParameters.hostApiSpecificStreamInfo = NULL; - captureParameters.channelCount = 1; - if (Pa_GetDeviceInfo(recDeviceNum)){ - captureParameters.suggestedLatency = - Pa_GetDeviceInfo(recDeviceNum)->defaultLowInputLatency; - }else{ - captureParameters.suggestedLatency = DEFAULT_LATENCY_CORRECTION/1000.0; - } - - error = Pa_OpenStream(&stream, - &captureParameters, &playbackParameters, - highestSampleRate, paFramesPerBufferUnspecified, - paClipOff | paDitherOff, - audacityAudioCallback, NULL); - - if (error) { - error = Pa_OpenStream(&stream, - &captureParameters, NULL, - highestSampleRate, paFramesPerBufferUnspecified, - paClipOff | paDitherOff, - audacityAudioCallback, NULL); - } - - if (error) { - s << wxString::Format(_("Received %d while opening devices\n"), error); - return o.GetString(); - } - - PxMixer *PortMixer = Px_OpenMixer(stream, 0); - - if (!PortMixer) { - s << _("Unable to open Portmixer\n"); - Pa_CloseStream(stream); - return o.GetString(); - } - - s << wxT("==============================\n"); - s << _("Available mixers:\n"); - - // FIXME: ? PortMixer errors on query not reported in GetDeviceInfo - cnt = Px_GetNumMixers(stream); - for (int i = 0; i < cnt; i++) { - wxString name = wxSafeConvertMB2WX(Px_GetMixerName(stream, i)); - s << wxString::Format(_("%d - %s\n"), i, name); - } - - s << wxT("==============================\n"); - s << _("Available recording sources:\n"); - cnt = Px_GetNumInputSources(PortMixer); - for (int i = 0; i < cnt; i++) { - wxString name = wxSafeConvertMB2WX(Px_GetInputSourceName(PortMixer, i)); - s << wxString::Format(_("%d - %s\n"), i, name); - } - - s << wxT("==============================\n"); - s << _("Available playback volumes:\n"); - cnt = Px_GetNumOutputVolumes(PortMixer); - for (int i = 0; i < cnt; i++) { - wxString name = wxSafeConvertMB2WX(Px_GetOutputVolumeName(PortMixer, i)); - s << wxString::Format(_("%d - %s\n"), i, name); - } - - // Determine mixer capabilities - it it doesn't support either - // input or output, we emulate them (by multiplying this value - // by all incoming/outgoing samples) - - MixerOutputVol = Px_GetPCMOutputVolume(PortMixer); - EmulateMixerOutputVol = false; - Px_SetPCMOutputVolume(PortMixer, 0.0); - if (Px_GetPCMOutputVolume(PortMixer) > 0.1) - EmulateMixerOutputVol = true; - Px_SetPCMOutputVolume(PortMixer, 0.2f); - if (Px_GetPCMOutputVolume(PortMixer) < 0.1 || - Px_GetPCMOutputVolume(PortMixer) > 0.3) - EmulateMixerOutputVol = true; - Px_SetPCMOutputVolume(PortMixer, MixerOutputVol); - - MixerInputVol = Px_GetInputVolume(PortMixer); - EmulateMixerInputVol = false; - Px_SetInputVolume(PortMixer, 0.0); - if (Px_GetInputVolume(PortMixer) > 0.1) - EmulateMixerInputVol = true; - Px_SetInputVolume(PortMixer, 0.2f); - if (Px_GetInputVolume(PortMixer) < 0.1 || - Px_GetInputVolume(PortMixer) > 0.3) - EmulateMixerInputVol = true; - Px_SetInputVolume(PortMixer, MixerInputVol); - - Pa_CloseStream(stream); - - s << wxT("==============================\n"); - s << ( EmulateMixerInputVol - ? _("Recording volume is emulated\n") - : _("Recording volume is native\n") ); - s << ( EmulateMixerOutputVol - ? _("Playback volume is emulated\n") - : _("Playback volume is native\n") ); - - Px_CloseMixer(PortMixer); - - } //end of massive if statement if a valid sample rate has been found -#endif - return o.GetString(); -} - -#ifdef EXPERIMENTAL_MIDI_OUT -// FIXME: When EXPERIMENTAL_MIDI_IN is added (eventually) this should also be enabled -- Poke -wxString AudioIO::GetMidiDeviceInfo() -{ - wxStringOutputStream o; - wxTextOutputStream s(o, wxEOL_UNIX); - - if (IsStreamActive()) { - return _("Stream is active ... unable to gather information.\n"); - } - - - // XXX: May need to trap errors as with the normal device info - int recDeviceNum = Pm_GetDefaultInputDeviceID(); - int playDeviceNum = Pm_GetDefaultOutputDeviceID(); - int cnt = Pm_CountDevices(); - - // PRL: why only into the log? - wxLogDebug(wxT("PortMidi reports %d MIDI devices"), cnt); - - s << wxT("==============================\n"); - s << wxString::Format(_("Default recording device number: %d\n"), recDeviceNum); - s << wxString::Format(_("Default playback device number: %d\n"), playDeviceNum); - - wxString recDevice = gPrefs->Read(wxT("/MidiIO/RecordingDevice"), wxT("")); - wxString playDevice = gPrefs->Read(wxT("/MidiIO/PlaybackDevice"), wxT("")); - - // This gets info on all available audio devices (input and output) - if (cnt <= 0) { - s << _("No devices found\n"); - return o.GetString(); - } - - for (int i = 0; i < cnt; i++) { - s << wxT("==============================\n"); - - const PmDeviceInfo* info = Pm_GetDeviceInfo(i); - if (!info) { - s << wxString::Format(_("Device info unavailable for: %d\n"), i); - continue; - } - - wxString name = wxSafeConvertMB2WX(info->name); - wxString hostName = wxSafeConvertMB2WX(info->interf); - - s << wxString::Format(_("Device ID: %d\n"), i); - s << wxString::Format(_("Device name: %s\n"), name); - s << wxString::Format(_("Host name: %s\n"), hostName); - /* i18n-hint: Supported, meaning made available by the system */ - s << wxString::Format(_("Supports output: %d\n"), info->output); - s << wxString::Format(_("Supports input: %d\n"), info->input); - s << wxString::Format(_("Opened: %d\n"), info->opened); - - if (name == playDevice && info->output) - playDeviceNum = i; - - if (name == recDevice && info->input) - recDeviceNum = i; - - // XXX: This is only done because the same was applied with PortAudio - // If PortMidi returns -1 for the default device, use the first one - if (recDeviceNum < 0 && info->input){ - recDeviceNum = i; - } - if (playDeviceNum < 0 && info->output){ - playDeviceNum = i; - } - } - - bool haveRecDevice = (recDeviceNum >= 0); - bool havePlayDevice = (playDeviceNum >= 0); - - s << wxT("==============================\n"); - if (haveRecDevice) - s << wxString::Format(_("Selected MIDI recording device: %d - %s\n"), recDeviceNum, recDevice); - else - s << wxString::Format(_("No MIDI recording device found for '%s'.\n"), recDevice); - - if (havePlayDevice) - s << wxString::Format(_("Selected MIDI playback device: %d - %s\n"), playDeviceNum, playDevice); - else - s << wxString::Format(_("No MIDI playback device found for '%s'.\n"), playDevice); - - // Mention our conditional compilation flags for Alpha only -#ifdef IS_ALPHA - - // Not internationalizing these alpha-only messages - s << wxT("==============================\n"); -#ifdef EXPERIMENTAL_MIDI_OUT - s << wxT("EXPERIMENTAL_MIDI_OUT is enabled\n"); -#else - s << wxT("EXPERIMENTAL_MIDI_OUT is NOT enabled\n"); -#endif -#ifdef EXPERIMENTAL_MIDI_IN - s << wxT("EXPERIMENTAL_MIDI_IN is enabled\n"); -#else - s << wxT("EXPERIMENTAL_MIDI_IN is NOT enabled\n"); -#endif - -#endif - - return o.GetString(); -} -#endif - // This method is the data gateway between the audio thread (which // communicates with the disk) and the PortAudio callback thread // (which communicates with the audio device). @@ -4731,6 +3582,7 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer, const PaStreamCallbackTimeInfo *timeInfo, const PaStreamCallbackFlags statusFlags, void *userData ) { + auto gAudioIO = AudioIO::Get(); return gAudioIO->AudioCallback( inputBuffer, outputBuffer, framesPerBuffer, timeInfo, statusFlags, userData); @@ -5625,118 +4477,6 @@ void AudioIoCallback::CallbackCheckCompletion( callbackReturn = paComplete; } -bool AudioIO::PlaybackSchedule::PassIsComplete() const -{ - // Test mTime within the PortAudio callback - if (Scrubbing()) - return false; // but may be true if playing at speed - return Overruns( GetTrackTime() ); -} - -bool AudioIO::PlaybackSchedule::Overruns( double trackTime ) const -{ - return (ReversedTime() ? trackTime <= mT1 : trackTime >= mT1); -} - -namespace -{ -/** @brief Compute the duration (in seconds at playback) of the specified region of the track. - * - * Takes a region of the time track (specified by the unwarped time points in the project), and - * calculates how long it will actually take to play this region back, taking the time track's - * warping effects into account. - * @param t0 unwarped time to start calculation from - * @param t1 unwarped time to stop calculation at - * @return the warped duration in seconds - */ -double ComputeWarpedLength(const Envelope &env, double t0, double t1) -{ - return env.IntegralOfInverse(t0, t1); -} - -/** @brief Compute how much unwarped time must have elapsed if length seconds of warped time has - * elapsed - * - * @param t0 The unwarped time (seconds from project start) at which to start - * @param length How many seconds of warped time went past. - * @return The end point (in seconds from project start) as unwarped time - */ -double SolveWarpedLength(const Envelope &env, double t0, double length) -{ - return env.SolveIntegralOfInverse(t0, length); -} -} - -double AudioIO::PlaybackSchedule::AdvancedTrackTime( - double time, double realElapsed, double speed ) const -{ - if (ReversedTime()) - realElapsed *= -1.0; - - // Defense against cases that might cause loops not to terminate - if ( fabs(mT0 - mT1) < 1e-9 ) - return mT0; - - if (mEnvelope) { - wxASSERT( speed == 1.0 ); - - double total=0.0; - bool foundTotal = false; - do { - auto oldTime = time; - if (foundTotal && fabs(realElapsed) > fabs(total)) - // Avoid SolveWarpedLength - time = mT1; - else - time = SolveWarpedLength(*mEnvelope, time, realElapsed); - - if (!Looping() || !Overruns( time )) - break; - - // Bug1922: The part of the time track outside the loop should not - // influence the result - double delta; - if (foundTotal && oldTime == mT0) - // Avoid integrating again - delta = total; - else { - delta = ComputeWarpedLength(*mEnvelope, oldTime, mT1); - if (oldTime == mT0) - foundTotal = true, total = delta; - } - realElapsed -= delta; - time = mT0; - } while ( true ); - } - else { - time += realElapsed * speed; - - // Wrap to start if looping - if (Looping()) { - while ( Overruns( time ) ) { - // 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! - time -= mT1 - mT0; - } - } - } - - return time; -} - -void AudioIO::PlaybackSchedule::TrackTimeUpdate(double realElapsed) -{ - // Update mTime within the PortAudio callback - - if (Interactive()) - return; - - auto time = GetTrackTime(); - auto newTime = AdvancedTrackTime( time, realElapsed, 1.0 ); - SetTrackTime( newTime ); -} - void AudioIO::TimeQueue::Producer( const PlaybackSchedule &schedule, double rate, double scrubSpeed, size_t nSamples ) @@ -5794,62 +4534,6 @@ double AudioIO::TimeQueue::Consumer( size_t nSamples, double rate ) return mData[ mHead.mIndex ]; } -double AudioIO::PlaybackSchedule::TrackDuration(double realElapsed) const -{ - if (mEnvelope) - return SolveWarpedLength(*mEnvelope, mT0, realElapsed); - else - return realElapsed; -} - -double AudioIO::PlaybackSchedule::RealDuration(double trackTime1) const -{ - double duration; - if (mEnvelope) - duration = ComputeWarpedLength(*mEnvelope, mT0, trackTime1); - else - duration = trackTime1 - mT0; - return fabs(duration); -} - -double AudioIO::PlaybackSchedule::RealTimeRemaining() const -{ - return mWarpedLength - mWarpedTime; -} - -void AudioIO::PlaybackSchedule::RealTimeAdvance( double increment ) -{ - mWarpedTime += increment; -} - -void AudioIO::PlaybackSchedule::RealTimeInit( double trackTime ) -{ - if (Scrubbing()) - mWarpedTime = 0.0; - else - mWarpedTime = RealDuration( trackTime ); -} - -void AudioIO::PlaybackSchedule::RealTimeRestart() -{ - mWarpedTime = 0; -} - -double AudioIO::RecordingSchedule::ToConsume() const -{ - return mDuration - Consumed(); -} - -double AudioIO::RecordingSchedule::Consumed() const -{ - return std::max( 0.0, mPosition + TotalCorrection() ); -} - -double AudioIO::RecordingSchedule::ToDiscard() const -{ - return std::max(0.0, -( mPosition + TotalCorrection() ) ); -} - bool AudioIO::IsCapturing() const { // Includes a test of mTime, used in the main thread diff --git a/src/AudioIO.h b/src/AudioIO.h index 247b6fd64..fc579fc4f 100644 --- a/src/AudioIO.h +++ b/src/AudioIO.h @@ -15,16 +15,12 @@ #include "Audacity.h" // for USE_* macros +#include "AudioIOBase.h" // to inherit + #include "Experimental.h" -#include "portaudio.h" - -#include -#include #include -#include #include // member variable -#include // member variable #ifdef USE_MIDI @@ -44,22 +40,17 @@ using NoteTrackConstArray = std::vector < std::shared_ptr< const NoteTrack > >; #endif // USE_MIDI -#if USE_PORTMIXER -#include "../lib-src/portmixer/include/portmixer.h" -#endif - #include // to declare custom event types #include "SampleFormat.h" class wxArrayString; +class AudioIOBase; class AudioIO; class RingBuffer; class Mixer; class Resample; -class BoundedEnvelope; class AudioThread; -class MeterPanel; class SelectedRegion; class AudacityProject; @@ -68,19 +59,8 @@ class WaveTrack; using WaveTrackArray = std::vector < std::shared_ptr < WaveTrack > >; using WaveTrackConstArray = std::vector < std::shared_ptr < const WaveTrack > >; -extern AUDACITY_DLL_API AudioIO *gAudioIO; - -void InitAudioIO(); -void DeinitAudioIO(); -wxString DeviceName(const PaDeviceInfo* info); -wxString HostName(const PaDeviceInfo* info); bool ValidateDeviceNames(); -class AudioIOListener; - -// #include if you need this constant -#define BAD_STREAM_TIME (-DBL_MAX) - #define MAX_MIDI_BUFFER_SIZE 5000 #define DEFAULT_SYNTH_LATENCY 5 @@ -108,48 +88,6 @@ wxDECLARE_EXPORTED_EVENT(AUDACITY_DLL_API, // So leave the separate thread ENABLED. #define USE_MIDI_THREAD -struct ScrubbingOptions; - -using PRCrossfadeData = std::vector< std::vector < float > >; - -// To avoid growing the argument list of StartStream, add fields here -struct AudioIOStartStreamOptions -{ - explicit - AudioIOStartStreamOptions(AudacityProject *pProject_, double rate_) - : pProject{ pProject_ } - , envelope(nullptr) - , listener(NULL) - , rate(rate_) - , playLooped(false) - , cutPreviewGapStart(0.0) - , cutPreviewGapLen(0.0) - , pStartTime(NULL) - , preRoll(0.0) - {} - - AudacityProject *pProject{}; - MeterPanel *captureMeter{}, *playbackMeter{}; - BoundedEnvelope *envelope; // for time warping - AudioIOListener* listener; - double rate; - bool playLooped; - double cutPreviewGapStart; - double cutPreviewGapLen; - double * pStartTime; - double preRoll; - -#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT - // Non-null value indicates that scrubbing will happen - // (do not specify a time track, looping, or recording, which - // are all incompatible with scrubbing): - ScrubbingOptions *pScrubbingOptions {}; -#endif - - // contents may get swapped with empty vector - PRCrossfadeData *pCrossfadeData{}; -}; - struct TransportTracks { WaveTrackArray playbackTracks; WaveTrackArray captureTracks; @@ -296,7 +234,9 @@ void MessageBuffer::Write( Data &&data ) mSlots[idx].mBusy.store( false, std::memory_order_release ); } -class AUDACITY_DLL_API AudioIoCallback { +class AUDACITY_DLL_API AudioIoCallback /* not final */ + : public AudioIOBase +{ public: AudioIoCallback(); ~AudioIoCallback(); @@ -374,9 +314,6 @@ public: double AudioTime() { return mPlaybackSchedule.mT0 + mNumFrames / mRate; } #endif - /** \brief Find out if playback / recording is currently paused */ - bool IsPaused() const; - /** \brief Get the number of audio samples ready in all of the playback * buffers. @@ -452,12 +389,8 @@ public: double mNextEventTime; /// Track of next event NoteTrack *mNextEventTrack; - /// True when output reaches mT1 - bool mMidiOutputComplete{ true }; /// Is the next event a note-on? bool mNextIsNoteOn; - /// mMidiStreamActive tells when mMidiStream is open for output - bool mMidiStreamActive; /// when true, mSendMidiState means send only updates, not note-on's, /// used to send state changes that precede the selected notes bool mSendMidiState; @@ -494,11 +427,8 @@ public: WaveTrackArray mPlaybackTracks; ArrayOf> mPlaybackMixers; - volatile int mStreamToken; static int mNextStreamToken; double mFactor; - /// Audio playback rate in samples per second - double mRate; unsigned long mMaxFramesOutput; // The actual number of frames output. bool mbMicroFades; @@ -512,9 +442,6 @@ public: size_t mPlaybackQueueMinimum; double mMinCaptureSecsToCopy; - /// True if audio playback is paused - bool mPaused; - PaStream *mPortStreamV19; bool mSoftwarePlaythrough; /// True if Sound Activated Recording is enabled bool mPauseRec; @@ -539,28 +466,9 @@ public: protected: - AudacityProject *mOwningProject; - wxWeakRef mInputMeter{}; - wxWeakRef mOutputMeter{}; bool mUpdateMeters; volatile bool mUpdatingMeters; - #if USE_PORTMIXER - PxMixer *mPortMixer; - float mPreviousHWPlaythrough; - #endif /* USE_PORTMIXER */ - - bool mEmulateMixerOutputVol; - /** @brief Can we control the hardware input level? - * - * This flag is set to true if using portmixer to control the - * input volume seems to be working (and so we offer the user the control), - * and to false (locking the control out) otherwise. This avoids stupid - * scaled clipping problems when trying to do software emulated input volume - * control */ - bool mInputMixerWorks; - float mMixerOutputVol; - AudioIOListener* mListener; friend class AudioThread; @@ -568,17 +476,9 @@ protected: friend class MidiThread; #endif - friend void InitAudioIO(); - bool mUsingAlsa { false }; // For cacheing supported sample rates - static int mCachedPlaybackIndex; - static std::vector mCachedPlaybackRates; - static int mCachedCaptureIndex; - static std::vector mCachedCaptureRates; - static std::vector mCachedSampleRates; - static double mCachedBestRateIn; static double mCachedBestRateOut; static bool mCachedBestRatePlaying; static bool mCachedBestRateCapturing; @@ -622,154 +522,7 @@ public: bool mDetectUpstreamDropouts{ true }; protected: - struct RecordingSchedule { - double mPreRoll{}; - double mLatencyCorrection{}; // negative value usually - double mDuration{}; - PRCrossfadeData mCrossfadeData; - - // These are initialized by the main thread, then updated - // only by the thread calling FillBuffers: - double mPosition{}; - bool mLatencyCorrected{}; - - double TotalCorrection() const { return mLatencyCorrection - mPreRoll; } - double ToConsume() const; - double Consumed() const; - double ToDiscard() const; - } mRecordingSchedule{}; - - struct PlaybackSchedule { - /// Playback starts at offset of mT0, which is measured in seconds. - double mT0; - /// Playback ends at offset of mT1, which is measured in seconds. Note that mT1 may be less than mT0 during scrubbing. - double mT1; - /// Current track time position during playback, in seconds. - /// Initialized by the main thread but updated by worker threads during - /// playback or recording, and periodically reread by the main thread for - /// purposes such as display update. - std::atomic mTime; - - /// Accumulated real time (not track position), starting at zero (unlike - /// mTime), and wrapping back to zero each time around looping play. - /// Thus, it is the length in real seconds between mT0 and mTime. - double mWarpedTime; - - /// Real length to be played (if looping, for each pass) after warping via a - /// time track, computed just once when starting the stream. - /// Length in real seconds between mT0 and mT1. Always positive. - double mWarpedLength; - - // 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 - - const BoundedEnvelope *mEnvelope; - - volatile enum { - PLAY_STRAIGHT, - PLAY_LOOPED, -#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT - PLAY_SCRUB, - PLAY_AT_SPEED, // a version of PLAY_SCRUB. -#endif - } mPlayMode { PLAY_STRAIGHT }; - double mCutPreviewGapStart; - double mCutPreviewGapLen; - - void Init( - double t0, double t1, - const AudioIOStartStreamOptions &options, - const RecordingSchedule *pRecordingSchedule ); - - /** \brief True if the end time is before the start time */ - bool ReversedTime() const - { - return mT1 < mT0; - } - - /** \brief Get current track time value, unadjusted - * - * Returns a time in seconds. - */ - double GetTrackTime() const - { return mTime.load(std::memory_order_relaxed); } - - /** \brief Set current track time value, unadjusted - */ - void SetTrackTime( double time ) - { mTime.store(time, std::memory_order_relaxed); } - - /** \brief Clamps argument to be between mT0 and mT1 - * - * Returns the bound if the value is out of bounds; does not wrap. - * Returns a time in seconds. - */ - double ClampTrackTime( double trackTime ) const; - - /** \brief Clamps mTime to be between mT0 and mT1 - * - * Returns the bound if the value is out of bounds; does not wrap. - * Returns a time in seconds. - */ - double LimitTrackTime() const; - - /** \brief Normalizes mTime, clamping it and handling gaps from cut preview. - * - * Clamps the time (unless scrubbing), and skips over the cut section. - * Returns a time in seconds. - */ - double NormalizeTrackTime() const; - - void ResetMode() { mPlayMode = PLAY_STRAIGHT; } - - bool PlayingStraight() const { return mPlayMode == PLAY_STRAIGHT; } - bool Looping() const { return mPlayMode == PLAY_LOOPED; } - bool Scrubbing() const { return mPlayMode == PLAY_SCRUB; } - bool PlayingAtSpeed() const { return mPlayMode == PLAY_AT_SPEED; } - bool Interactive() const { return Scrubbing() || PlayingAtSpeed(); } - - // Returns true if a loop pass, or the sole pass of straight play, - // is completed at the current value of mTime - bool PassIsComplete() const; - - // Returns true if time equals t1 or is on opposite side of t1, to t0 - bool Overruns( double trackTime ) const; - - // Compute the NEW track time for the given one and a real duration, - // taking into account whether the schedule is for looping - double AdvancedTrackTime( - double trackTime, double realElapsed, double speed) const; - - // Use the function above in the callback after consuming samples from the - // playback ring buffers, during usual straight or looping play - void TrackTimeUpdate(double realElapsed); - - // Convert a nonnegative real duration to an increment of track time - // relative to mT0. - double TrackDuration(double realElapsed) const; - - // Convert time between mT0 and argument to real duration, according to - // time track if one is given; result is always nonnegative - double RealDuration(double trackTime1) const; - - // How much real time left? - double RealTimeRemaining() const; - - // Advance the real time position - void RealTimeAdvance( double increment ); - - // Determine starting duration within the first pass -- sometimes not - // zero - void RealTimeInit( double trackTime ); - - void RealTimeRestart(); - - } mPlaybackSchedule; + RecordingSchedule mRecordingSchedule{}; // Another circular buffer // Holds track time values corresponding to every nth sample in the playback @@ -794,17 +547,17 @@ protected: }; -class AUDACITY_DLL_API AudioIO final : public AudioIoCallback { +class AUDACITY_DLL_API AudioIO final + : public AudioIoCallback +{ - public: AudioIO(); ~AudioIO(); +public: // This might return null during application startup or shutdown static AudioIO *Get(); -public: - AudioIOListener* GetListener() { return mListener; } void SetListener(AudioIOListener* listener); @@ -822,7 +575,7 @@ public: * Allocates buffers for recording and playback, gets the Audio thread to * fill them, and sets the stream rolling. * If successful, returns a token identifying this particular stream - * instance. For use with IsStreamActive() below */ + * instance. For use with IsStreamActive() */ int StartStream(const TransportTracks &tracks, double t0, double t1, @@ -855,21 +608,6 @@ public: double GetLastScrubTime() const; #endif - /** \brief Returns true if audio i/o is busy starting, stopping, playing, - * or recording. - * - * When this is false, it's safe to start playing or recording */ - bool IsBusy() const; - - /** \brief Returns true if the audio i/o is running at all, but not during - * cleanup - * - * Doesn't return true if the device has been closed but some disk i/o or - * cleanup is still going on. If you want to know if it's safe to start a - * NEW stream, use IsBusy() */ - bool IsStreamActive() const; - bool IsStreamActive(int token) const; - public: wxString LastPaErrorString(); @@ -899,17 +637,6 @@ public: bool GetHasSolo() { return mHasSolo; } #endif - /** \brief Returns true if the stream is active, or even if audio I/O is - * busy cleaning up its data or writing to disk. - * - * This is used by TrackPanel to determine when a track has been completely - * recorded, and it's safe to flush to disk. */ - bool IsAudioTokenActive(int token) const; - - /** \brief Returns true if we're monitoring input (but not recording or - * playing actual audio) */ - bool IsMonitoring() const; - /** \brief Pause and un-pause playback and recording */ void SetPaused(bool state); @@ -919,7 +646,6 @@ public: * with that stream. If no mixer is available, output is emulated and * input is stuck at 1.0f (a gain is applied to output samples). */ - void SetMixer(int inputSource); void SetMixer(int inputSource, float inputVolume, float playbackVolume); void GetMixer(int *inputSource, float *inputVolume, @@ -927,7 +653,7 @@ public: /** @brief Find out if the input hardware level control is available * * Checks the mInputMixerWorks variable, which is set up in - * AudioIO::HandleDeviceChange(). External people care, because we want to + * AudioIOBase::HandleDeviceChange(). External people care, because we want to * disable the UI if it doesn't work. */ bool InputMixerWorks(); @@ -935,7 +661,7 @@ public: /** @brief Find out if the output level control is being emulated via software attenuation * * Checks the mEmulateMixerOutputVol variable, which is set up in - * AudioIO::HandleDeviceChange(). External classes care, because we want to + * AudioIOBase::HandleDeviceChange(). External classes care, because we want to * modify the UI if it doesn't work. */ bool OutputMixerEmulated(); @@ -946,82 +672,6 @@ public: * soundcard mixer (driven by PortMixer) */ wxArrayString GetInputSourceNames(); - /** \brief update state after changing what audio devices are selected - * - * Called when the devices stored in the preferences are changed to update - * the audio mixer capabilities - * - * \todo: Make this do a sample rate query and store the result in the - * AudioIO object to avoid doing it later? Would simplify the - * GetSupported*Rate functions considerably */ - void HandleDeviceChange(); - - /** \brief Get a list of sample rates the output (playback) device - * supports. - * - * If no information about available sample rates can be fetched, - * an empty list is returned. - * - * You can explicitely give the index of the device. If you don't - * give it, the currently selected device from the preferences will be used. - * - * You may also specify a rate for which to check in addition to the - * standard rates. - */ - static std::vector GetSupportedPlaybackRates(int DevIndex = -1, - double rate = 0.0); - - /** \brief Get a list of sample rates the input (recording) device - * supports. - * - * If no information about available sample rates can be fetched, - * an empty list is returned. - * - * You can explicitely give the index of the device. If you don't - * give it, the currently selected device from the preferences will be used. - * - * You may also specify a rate for which to check in addition to the - * standard rates. - */ - static std::vector GetSupportedCaptureRates(int devIndex = -1, - double rate = 0.0); - - /** \brief Get a list of sample rates the current input/output device - * combination supports. - * - * Since there is no concept (yet) for different input/output - * sample rates, this currently returns only sample rates that are - * supported on both the output and input device. If no information - * about available sample rates can be fetched, it returns a default - * list. - * You can explicitely give the indexes of the playDevice/recDevice. - * If you don't give them, the selected devices from the preferences - * will be used. - * You may also specify a rate for which to check in addition to the - * standard rates. - */ - static std::vector GetSupportedSampleRates(int playDevice = -1, - int recDevice = -1, - double rate = 0.0); - - /** \brief Get a supported sample rate which can be used a an optimal - * default. - * - * Currently, this uses the first supported rate in the list - * [44100, 48000, highest sample rate]. Used in Project as a default value - * for project rates if one cannot be retrieved from the preferences. - * So all in all not that useful or important really - */ - static int GetOptimalSupportedSampleRate(); - - /** \brief During playback, the track time most recently played - * - * When playing looped, this will start from t0 again, - * too. So the returned time should be always between - * t0 and t1 - */ - double GetStreamTime(); - sampleFormat GetCaptureFormat() { return mCaptureFormat; } unsigned GetNumPlaybackChannels() const { return mNumPlaybackChannels; } unsigned GetNumCaptureChannels() const { return mNumCaptureChannels; } @@ -1029,24 +679,6 @@ public: // Meaning really capturing, not just pre-rolling bool IsCapturing() const; - /** \brief Array of common audio sample rates - * - * These are the rates we will always support, regardless of hardware support - * for them (by resampling in audacity if needed) */ - static const int StandardRates[]; - /** \brief How many standard sample rates there are */ - static const int NumStandardRates; - - /** \brief Get diagnostic information on all the available audio I/O devices - * - */ - wxString GetDeviceInfo(); - -#ifdef EXPERIMENTAL_MIDI_OUT - /** \brief Get diagnostic information on all the available MIDI I/O devices */ - wxString GetMidiDeviceInfo(); -#endif - /** \brief Ensure selected device names are valid * */ @@ -1065,8 +697,6 @@ public: #endif bool IsAvailable(AudacityProject *projecT) const; - void SetCaptureMeter(AudacityProject *project, MeterPanel *meter); - void SetPlaybackMeter(AudacityProject *project, MeterPanel *meter); /** \brief Return a valid sample rate that is supported by the current I/O * device(s). @@ -1085,12 +715,13 @@ public: friend class MidiThread; #endif - friend void InitAudioIO(); - + static void Init(); + static void Deinit(); private: + /** \brief Set the current VU meters - this should be done once after * each call to StartStream currently */ void SetMeters(); @@ -1142,39 +773,6 @@ private: * all record buffers without underflow). */ size_t GetCommonlyAvailCapture(); - /** \brief get the index of the supplied (named) recording device, or the - * device selected in the preferences if none given. - * - * Pure utility function, but it comes round a number of times in the code - * and would be neater done once. If the device isn't found, return the - * default device index. - */ - static int getRecordDevIndex(const wxString &devName = {}); - /** \brief get the index of the device selected in the preferences. - * - * If the device isn't found, returns -1 - */ -#if USE_PORTMIXER - static int getRecordSourceIndex(PxMixer *portMixer); -#endif - - /** \brief get the index of the supplied (named) playback device, or the - * device selected in the preferences if none given. - * - * Pure utility function, but it comes round a number of times in the code - * and would be neater done once. If the device isn't found, return the - * default device index. - */ - static int getPlayDevIndex(const wxString &devName = {}); - - /** \brief Array of audio sample rates to try to use - * - * These are the rates we will check if a device supports, and is as long - * as I can think of (to try and work out what the card can do) */ - static const int RatesToTry[]; - /** \brief How many sample rates to try */ - static const int NumRatesToTry; - /** \brief Allocate RingBuffer structures, and others, needed for playback * and recording. * @@ -1191,6 +789,4 @@ private: void StartStreamCleanup(bool bOnlyBuffers = false); }; -using AudioIOBase = AudioIO; - #endif diff --git a/src/AudioIOBase.cpp b/src/AudioIOBase.cpp index e69de29bb..e4c803e58 100644 --- a/src/AudioIOBase.cpp +++ b/src/AudioIOBase.cpp @@ -0,0 +1,1351 @@ +/********************************************************************** + +Audacity: A Digital Audio Editor + +AudioIOBase.cpp + +Paul Licameli split from AudioIO.cpp + +**********************************************************************/ + +#include "Audacity.h" +#include "AudioIOBase.h" + +#include "Experimental.h" + +#include +#include + +#include "Envelope.h" +#include "prefs/RecordingPrefs.h" +#include "widgets/Meter.h" + +#if USE_PORTMIXER +#include "portmixer.h" +#endif + +#ifdef EXPERIMENTAL_MIDI_OUT +#include "../lib-src/portmidi/pm_common/portmidi.h" +#endif + +int AudioIOBase::mCachedPlaybackIndex = -1; +std::vector AudioIOBase::mCachedPlaybackRates; +int AudioIOBase::mCachedCaptureIndex = -1; +std::vector AudioIOBase::mCachedCaptureRates; +std::vector AudioIOBase::mCachedSampleRates; +double AudioIOBase::mCachedBestRateIn = 0.0; + +const int AudioIOBase::StandardRates[] = { + 8000, + 11025, + 16000, + 22050, + 32000, + 44100, + 48000, + 88200, + 96000, + 176400, + 192000, + 352800, + 384000 +}; + +const int AudioIOBase::NumStandardRates = WXSIZEOF(AudioIOBase::StandardRates); + +const int AudioIOBase::RatesToTry[] = { + 8000, + 9600, + 11025, + 12000, + 15000, + 16000, + 22050, + 24000, + 32000, + 44100, + 48000, + 88200, + 96000, + 176400, + 192000, + 352800, + 384000 +}; +const int AudioIOBase::NumRatesToTry = WXSIZEOF(AudioIOBase::RatesToTry); + +wxString AudioIOBase::DeviceName(const PaDeviceInfo* info) +{ + wxString infoName = wxSafeConvertMB2WX(info->name); + + return infoName; +} + +wxString AudioIOBase::HostName(const PaDeviceInfo* info) +{ + wxString hostapiName = wxSafeConvertMB2WX(Pa_GetHostApiInfo(info->hostApi)->name); + + return hostapiName; +} + +std::unique_ptr AudioIOBase::ugAudioIO; + +AudioIOBase *AudioIOBase::Get() +{ + return ugAudioIO.get(); +} + +void AudioIOBase::SetMixer(int inputSource) +{ +#if defined(USE_PORTMIXER) + int oldRecordSource = Px_GetCurrentInputSource(mPortMixer); + if ( inputSource != oldRecordSource ) + Px_SetCurrentInputSource(mPortMixer, inputSource); +#endif +} + +void AudioIOBase::HandleDeviceChange() +{ + // This should not happen, but it would screw things up if it did. + // Vaughan, 2010-10-08: But it *did* happen, due to a bug, and nobody + // caught it because this method just returned. Added wxASSERT(). + wxASSERT(!IsStreamActive()); + if (IsStreamActive()) + return; + + // get the selected record and playback devices + const int playDeviceNum = getPlayDevIndex(); + const int recDeviceNum = getRecordDevIndex(); + + // If no change needed, return + if (mCachedPlaybackIndex == playDeviceNum && + mCachedCaptureIndex == recDeviceNum) + return; + + // cache playback/capture rates + mCachedPlaybackRates = GetSupportedPlaybackRates(playDeviceNum); + mCachedCaptureRates = GetSupportedCaptureRates(recDeviceNum); + mCachedSampleRates = GetSupportedSampleRates(playDeviceNum, recDeviceNum); + mCachedPlaybackIndex = playDeviceNum; + mCachedCaptureIndex = recDeviceNum; + mCachedBestRateIn = 0.0; + +#if defined(USE_PORTMIXER) + + // if we have a PortMixer object, close it down + if (mPortMixer) { + #if __WXMAC__ + // on the Mac we must make sure that we restore the hardware playthrough + // state of the sound device to what it was before, because there isn't + // a UI for this (!) + if (Px_SupportsPlaythrough(mPortMixer) && mPreviousHWPlaythrough >= 0.0) + Px_SetPlaythrough(mPortMixer, mPreviousHWPlaythrough); + mPreviousHWPlaythrough = -1.0; + #endif + Px_CloseMixer(mPortMixer); + mPortMixer = NULL; + } + + // that might have given us no rates whatsoever, so we have to guess an + // answer to do the next bit + int numrates = mCachedSampleRates.size(); + int highestSampleRate; + if (numrates > 0) + { + highestSampleRate = mCachedSampleRates[numrates - 1]; + } + else + { // we don't actually have any rates that work for Rec and Play. Guess one + // to use for messing with the mixer, which doesn't actually do either + highestSampleRate = 44100; + // mCachedSampleRates is still empty, but it's not used again, so + // can ignore + } + mInputMixerWorks = false; + mEmulateMixerOutputVol = true; + mMixerOutputVol = 1.0; + + int error; + // This tries to open the device with the samplerate worked out above, which + // will be the highest available for play and record on the device, or + // 44.1kHz if the info cannot be fetched. + + PaStream *stream; + + PaStreamParameters playbackParameters; + + playbackParameters.device = playDeviceNum; + playbackParameters.sampleFormat = paFloat32; + playbackParameters.hostApiSpecificStreamInfo = NULL; + playbackParameters.channelCount = 1; + if (Pa_GetDeviceInfo(playDeviceNum)) + playbackParameters.suggestedLatency = + Pa_GetDeviceInfo(playDeviceNum)->defaultLowOutputLatency; + else + playbackParameters.suggestedLatency = DEFAULT_LATENCY_CORRECTION/1000.0; + + PaStreamParameters captureParameters; + + captureParameters.device = recDeviceNum; + captureParameters.sampleFormat = paFloat32;; + captureParameters.hostApiSpecificStreamInfo = NULL; + captureParameters.channelCount = 1; + if (Pa_GetDeviceInfo(recDeviceNum)) + captureParameters.suggestedLatency = + Pa_GetDeviceInfo(recDeviceNum)->defaultLowInputLatency; + else + captureParameters.suggestedLatency = DEFAULT_LATENCY_CORRECTION/1000.0; + + // try opening for record and playback + // Not really doing I/O so pass nullptr for the callback function + error = Pa_OpenStream(&stream, + &captureParameters, &playbackParameters, + highestSampleRate, paFramesPerBufferUnspecified, + paClipOff | paDitherOff, + nullptr, NULL); + + if (!error) { + // Try portmixer for this stream + mPortMixer = Px_OpenMixer(stream, 0); + if (!mPortMixer) { + Pa_CloseStream(stream); + error = true; + } + } + + // if that failed, try just for record + if( error ) { + error = Pa_OpenStream(&stream, + &captureParameters, NULL, + highestSampleRate, paFramesPerBufferUnspecified, + paClipOff | paDitherOff, + nullptr, NULL); + + if (!error) { + mPortMixer = Px_OpenMixer(stream, 0); + if (!mPortMixer) { + Pa_CloseStream(stream); + error = true; + } + } + } + + // finally, try just for playback + if ( error ) { + error = Pa_OpenStream(&stream, + NULL, &playbackParameters, + highestSampleRate, paFramesPerBufferUnspecified, + paClipOff | paDitherOff, + nullptr, NULL); + + if (!error) { + mPortMixer = Px_OpenMixer(stream, 0); + if (!mPortMixer) { + Pa_CloseStream(stream); + error = true; + } + } + } + + // FIXME: TRAP_ERR errors in HandleDeviceChange not reported. + // if it's still not working, give up + if( error ) + return; + + // Set input source +#if USE_PORTMIXER + int sourceIndex; + if (gPrefs->Read(wxT("/AudioIO/RecordingSourceIndex"), &sourceIndex)) { + if (sourceIndex >= 0) { + //the current index of our source may be different because the stream + //is a combination of two devices, so update it. + sourceIndex = getRecordSourceIndex(mPortMixer); + if (sourceIndex >= 0) + SetMixer(sourceIndex); + } + } +#endif + + // Determine mixer capabilities - if it doesn't support control of output + // signal level, we emulate it (by multiplying this value by all outgoing + // samples) + + mMixerOutputVol = Px_GetPCMOutputVolume(mPortMixer); + mEmulateMixerOutputVol = false; + Px_SetPCMOutputVolume(mPortMixer, 0.0); + if (Px_GetPCMOutputVolume(mPortMixer) > 0.1) + mEmulateMixerOutputVol = true; + Px_SetPCMOutputVolume(mPortMixer, 0.2f); + if (Px_GetPCMOutputVolume(mPortMixer) < 0.1 || + Px_GetPCMOutputVolume(mPortMixer) > 0.3) + mEmulateMixerOutputVol = true; + Px_SetPCMOutputVolume(mPortMixer, mMixerOutputVol); + + float inputVol = Px_GetInputVolume(mPortMixer); + mInputMixerWorks = true; // assume it works unless proved wrong + Px_SetInputVolume(mPortMixer, 0.0); + if (Px_GetInputVolume(mPortMixer) > 0.1) + mInputMixerWorks = false; // can't set to zero + Px_SetInputVolume(mPortMixer, 0.2f); + if (Px_GetInputVolume(mPortMixer) < 0.1 || + Px_GetInputVolume(mPortMixer) > 0.3) + mInputMixerWorks = false; // can't set level accurately + Px_SetInputVolume(mPortMixer, inputVol); + + Pa_CloseStream(stream); + + + #if 0 + wxPrintf("PortMixer: Playback: %s Recording: %s\n", + mEmulateMixerOutputVol? "emulated": "native", + mInputMixerWorks? "hardware": "no control"); + #endif + + mMixerOutputVol = 1.0; + +#endif // USE_PORTMIXER +} + +void AudioIOBase::SetCaptureMeter(AudacityProject *project, MeterPanel *meter) +{ + if (( mOwningProject ) && ( mOwningProject != project)) + return; + + if (meter) + { + mInputMeter = meter; + mInputMeter->Reset(mRate, true); + } + else + mInputMeter.Release(); +} + +void AudioIOBase::SetPlaybackMeter(AudacityProject *project, MeterPanel *meter) +{ + if (( mOwningProject ) && ( mOwningProject != project)) + return; + + if (meter) + { + mOutputMeter = meter; + mOutputMeter->Reset(mRate, true); + } + else + mOutputMeter.Release(); +} + +bool AudioIOBase::IsPaused() const +{ + return mPaused; +} + +bool AudioIOBase::IsBusy() const +{ + if (mStreamToken != 0) + return true; + + return false; +} + +bool AudioIOBase::IsStreamActive() const +{ + bool isActive = false; + // JKC: Not reporting any Pa error, but that looks OK. + if( mPortStreamV19 ) + isActive = (Pa_IsStreamActive( mPortStreamV19 ) > 0); + +#ifdef EXPERIMENTAL_MIDI_OUT + if( mMidiStreamActive && !mMidiOutputComplete ) + isActive = true; +#endif + return isActive; +} + +bool AudioIOBase::IsStreamActive(int token) const +{ + return (this->IsStreamActive() && this->IsAudioTokenActive(token)); +} + +bool AudioIOBase::IsAudioTokenActive(int token) const +{ + return ( token > 0 && token == mStreamToken ); +} + +bool AudioIOBase::IsMonitoring() const +{ + return ( mPortStreamV19 && mStreamToken==0 ); +} + +void AudioIOBase::PlaybackSchedule::Init( + const double t0, const double t1, + const AudioIOStartStreamOptions &options, + const RecordingSchedule *pRecordingSchedule ) +{ + if ( pRecordingSchedule ) + // It does not make sense to apply the time warp during overdub recording, + // which defeats the purpose of making the recording synchronized with + // the existing audio. (Unless we figured out the inverse warp of the + // captured samples in real time.) + // So just quietly ignore the time track. + mEnvelope = nullptr; + else + mEnvelope = options.envelope; + + mT0 = t0; + if (pRecordingSchedule) + mT0 -= pRecordingSchedule->mPreRoll; + + mT1 = t1; + if (pRecordingSchedule) + // adjust mT1 so that we don't give paComplete too soon to fill up the + // desired length of recording + mT1 -= pRecordingSchedule->mLatencyCorrection; + + // Main thread's initialization of mTime + SetTrackTime( mT0 ); + + mPlayMode = options.playLooped + ? PlaybackSchedule::PLAY_LOOPED + : PlaybackSchedule::PLAY_STRAIGHT; + mCutPreviewGapStart = options.cutPreviewGapStart; + mCutPreviewGapLen = options.cutPreviewGapLen; + +#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT + bool scrubbing = (options.pScrubbingOptions != nullptr); + + // Scrubbing is not compatible with looping or recording or a time track! + if (scrubbing) + { + const auto &scrubOptions = *options.pScrubbingOptions; + if (pRecordingSchedule || + Looping() || + mEnvelope || + scrubOptions.maxSpeed < ScrubbingOptions::MinAllowedScrubSpeed()) { + wxASSERT(false); + scrubbing = false; + } + else + mPlayMode = (scrubOptions.isPlayingAtSpeed) + ? PlaybackSchedule::PLAY_AT_SPEED + : PlaybackSchedule::PLAY_SCRUB; + } +#endif + + mWarpedTime = 0.0; +#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT + if (Scrubbing()) + mWarpedLength = 0.0f; + else +#endif + mWarpedLength = RealDuration(mT1); +} + +double AudioIOBase::PlaybackSchedule::LimitTrackTime() const +{ + // Track time readout for the main thread + // Allows for forward or backward play + return ClampTrackTime( GetTrackTime() ); +} + +double AudioIOBase::PlaybackSchedule::ClampTrackTime( double trackTime ) const +{ + if (ReversedTime()) + return std::max(mT1, std::min(mT0, trackTime)); + else + return std::max(mT0, std::min(mT1, trackTime)); +} + +double AudioIOBase::PlaybackSchedule::NormalizeTrackTime() const +{ + // Track time readout for the main thread + + // dmazzoni: This function is needed for two reasons: + // One is for looped-play mode - this function makes sure that the + // position indicator keeps wrapping around. The other reason is + // more subtle - it's because PortAudio can query the hardware for + // the current stream time, and this query is not always accurate. + // Sometimes it's a little behind or ahead, and so this function + // makes sure that at least we clip it to the selection. + // + // msmeyer: There is also the possibility that we are using "cut preview" + // mode. In this case, we should jump over a defined "gap" in the + // audio. + + double absoluteTime; + +#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 (Interactive()) + absoluteTime = GetTrackTime(); + else +#endif + absoluteTime = LimitTrackTime(); + + if (mCutPreviewGapLen > 0) + { + // msmeyer: We're in cut preview mode, so if we are on the right + // side of the gap, we jump over it. + if (absoluteTime > mCutPreviewGapStart) + absoluteTime += mCutPreviewGapLen; + } + + return absoluteTime; +} + +double AudioIOBase::GetStreamTime() +{ + // Track time readout for the main thread + + if( !IsStreamActive() ) + return BAD_STREAM_TIME; + + return mPlaybackSchedule.NormalizeTrackTime(); +} + +std::vector AudioIOBase::GetSupportedPlaybackRates(int devIndex, double rate) +{ + if (devIndex == -1) + { // weren't given a device index, get the prefs / default one + devIndex = getPlayDevIndex(); + } + + // Check if we can use the cached rates + if (mCachedPlaybackIndex != -1 && devIndex == mCachedPlaybackIndex + && (rate == 0.0 || make_iterator_range(mCachedPlaybackRates).contains(rate))) + { + return mCachedPlaybackRates; + } + + std::vector supported; + int irate = (int)rate; + const PaDeviceInfo* devInfo = NULL; + int i; + + devInfo = Pa_GetDeviceInfo(devIndex); + + if (!devInfo) + { + wxLogDebug(wxT("GetSupportedPlaybackRates() Could not get device info!")); + return supported; + } + + // LLL: Remove when a proper method of determining actual supported + // DirectSound rate is devised. + const PaHostApiInfo* hostInfo = Pa_GetHostApiInfo(devInfo->hostApi); + bool isDirectSound = (hostInfo && hostInfo->type == paDirectSound); + + PaStreamParameters pars; + + pars.device = devIndex; + pars.channelCount = 1; + pars.sampleFormat = paFloat32; + pars.suggestedLatency = devInfo->defaultHighOutputLatency; + pars.hostApiSpecificStreamInfo = NULL; + + // JKC: PortAudio Errors handled OK here. No need to report them + for (i = 0; i < NumRatesToTry; i++) + { + // LLL: Remove when a proper method of determining actual supported + // DirectSound rate is devised. + if (!(isDirectSound && RatesToTry[i] > 200000)){ + if (Pa_IsFormatSupported(NULL, &pars, RatesToTry[i]) == 0) + supported.push_back(RatesToTry[i]); + Pa_Sleep( 10 );// There are ALSA drivers that don't like being probed + // too quickly. + } + } + + if (irate != 0 && !make_iterator_range(supported).contains(irate)) + { + // LLL: Remove when a proper method of determining actual supported + // DirectSound rate is devised. + if (!(isDirectSound && RatesToTry[i] > 200000)) + if (Pa_IsFormatSupported(NULL, &pars, irate) == 0) + supported.push_back(irate); + } + + return supported; +} + +std::vector AudioIOBase::GetSupportedCaptureRates(int devIndex, double rate) +{ + if (devIndex == -1) + { // not given a device, look up in prefs / default + devIndex = getRecordDevIndex(); + } + + // Check if we can use the cached rates + if (mCachedCaptureIndex != -1 && devIndex == mCachedCaptureIndex + && (rate == 0.0 || make_iterator_range(mCachedCaptureRates).contains(rate))) + { + return mCachedCaptureRates; + } + + std::vector supported; + int irate = (int)rate; + const PaDeviceInfo* devInfo = NULL; + int i; + + devInfo = Pa_GetDeviceInfo(devIndex); + + if (!devInfo) + { + wxLogDebug(wxT("GetSupportedCaptureRates() Could not get device info!")); + return supported; + } + + double latencyDuration = DEFAULT_LATENCY_DURATION; + long recordChannels = 1; + gPrefs->Read(wxT("/AudioIO/LatencyDuration"), &latencyDuration); + gPrefs->Read(wxT("/AudioIO/RecordChannels"), &recordChannels); + + // LLL: Remove when a proper method of determining actual supported + // DirectSound rate is devised. + const PaHostApiInfo* hostInfo = Pa_GetHostApiInfo(devInfo->hostApi); + bool isDirectSound = (hostInfo && hostInfo->type == paDirectSound); + + PaStreamParameters pars; + + pars.device = devIndex; + pars.channelCount = recordChannels; + pars.sampleFormat = paFloat32; + pars.suggestedLatency = latencyDuration / 1000.0; + pars.hostApiSpecificStreamInfo = NULL; + + for (i = 0; i < NumRatesToTry; i++) + { + // LLL: Remove when a proper method of determining actual supported + // DirectSound rate is devised. + if (!(isDirectSound && RatesToTry[i] > 200000)) + { + if (Pa_IsFormatSupported(&pars, NULL, RatesToTry[i]) == 0) + supported.push_back(RatesToTry[i]); + Pa_Sleep( 10 );// There are ALSA drivers that don't like being probed + // too quickly. + } + } + + if (irate != 0 && !make_iterator_range(supported).contains(irate)) + { + // LLL: Remove when a proper method of determining actual supported + // DirectSound rate is devised. + if (!(isDirectSound && RatesToTry[i] > 200000)) + if (Pa_IsFormatSupported(&pars, NULL, irate) == 0) + supported.push_back(irate); + } + + return supported; +} + +std::vector AudioIOBase::GetSupportedSampleRates( + int playDevice, int recDevice, double rate) +{ + // Not given device indices, look up prefs + if (playDevice == -1) { + playDevice = getPlayDevIndex(); + } + if (recDevice == -1) { + recDevice = getRecordDevIndex(); + } + + // Check if we can use the cached rates + if (mCachedPlaybackIndex != -1 && mCachedCaptureIndex != -1 && + playDevice == mCachedPlaybackIndex && + recDevice == mCachedCaptureIndex && + (rate == 0.0 || make_iterator_range(mCachedSampleRates).contains(rate))) + { + return mCachedSampleRates; + } + + auto playback = GetSupportedPlaybackRates(playDevice, rate); + auto capture = GetSupportedCaptureRates(recDevice, rate); + int i; + + // Return only sample rates which are in both arrays + std::vector result; + + for (i = 0; i < (int)playback.size(); i++) + if (make_iterator_range(capture).contains(playback[i])) + result.push_back(playback[i]); + + // If this yields no results, use the default sample rates nevertheless +/* if (result.empty()) + { + for (i = 0; i < NumStandardRates; i++) + result.push_back(StandardRates[i]); + }*/ + + return result; +} + +/** \todo: should this take into account PortAudio's value for + * PaDeviceInfo::defaultSampleRate? In principal this should let us work out + * which rates are "real" and which resampled in the drivers, and so prefer + * the real rates. */ +int AudioIOBase::GetOptimalSupportedSampleRate() +{ + auto rates = GetSupportedSampleRates(); + + if (make_iterator_range(rates).contains(44100)) + return 44100; + + if (make_iterator_range(rates).contains(48000)) + return 48000; + + // if there are no supported rates, the next bit crashes. So check first, + // and give them a "sensible" value if there are no valid values. They + // will still get an error later, but with any luck may have changed + // something by then. It's no worse than having an invalid default rate + // stored in the preferences, which we don't check for + if (rates.empty()) return 44100; + + return rates.back(); +} + +#if USE_PORTMIXER +int AudioIOBase::getRecordSourceIndex(PxMixer *portMixer) +{ + int i; + wxString sourceName = gPrefs->Read(wxT("/AudioIO/RecordingSource"), wxT("")); + int numSources = Px_GetNumInputSources(portMixer); + for (i = 0; i < numSources; i++) { + if (sourceName == wxString(wxSafeConvertMB2WX(Px_GetInputSourceName(portMixer, i)))) + return i; + } + return -1; +} +#endif + +int AudioIOBase::getPlayDevIndex(const wxString &devNameArg) +{ + wxString devName(devNameArg); + // if we don't get given a device, look up the preferences + if (devName.empty()) + { + devName = gPrefs->Read(wxT("/AudioIO/PlaybackDevice"), wxT("")); + } + + wxString hostName = gPrefs->Read(wxT("/AudioIO/Host"), wxT("")); + PaHostApiIndex hostCnt = Pa_GetHostApiCount(); + PaHostApiIndex hostNum; + for (hostNum = 0; hostNum < hostCnt; hostNum++) + { + const PaHostApiInfo *hinfo = Pa_GetHostApiInfo(hostNum); + if (hinfo && wxString(wxSafeConvertMB2WX(hinfo->name)) == hostName) + { + for (PaDeviceIndex hostDevice = 0; hostDevice < hinfo->deviceCount; hostDevice++) + { + PaDeviceIndex deviceNum = Pa_HostApiDeviceIndexToDeviceIndex(hostNum, hostDevice); + + const PaDeviceInfo *dinfo = Pa_GetDeviceInfo(deviceNum); + if (dinfo && DeviceName(dinfo) == devName && dinfo->maxOutputChannels > 0 ) + { + // this device name matches the stored one, and works. + // So we say this is the answer and return it + return deviceNum; + } + } + + // The device wasn't found so use the default for this host. + // LL: At this point, preferences and active no longer match. + return hinfo->defaultOutputDevice; + } + } + + // The host wasn't found, so use the default output device. + // FIXME: TRAP_ERR PaErrorCode not handled well (this code is similar to input code + // and the input side has more comments.) + + PaDeviceIndex deviceNum = Pa_GetDefaultOutputDevice(); + + // Sometimes PortAudio returns -1 if it cannot find a suitable default + // device, so we just use the first one available + // + // LL: At this point, preferences and active no longer match + // + // And I can't imagine how far we'll get specifying an "invalid" index later + // on...are we certain "0" even exists? + if (deviceNum < 0) { + wxASSERT(false); + deviceNum = 0; + } + + return deviceNum; +} + +int AudioIOBase::getRecordDevIndex(const wxString &devNameArg) +{ + wxString devName(devNameArg); + // if we don't get given a device, look up the preferences + if (devName.empty()) + { + devName = gPrefs->Read(wxT("/AudioIO/RecordingDevice"), wxT("")); + } + + wxString hostName = gPrefs->Read(wxT("/AudioIO/Host"), wxT("")); + PaHostApiIndex hostCnt = Pa_GetHostApiCount(); + PaHostApiIndex hostNum; + for (hostNum = 0; hostNum < hostCnt; hostNum++) + { + const PaHostApiInfo *hinfo = Pa_GetHostApiInfo(hostNum); + if (hinfo && wxString(wxSafeConvertMB2WX(hinfo->name)) == hostName) + { + for (PaDeviceIndex hostDevice = 0; hostDevice < hinfo->deviceCount; hostDevice++) + { + PaDeviceIndex deviceNum = Pa_HostApiDeviceIndexToDeviceIndex(hostNum, hostDevice); + + const PaDeviceInfo *dinfo = Pa_GetDeviceInfo(deviceNum); + if (dinfo && DeviceName(dinfo) == devName && dinfo->maxInputChannels > 0 ) + { + // this device name matches the stored one, and works. + // So we say this is the answer and return it + return deviceNum; + } + } + + // The device wasn't found so use the default for this host. + // LL: At this point, preferences and active no longer match. + return hinfo->defaultInputDevice; + } + } + + // The host wasn't found, so use the default input device. + // FIXME: TRAP_ERR PaErrorCode not handled well in getRecordDevIndex() + PaDeviceIndex deviceNum = Pa_GetDefaultInputDevice(); + + // Sometimes PortAudio returns -1 if it cannot find a suitable default + // device, so we just use the first one available + // PortAudio has an error reporting function. We should log/report the error? + // + // LL: At this point, preferences and active no longer match + // + // And I can't imagine how far we'll get specifying an "invalid" index later + // on...are we certain "0" even exists? + if (deviceNum < 0) { + // JKC: This ASSERT will happen if you run with no config file + // This happens once. Config file will exist on the next run. + // TODO: Look into this a bit more. Could be relevant to blank Device Toolbar. + wxASSERT(false); + deviceNum = 0; + } + + return deviceNum; +} + +wxString AudioIOBase::GetDeviceInfo() +{ + wxStringOutputStream o; + wxTextOutputStream s(o, wxEOL_UNIX); + + if (IsStreamActive()) { + return _("Stream is active ... unable to gather information.\n"); + } + + + // FIXME: TRAP_ERR PaErrorCode not handled. 3 instances in GetDeviceInfo(). + int recDeviceNum = Pa_GetDefaultInputDevice(); + int playDeviceNum = Pa_GetDefaultOutputDevice(); + int cnt = Pa_GetDeviceCount(); + + // PRL: why only into the log? + wxLogDebug(wxT("Portaudio reports %d audio devices"),cnt); + + s << wxT("==============================\n"); + s << wxString::Format(_("Default recording device number: %d\n"), recDeviceNum); + s << wxString::Format(_("Default playback device number: %d\n"), playDeviceNum); + + wxString recDevice = gPrefs->Read(wxT("/AudioIO/RecordingDevice"), wxT("")); + wxString playDevice = gPrefs->Read(wxT("/AudioIO/PlaybackDevice"), wxT("")); + int j; + + // This gets info on all available audio devices (input and output) + if (cnt <= 0) { + s << _("No devices found\n"); + return o.GetString(); + } + + const PaDeviceInfo* info; + + for (j = 0; j < cnt; j++) { + s << wxT("==============================\n"); + + info = Pa_GetDeviceInfo(j); + if (!info) { + s << wxString::Format(_("Device info unavailable for: %d\n"), j); + continue; + } + + wxString name = DeviceName(info); + s << wxString::Format(_("Device ID: %d\n"), j); + s << wxString::Format(_("Device name: %s\n"), name); + s << wxString::Format(_("Host name: %s\n"), HostName(info)); + s << wxString::Format(_("Recording channels: %d\n"), info->maxInputChannels); + s << wxString::Format(_("Playback channels: %d\n"), info->maxOutputChannels); + s << wxString::Format(_("Low Recording Latency: %g\n"), info->defaultLowInputLatency); + s << wxString::Format(_("Low Playback Latency: %g\n"), info->defaultLowOutputLatency); + s << wxString::Format(_("High Recording Latency: %g\n"), info->defaultHighInputLatency); + s << wxString::Format(_("High Playback Latency: %g\n"), info->defaultHighOutputLatency); + + auto rates = GetSupportedPlaybackRates(j, 0.0); + + /* i18n-hint: Supported, meaning made available by the system */ + s << _("Supported Rates:\n"); + for (int k = 0; k < (int) rates.size(); k++) { + s << wxT(" ") << (int)rates[k] << wxT("\n"); + } + + if (name == playDevice && info->maxOutputChannels > 0) + playDeviceNum = j; + + if (name == recDevice && info->maxInputChannels > 0) + recDeviceNum = j; + + // Sometimes PortAudio returns -1 if it cannot find a suitable default + // device, so we just use the first one available + if (recDeviceNum < 0 && info->maxInputChannels > 0){ + recDeviceNum = j; + } + if (playDeviceNum < 0 && info->maxOutputChannels > 0){ + playDeviceNum = j; + } + } + + bool haveRecDevice = (recDeviceNum >= 0); + bool havePlayDevice = (playDeviceNum >= 0); + + s << wxT("==============================\n"); + if (haveRecDevice) + s << wxString::Format(_("Selected recording device: %d - %s\n"), recDeviceNum, recDevice); + else + s << wxString::Format(_("No recording device found for '%s'.\n"), recDevice); + + if (havePlayDevice) + s << wxString::Format(_("Selected playback device: %d - %s\n"), playDeviceNum, playDevice); + else + s << wxString::Format(_("No playback device found for '%s'.\n"), playDevice); + + std::vector supportedSampleRates; + + if (havePlayDevice && haveRecDevice) { + supportedSampleRates = GetSupportedSampleRates(playDeviceNum, recDeviceNum); + + s << _("Supported Rates:\n"); + for (int k = 0; k < (int) supportedSampleRates.size(); k++) { + s << wxT(" ") << (int)supportedSampleRates[k] << wxT("\n"); + } + } + else { + s << _("Cannot check mutual sample rates without both devices.\n"); + return o.GetString(); + } + +#if defined(USE_PORTMIXER) + if (supportedSampleRates.size() > 0) + { + int highestSampleRate = supportedSampleRates.back(); + bool EmulateMixerInputVol = true; + bool EmulateMixerOutputVol = true; + float MixerInputVol = 1.0; + float MixerOutputVol = 1.0; + + int error; + + PaStream *stream; + + PaStreamParameters playbackParameters; + + playbackParameters.device = playDeviceNum; + playbackParameters.sampleFormat = paFloat32; + playbackParameters.hostApiSpecificStreamInfo = NULL; + playbackParameters.channelCount = 1; + if (Pa_GetDeviceInfo(playDeviceNum)){ + playbackParameters.suggestedLatency = + Pa_GetDeviceInfo(playDeviceNum)->defaultLowOutputLatency; + } + else{ + playbackParameters.suggestedLatency = DEFAULT_LATENCY_CORRECTION/1000.0; + } + + PaStreamParameters captureParameters; + + captureParameters.device = recDeviceNum; + captureParameters.sampleFormat = paFloat32;; + captureParameters.hostApiSpecificStreamInfo = NULL; + captureParameters.channelCount = 1; + if (Pa_GetDeviceInfo(recDeviceNum)){ + captureParameters.suggestedLatency = + Pa_GetDeviceInfo(recDeviceNum)->defaultLowInputLatency; + }else{ + captureParameters.suggestedLatency = DEFAULT_LATENCY_CORRECTION/1000.0; + } + + // Not really doing I/O so pass nullptr for the callback function + error = Pa_OpenStream(&stream, + &captureParameters, &playbackParameters, + highestSampleRate, paFramesPerBufferUnspecified, + paClipOff | paDitherOff, + nullptr, NULL); + + if (error) { + error = Pa_OpenStream(&stream, + &captureParameters, NULL, + highestSampleRate, paFramesPerBufferUnspecified, + paClipOff | paDitherOff, + nullptr, NULL); + } + + if (error) { + s << wxString::Format(_("Received %d while opening devices\n"), error); + return o.GetString(); + } + + PxMixer *PortMixer = Px_OpenMixer(stream, 0); + + if (!PortMixer) { + s << _("Unable to open Portmixer\n"); + Pa_CloseStream(stream); + return o.GetString(); + } + + s << wxT("==============================\n"); + s << _("Available mixers:\n"); + + // FIXME: ? PortMixer errors on query not reported in GetDeviceInfo + cnt = Px_GetNumMixers(stream); + for (int i = 0; i < cnt; i++) { + wxString name = wxSafeConvertMB2WX(Px_GetMixerName(stream, i)); + s << wxString::Format(_("%d - %s\n"), i, name); + } + + s << wxT("==============================\n"); + s << _("Available recording sources:\n"); + cnt = Px_GetNumInputSources(PortMixer); + for (int i = 0; i < cnt; i++) { + wxString name = wxSafeConvertMB2WX(Px_GetInputSourceName(PortMixer, i)); + s << wxString::Format(_("%d - %s\n"), i, name); + } + + s << wxT("==============================\n"); + s << _("Available playback volumes:\n"); + cnt = Px_GetNumOutputVolumes(PortMixer); + for (int i = 0; i < cnt; i++) { + wxString name = wxSafeConvertMB2WX(Px_GetOutputVolumeName(PortMixer, i)); + s << wxString::Format(_("%d - %s\n"), i, name); + } + + // Determine mixer capabilities - it it doesn't support either + // input or output, we emulate them (by multiplying this value + // by all incoming/outgoing samples) + + MixerOutputVol = Px_GetPCMOutputVolume(PortMixer); + EmulateMixerOutputVol = false; + Px_SetPCMOutputVolume(PortMixer, 0.0); + if (Px_GetPCMOutputVolume(PortMixer) > 0.1) + EmulateMixerOutputVol = true; + Px_SetPCMOutputVolume(PortMixer, 0.2f); + if (Px_GetPCMOutputVolume(PortMixer) < 0.1 || + Px_GetPCMOutputVolume(PortMixer) > 0.3) + EmulateMixerOutputVol = true; + Px_SetPCMOutputVolume(PortMixer, MixerOutputVol); + + MixerInputVol = Px_GetInputVolume(PortMixer); + EmulateMixerInputVol = false; + Px_SetInputVolume(PortMixer, 0.0); + if (Px_GetInputVolume(PortMixer) > 0.1) + EmulateMixerInputVol = true; + Px_SetInputVolume(PortMixer, 0.2f); + if (Px_GetInputVolume(PortMixer) < 0.1 || + Px_GetInputVolume(PortMixer) > 0.3) + EmulateMixerInputVol = true; + Px_SetInputVolume(PortMixer, MixerInputVol); + + Pa_CloseStream(stream); + + s << wxT("==============================\n"); + s << ( EmulateMixerInputVol + ? _("Recording volume is emulated\n") + : _("Recording volume is native\n") ); + s << ( EmulateMixerOutputVol + ? _("Playback volume is emulated\n") + : _("Playback volume is native\n") ); + + Px_CloseMixer(PortMixer); + + } //end of massive if statement if a valid sample rate has been found +#endif + return o.GetString(); +} + +#ifdef EXPERIMENTAL_MIDI_OUT +// FIXME: When EXPERIMENTAL_MIDI_IN is added (eventually) this should also be enabled -- Poke +wxString AudioIOBase::GetMidiDeviceInfo() +{ + wxStringOutputStream o; + wxTextOutputStream s(o, wxEOL_UNIX); + + if (IsStreamActive()) { + return _("Stream is active ... unable to gather information.\n"); + } + + + // XXX: May need to trap errors as with the normal device info + int recDeviceNum = Pm_GetDefaultInputDeviceID(); + int playDeviceNum = Pm_GetDefaultOutputDeviceID(); + int cnt = Pm_CountDevices(); + + // PRL: why only into the log? + wxLogDebug(wxT("PortMidi reports %d MIDI devices"), cnt); + + s << wxT("==============================\n"); + s << wxString::Format(_("Default recording device number: %d\n"), recDeviceNum); + s << wxString::Format(_("Default playback device number: %d\n"), playDeviceNum); + + wxString recDevice = gPrefs->Read(wxT("/MidiIO/RecordingDevice"), wxT("")); + wxString playDevice = gPrefs->Read(wxT("/MidiIO/PlaybackDevice"), wxT("")); + + // This gets info on all available audio devices (input and output) + if (cnt <= 0) { + s << _("No devices found\n"); + return o.GetString(); + } + + for (int i = 0; i < cnt; i++) { + s << wxT("==============================\n"); + + const PmDeviceInfo* info = Pm_GetDeviceInfo(i); + if (!info) { + s << wxString::Format(_("Device info unavailable for: %d\n"), i); + continue; + } + + wxString name = wxSafeConvertMB2WX(info->name); + wxString hostName = wxSafeConvertMB2WX(info->interf); + + s << wxString::Format(_("Device ID: %d\n"), i); + s << wxString::Format(_("Device name: %s\n"), name); + s << wxString::Format(_("Host name: %s\n"), hostName); + /* i18n-hint: Supported, meaning made available by the system */ + s << wxString::Format(_("Supports output: %d\n"), info->output); + s << wxString::Format(_("Supports input: %d\n"), info->input); + s << wxString::Format(_("Opened: %d\n"), info->opened); + + if (name == playDevice && info->output) + playDeviceNum = i; + + if (name == recDevice && info->input) + recDeviceNum = i; + + // XXX: This is only done because the same was applied with PortAudio + // If PortMidi returns -1 for the default device, use the first one + if (recDeviceNum < 0 && info->input){ + recDeviceNum = i; + } + if (playDeviceNum < 0 && info->output){ + playDeviceNum = i; + } + } + + bool haveRecDevice = (recDeviceNum >= 0); + bool havePlayDevice = (playDeviceNum >= 0); + + s << wxT("==============================\n"); + if (haveRecDevice) + s << wxString::Format(_("Selected MIDI recording device: %d - %s\n"), recDeviceNum, recDevice); + else + s << wxString::Format(_("No MIDI recording device found for '%s'.\n"), recDevice); + + if (havePlayDevice) + s << wxString::Format(_("Selected MIDI playback device: %d - %s\n"), playDeviceNum, playDevice); + else + s << wxString::Format(_("No MIDI playback device found for '%s'.\n"), playDevice); + + // Mention our conditional compilation flags for Alpha only +#ifdef IS_ALPHA + + // Not internationalizing these alpha-only messages + s << wxT("==============================\n"); +#ifdef EXPERIMENTAL_MIDI_OUT + s << wxT("EXPERIMENTAL_MIDI_OUT is enabled\n"); +#else + s << wxT("EXPERIMENTAL_MIDI_OUT is NOT enabled\n"); +#endif +#ifdef EXPERIMENTAL_MIDI_IN + s << wxT("EXPERIMENTAL_MIDI_IN is enabled\n"); +#else + s << wxT("EXPERIMENTAL_MIDI_IN is NOT enabled\n"); +#endif + +#endif + + return o.GetString(); +} +#endif + +bool AudioIOBase::PlaybackSchedule::PassIsComplete() const +{ + // Test mTime within the PortAudio callback + if (Scrubbing()) + return false; // but may be true if playing at speed + return Overruns( GetTrackTime() ); +} + +bool AudioIOBase::PlaybackSchedule::Overruns( double trackTime ) const +{ + return (ReversedTime() ? trackTime <= mT1 : trackTime >= mT1); +} + +namespace +{ +/** @brief Compute the duration (in seconds at playback) of the specified region of the track. + * + * Takes a region of the time track (specified by the unwarped time points in the project), and + * calculates how long it will actually take to play this region back, taking the time track's + * warping effects into account. + * @param t0 unwarped time to start calculation from + * @param t1 unwarped time to stop calculation at + * @return the warped duration in seconds + */ +double ComputeWarpedLength(const Envelope &env, double t0, double t1) +{ + return env.IntegralOfInverse(t0, t1); +} + +/** @brief Compute how much unwarped time must have elapsed if length seconds of warped time has + * elapsed + * + * @param t0 The unwarped time (seconds from project start) at which to start + * @param length How many seconds of warped time went past. + * @return The end point (in seconds from project start) as unwarped time + */ +double SolveWarpedLength(const Envelope &env, double t0, double length) +{ + return env.SolveIntegralOfInverse(t0, length); +} +} + +double AudioIOBase::PlaybackSchedule::AdvancedTrackTime( + double time, double realElapsed, double speed ) const +{ + if (ReversedTime()) + realElapsed *= -1.0; + + // Defense against cases that might cause loops not to terminate + if ( fabs(mT0 - mT1) < 1e-9 ) + return mT0; + + if (mEnvelope) { + wxASSERT( speed == 1.0 ); + + double total=0.0; + bool foundTotal = false; + do { + auto oldTime = time; + if (foundTotal && fabs(realElapsed) > fabs(total)) + // Avoid SolveWarpedLength + time = mT1; + else + time = SolveWarpedLength(*mEnvelope, time, realElapsed); + + if (!Looping() || !Overruns( time )) + break; + + // Bug1922: The part of the time track outside the loop should not + // influence the result + double delta; + if (foundTotal && oldTime == mT0) + // Avoid integrating again + delta = total; + else { + delta = ComputeWarpedLength(*mEnvelope, oldTime, mT1); + if (oldTime == mT0) + foundTotal = true, total = delta; + } + realElapsed -= delta; + time = mT0; + } while ( true ); + } + else { + time += realElapsed * speed; + + // Wrap to start if looping + if (Looping()) { + while ( Overruns( time ) ) { + // 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! + time -= mT1 - mT0; + } + } + } + + return time; +} + +void AudioIOBase::PlaybackSchedule::TrackTimeUpdate(double realElapsed) +{ + // Update mTime within the PortAudio callback + + if (Interactive()) + return; + + auto time = GetTrackTime(); + auto newTime = AdvancedTrackTime( time, realElapsed, 1.0 ); + SetTrackTime( newTime ); +} + +double AudioIOBase::PlaybackSchedule::TrackDuration(double realElapsed) const +{ + if (mEnvelope) + return SolveWarpedLength(*mEnvelope, mT0, realElapsed); + else + return realElapsed; +} + +double AudioIOBase::PlaybackSchedule::RealDuration(double trackTime1) const +{ + double duration; + if (mEnvelope) + duration = ComputeWarpedLength(*mEnvelope, mT0, trackTime1); + else + duration = trackTime1 - mT0; + return fabs(duration); +} + +double AudioIOBase::PlaybackSchedule::RealTimeRemaining() const +{ + return mWarpedLength - mWarpedTime; +} + +void AudioIOBase::PlaybackSchedule::RealTimeAdvance( double increment ) +{ + mWarpedTime += increment; +} + +void AudioIOBase::PlaybackSchedule::RealTimeInit( double trackTime ) +{ + if (Scrubbing()) + mWarpedTime = 0.0; + else + mWarpedTime = RealDuration( trackTime ); +} + +void AudioIOBase::PlaybackSchedule::RealTimeRestart() +{ + mWarpedTime = 0; +} + +double AudioIOBase::RecordingSchedule::ToConsume() const +{ + return mDuration - Consumed(); +} + +double AudioIOBase::RecordingSchedule::Consumed() const +{ + return std::max( 0.0, mPosition + TotalCorrection() ); +} + +double AudioIOBase::RecordingSchedule::ToDiscard() const +{ + return std::max(0.0, -( mPosition + TotalCorrection() ) ); +} diff --git a/src/AudioIOBase.h b/src/AudioIOBase.h index d6195b6a1..604c74837 100644 --- a/src/AudioIOBase.h +++ b/src/AudioIOBase.h @@ -11,6 +11,472 @@ Paul Licameli split from AudioIO.h #ifndef __AUDACITY_AUDIO_IO_BASE__ #define __AUDACITY_AUDIO_IO_BASE__ -#include "AudioIO.h" +#include "Audacity.h" // for USE_* macros +#include "Experimental.h" + +#include +#include +#include +#include +#include +#include // member variable +#include "portaudio.h" + +#if USE_PORTMIXER +#include "../lib-src/portmixer/include/portmixer.h" +#endif + +class AudioIOBase; + +class AudacityProject; +class AudioIOListener; +class BoundedEnvelope; +class MeterPanel; +using PRCrossfadeData = std::vector< std::vector < float > >; + +#define BAD_STREAM_TIME (-DBL_MAX) + +// For putting an increment of work in the scrubbing queue +struct ScrubbingOptions { + ScrubbingOptions() {} + + bool adjustStart {}; + + // usually from TrackList::GetEndTime() + double maxTime {}; + double minTime {}; + + bool bySpeed {}; + bool isPlayingAtSpeed{}; + + double delay {}; + + // Limiting values for the speed of a scrub interval: + double minSpeed { 0.0 }; + double maxSpeed { 1.0 }; + + + // When maximum speed scrubbing skips to follow the mouse, + // this is the minimum amount of playback allowed at the maximum speed: + double minStutterTime {}; + + static double MaxAllowedScrubSpeed() + { return 32.0; } // Is five octaves enough for your amusement? + static double MinAllowedScrubSpeed() + { return 0.01; } // Mixer needs a lower bound speed. Scrub no slower than this. +}; + +// To avoid growing the argument list of StartStream, add fields here +struct AudioIOStartStreamOptions +{ + explicit + AudioIOStartStreamOptions(AudacityProject *pProject_, double rate_) + : pProject{ pProject_ } + , envelope(nullptr) + , listener(NULL) + , rate(rate_) + , playLooped(false) + , cutPreviewGapStart(0.0) + , cutPreviewGapLen(0.0) + , pStartTime(NULL) + , preRoll(0.0) + {} + + AudacityProject *pProject{}; + MeterPanel *captureMeter{}, *playbackMeter{}; + BoundedEnvelope *envelope; // for time warping + AudioIOListener* listener; + double rate; + bool playLooped; + double cutPreviewGapStart; + double cutPreviewGapLen; + double * pStartTime; + double preRoll; + +#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT + // Non-null value indicates that scrubbing will happen + // (do not specify a time track, looping, or recording, which + // are all incompatible with scrubbing): + ScrubbingOptions *pScrubbingOptions {}; +#endif + + // contents may get swapped with empty vector + PRCrossfadeData *pCrossfadeData{}; +}; + +///\brief A singleton object supporting queries of the state of any active +/// audio streams, and audio device capabilities +class AudioIOBase /* not final */ +{ +public: + static AudioIOBase *Get(); + + void SetCaptureMeter(AudacityProject *project, MeterPanel *meter); + void SetPlaybackMeter(AudacityProject *project, MeterPanel *meter); + + /** \brief update state after changing what audio devices are selected + * + * Called when the devices stored in the preferences are changed to update + * the audio mixer capabilities + * + * \todo: Make this do a sample rate query and store the result in the + * AudioIO object to avoid doing it later? Would simplify the + * GetSupported*Rate functions considerably */ + void HandleDeviceChange(); + + /** \brief Get a list of sample rates the output (playback) device + * supports. + * + * If no information about available sample rates can be fetched, + * an empty list is returned. + * + * You can explicitely give the index of the device. If you don't + * give it, the currently selected device from the preferences will be used. + * + * You may also specify a rate for which to check in addition to the + * standard rates. + */ + static std::vector GetSupportedPlaybackRates(int DevIndex = -1, + double rate = 0.0); + + /** \brief Get a list of sample rates the input (recording) device + * supports. + * + * If no information about available sample rates can be fetched, + * an empty list is returned. + * + * You can explicitely give the index of the device. If you don't + * give it, the currently selected device from the preferences will be used. + * + * You may also specify a rate for which to check in addition to the + * standard rates. + */ + static std::vector GetSupportedCaptureRates(int devIndex = -1, + double rate = 0.0); + + /** \brief Get a list of sample rates the current input/output device + * combination supports. + * + * Since there is no concept (yet) for different input/output + * sample rates, this currently returns only sample rates that are + * supported on both the output and input device. If no information + * about available sample rates can be fetched, it returns a default + * list. + * You can explicitely give the indexes of the playDevice/recDevice. + * If you don't give them, the selected devices from the preferences + * will be used. + * You may also specify a rate for which to check in addition to the + * standard rates. + */ + static std::vector GetSupportedSampleRates(int playDevice = -1, + int recDevice = -1, + double rate = 0.0); + + /** \brief Get a supported sample rate which can be used a an optimal + * default. + * + * Currently, this uses the first supported rate in the list + * [44100, 48000, highest sample rate]. Used in Project as a default value + * for project rates if one cannot be retrieved from the preferences. + * So all in all not that useful or important really + */ + static int GetOptimalSupportedSampleRate(); + + /** \brief During playback, the track time most recently played + * + * When playing looped, this will start from t0 again, + * too. So the returned time should be always between + * t0 and t1 + */ + double GetStreamTime(); + + /** \brief Array of common audio sample rates + * + * These are the rates we will always support, regardless of hardware support + * for them (by resampling in audacity if needed) */ + static const int StandardRates[]; + /** \brief How many standard sample rates there are */ + static const int NumStandardRates; + + /** \brief Get diagnostic information on all the available audio I/O devices + * + */ + wxString GetDeviceInfo(); + +#ifdef EXPERIMENTAL_MIDI_OUT + /** \brief Get diagnostic information on all the available MIDI I/O devices */ + wxString GetMidiDeviceInfo(); +#endif + + /** \brief Find out if playback / recording is currently paused */ + bool IsPaused() const; + + /** \brief Returns true if audio i/o is busy starting, stopping, playing, + * or recording. + * + * When this is false, it's safe to start playing or recording */ + bool IsBusy() const; + + /** \brief Returns true if the audio i/o is running at all, but not during + * cleanup + * + * Doesn't return true if the device has been closed but some disk i/o or + * cleanup is still going on. If you want to know if it's safe to start a + * NEW stream, use IsBusy() */ + bool IsStreamActive() const; + bool IsStreamActive(int token) const; + + /** \brief Returns true if the stream is active, or even if audio I/O is + * busy cleaning up its data or writing to disk. + * + * This is used by TrackPanel to determine when a track has been completely + * recorded, and it's safe to flush to disk. */ + bool IsAudioTokenActive(int token) const; + + /** \brief Returns true if we're monitoring input (but not recording or + * playing actual audio) */ + bool IsMonitoring() const; + + /* Mixer services are always available. If no stream is running, these + * methods use whatever device is specified by the preferences. If a + * stream *is* running, naturally they manipulate the mixer associated + * with that stream. If no mixer is available, output is emulated and + * input is stuck at 1.0f (a gain is applied to output samples). + */ + void SetMixer(int inputSource); + +protected: + static std::unique_ptr ugAudioIO; + static wxString DeviceName(const PaDeviceInfo* info); + static wxString HostName(const PaDeviceInfo* info); + + AudacityProject *mOwningProject; + + /// True if audio playback is paused + bool mPaused; + + /// True when output reaches mT1 + bool mMidiOutputComplete{ true }; + + /// mMidiStreamActive tells when mMidiStream is open for output + bool mMidiStreamActive; + + volatile int mStreamToken; + + /// Audio playback rate in samples per second + double mRate; + + PaStream *mPortStreamV19; + + wxWeakRef mInputMeter{}; + wxWeakRef mOutputMeter{}; + + #if USE_PORTMIXER + PxMixer *mPortMixer; + float mPreviousHWPlaythrough; + #endif /* USE_PORTMIXER */ + + bool mEmulateMixerOutputVol; + /** @brief Can we control the hardware input level? + * + * This flag is set to true if using portmixer to control the + * input volume seems to be working (and so we offer the user the control), + * and to false (locking the control out) otherwise. This avoids stupid + * scaled clipping problems when trying to do software emulated input volume + * control */ + bool mInputMixerWorks; + float mMixerOutputVol; + + // For cacheing supported sample rates + static int mCachedPlaybackIndex; + static std::vector mCachedPlaybackRates; + static int mCachedCaptureIndex; + static std::vector mCachedCaptureRates; + static std::vector mCachedSampleRates; + static double mCachedBestRateIn; + + struct RecordingSchedule { + double mPreRoll{}; + double mLatencyCorrection{}; // negative value usually + double mDuration{}; + PRCrossfadeData mCrossfadeData; + + // These are initialized by the main thread, then updated + // only by the thread calling FillBuffers: + double mPosition{}; + bool mLatencyCorrected{}; + + double TotalCorrection() const { return mLatencyCorrection - mPreRoll; } + double ToConsume() const; + double Consumed() const; + double ToDiscard() const; + }; + + struct PlaybackSchedule { + /// Playback starts at offset of mT0, which is measured in seconds. + double mT0; + /// Playback ends at offset of mT1, which is measured in seconds. Note that mT1 may be less than mT0 during scrubbing. + double mT1; + /// Current track time position during playback, in seconds. + /// Initialized by the main thread but updated by worker threads during + /// playback or recording, and periodically reread by the main thread for + /// purposes such as display update. + std::atomic mTime; + + /// Accumulated real time (not track position), starting at zero (unlike + /// mTime), and wrapping back to zero each time around looping play. + /// Thus, it is the length in real seconds between mT0 and mTime. + double mWarpedTime; + + /// Real length to be played (if looping, for each pass) after warping via a + /// time track, computed just once when starting the stream. + /// Length in real seconds between mT0 and mT1. Always positive. + double mWarpedLength; + + // 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 + + const BoundedEnvelope *mEnvelope; + + volatile enum { + PLAY_STRAIGHT, + PLAY_LOOPED, +#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT + PLAY_SCRUB, + PLAY_AT_SPEED, // a version of PLAY_SCRUB. +#endif + } mPlayMode { PLAY_STRAIGHT }; + double mCutPreviewGapStart; + double mCutPreviewGapLen; + + void Init( + double t0, double t1, + const AudioIOStartStreamOptions &options, + const RecordingSchedule *pRecordingSchedule ); + + /** \brief True if the end time is before the start time */ + bool ReversedTime() const + { + return mT1 < mT0; + } + + /** \brief Get current track time value, unadjusted + * + * Returns a time in seconds. + */ + double GetTrackTime() const + { return mTime.load(std::memory_order_relaxed); } + + /** \brief Set current track time value, unadjusted + */ + void SetTrackTime( double time ) + { mTime.store(time, std::memory_order_relaxed); } + + /** \brief Clamps argument to be between mT0 and mT1 + * + * Returns the bound if the value is out of bounds; does not wrap. + * Returns a time in seconds. + */ + double ClampTrackTime( double trackTime ) const; + + /** \brief Clamps mTime to be between mT0 and mT1 + * + * Returns the bound if the value is out of bounds; does not wrap. + * Returns a time in seconds. + */ + double LimitTrackTime() const; + + /** \brief Normalizes mTime, clamping it and handling gaps from cut preview. + * + * Clamps the time (unless scrubbing), and skips over the cut section. + * Returns a time in seconds. + */ + double NormalizeTrackTime() const; + + void ResetMode() { mPlayMode = PLAY_STRAIGHT; } + + bool PlayingStraight() const { return mPlayMode == PLAY_STRAIGHT; } + bool Looping() const { return mPlayMode == PLAY_LOOPED; } + bool Scrubbing() const { return mPlayMode == PLAY_SCRUB; } + bool PlayingAtSpeed() const { return mPlayMode == PLAY_AT_SPEED; } + bool Interactive() const { return Scrubbing() || PlayingAtSpeed(); } + + // Returns true if a loop pass, or the sole pass of straight play, + // is completed at the current value of mTime + bool PassIsComplete() const; + + // Returns true if time equals t1 or is on opposite side of t1, to t0 + bool Overruns( double trackTime ) const; + + // Compute the NEW track time for the given one and a real duration, + // taking into account whether the schedule is for looping + double AdvancedTrackTime( + double trackTime, double realElapsed, double speed) const; + + // Use the function above in the callback after consuming samples from the + // playback ring buffers, during usual straight or looping play + void TrackTimeUpdate(double realElapsed); + + // Convert a nonnegative real duration to an increment of track time + // relative to mT0. + double TrackDuration(double realElapsed) const; + + // Convert time between mT0 and argument to real duration, according to + // time track if one is given; result is always nonnegative + double RealDuration(double trackTime1) const; + + // How much real time left? + double RealTimeRemaining() const; + + // Advance the real time position + void RealTimeAdvance( double increment ); + + // Determine starting duration within the first pass -- sometimes not + // zero + void RealTimeInit( double trackTime ); + + void RealTimeRestart(); + + } mPlaybackSchedule; + + /** \brief get the index of the supplied (named) recording device, or the + * device selected in the preferences if none given. + * + * Pure utility function, but it comes round a number of times in the code + * and would be neater done once. If the device isn't found, return the + * default device index. + */ + static int getRecordDevIndex(const wxString &devName = {}); + + /** \brief get the index of the device selected in the preferences. + * + * If the device isn't found, returns -1 + */ +#if USE_PORTMIXER + static int getRecordSourceIndex(PxMixer *portMixer); +#endif + + /** \brief get the index of the supplied (named) playback device, or the + * device selected in the preferences if none given. + * + * Pure utility function, but it comes round a number of times in the code + * and would be neater done once. If the device isn't found, return the + * default device index. + */ + static int getPlayDevIndex(const wxString &devName = {}); + + /** \brief Array of audio sample rates to try to use + * + * These are the rates we will check if a device supports, and is as long + * as I can think of (to try and work out what the card can do) */ + static const int RatesToTry[]; + /** \brief How many sample rates to try */ + static const int NumRatesToTry; +}; #endif diff --git a/src/tracks/ui/Scrubbing.h b/src/tracks/ui/Scrubbing.h index bdc8c66c8..f32de50e4 100644 --- a/src/tracks/ui/Scrubbing.h +++ b/src/tracks/ui/Scrubbing.h @@ -16,6 +16,7 @@ Paul Licameli split from TrackPanel.cpp #include #include +#include "../../AudioIOBase.h" // for ScrubbingOptions #include "../../ClientData.h" #include "../../widgets/Overlay.h" // to inherit #include "../../commands/CommandContext.h" @@ -34,36 +35,6 @@ extern AudacityProject *GetActiveProject(); #define USE_SCRUB_THREAD #endif -// For putting an increment of work in the scrubbing queue -struct ScrubbingOptions { - ScrubbingOptions() {} - - bool adjustStart {}; - - // usually from TrackList::GetEndTime() - double maxTime {}; - double minTime {}; - - bool bySpeed {}; - bool isPlayingAtSpeed{}; - - double delay {}; - - // Limiting values for the speed of a scrub interval: - double minSpeed { 0.0 }; - double maxSpeed { 1.0 }; - - - // When maximum speed scrubbing skips to follow the mouse, - // this is the minimum amount of playback allowed at the maximum speed: - double minStutterTime {}; - - static double MaxAllowedScrubSpeed() - { return 32.0; } // Is five octaves enough for your amusement? - static double MinAllowedScrubSpeed() - { return 0.01; } // Mixer needs a lower bound speed. Scrub no slower than this. -}; - // Scrub state object class Scrubber final : public wxEvtHandler