/********************************************************************** 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.h" #include "prefs/RecordingPrefs.h" #include "widgets/MeterPanelBase.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(); } AudioIOBase::~AudioIOBase() = default; 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, MeterPanelBase *meter) { if (( mOwningProject ) && ( mOwningProject != project)) return; if (meter) { mInputMeter = meter; mInputMeter->Reset(mRate, true); } else mInputMeter.Release(); } void AudioIOBase::SetPlaybackMeter(AudacityProject *project, MeterPanelBase *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() ) ); }