audacia/src/AudioIOBase.cpp
Paul Licameli ccc49f8ccf Abstract virtual AudioIOBase::StopStream...
... so that DeviceManager, DeviceToolbar, and PrefsDialog do not depend directly
on AudioIO.

But no function in the base class for starting streams, which would require
mention of Track types, which we want to avoid.
2019-06-10 21:55:29 -04:00

1355 lines
42 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
AudioIOBase.cpp
Paul Licameli split from AudioIO.cpp
**********************************************************************/
#include "Audacity.h"
#include "AudioIOBase.h"
#include "Experimental.h"
#include <wx/sstream.h>
#include <wx/txtstrm.h>
#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<long> AudioIOBase::mCachedPlaybackRates;
int AudioIOBase::mCachedCaptureIndex = -1;
std::vector<long> AudioIOBase::mCachedCaptureRates;
std::vector<long> 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> 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<long> 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<long> 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<long> 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<long> 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<long> 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<long> 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<long> 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() ) );
}