mirror of
https://github.com/cookiengineer/audacity
synced 2025-06-16 16:10:06 +02:00
... Giving many examples of use of Settings objects. Many other rewrites like this should be made to eliminate as many direct uses of gPrefs as we can. Don't rely on long distance coincidences of literals for paths or defaults. For each of several paths like /AudioIO/Host, all uses of that path are replaced with use of a global Settings object defined in one place, in AudioIOBase. The object also gives the benefit of caching the last-read or written value. Other users of those preferences must then include "AudioIOBase.h" to make the dependency explicit at compile time. It should be checked that no other mentions of those paths remain in the source, and that there was no unintended change in default values. This also inverts dependency of AudioIOBase on RecordingPrefs, which is GUI for changing some of these settings.
1075 lines
34 KiB
C++
1075 lines
34 KiB
C++
/**********************************************************************
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
AudioIOBase.cpp
|
|
|
|
Paul Licameli split from AudioIO.cpp
|
|
|
|
**********************************************************************/
|
|
|
|
|
|
#include "AudioIOBase.h"
|
|
|
|
|
|
|
|
#include <wx/log.h>
|
|
#include <wx/sstream.h>
|
|
#include <wx/txtstrm.h>
|
|
|
|
#include "Prefs.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 =
|
|
AudioIOLatencyCorrection.GetDefault()/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 =
|
|
AudioIOLatencyCorrection.GetDefault()/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
|
|
auto sourceIndex = AudioIORecordingSourceIndex.Read(); // defaults to -1
|
|
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 );
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
auto latencyDuration = AudioIOLatencyDuration.Read();
|
|
// Why not defaulting to 2 as elsewhere?
|
|
auto recordChannels = AudioIORecordChannels.ReadWithDefault(1);
|
|
|
|
// 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;
|
|
auto sourceName = AudioIORecordingSource.Read();
|
|
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 = AudioIOPlaybackDevice.Read();
|
|
|
|
auto hostName = AudioIOHost.Read();
|
|
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 = AudioIORecordingDevice.Read();
|
|
|
|
auto hostName = AudioIOHost.Read();
|
|
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 XO("Stream is active ... unable to gather information.\n")
|
|
.Translation();
|
|
}
|
|
|
|
|
|
// 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 << XO("Default recording device number: %d\n").Format( recDeviceNum );
|
|
s << XO("Default playback device number: %d\n").Format( playDeviceNum);
|
|
|
|
auto recDevice = AudioIORecordingDevice.Read();
|
|
auto playDevice = AudioIOPlaybackDevice.Read();
|
|
int j;
|
|
|
|
// This gets info on all available audio devices (input and output)
|
|
if (cnt <= 0) {
|
|
s << XO("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 << XO("Device info unavailable for: %d\n").Format( j );
|
|
continue;
|
|
}
|
|
|
|
wxString name = DeviceName(info);
|
|
s << XO("Device ID: %d\n").Format( j );
|
|
s << XO("Device name: %s\n").Format( name );
|
|
s << XO("Host name: %s\n").Format( HostName(info) );
|
|
s << XO("Recording channels: %d\n").Format( info->maxInputChannels );
|
|
s << XO("Playback channels: %d\n").Format( info->maxOutputChannels );
|
|
s << XO("Low Recording Latency: %g\n").Format( info->defaultLowInputLatency );
|
|
s << XO("Low Playback Latency: %g\n").Format( info->defaultLowOutputLatency );
|
|
s << XO("High Recording Latency: %g\n").Format( info->defaultHighInputLatency );
|
|
s << XO("High Playback Latency: %g\n").Format( info->defaultHighOutputLatency );
|
|
|
|
auto rates = GetSupportedPlaybackRates(j, 0.0);
|
|
|
|
/* i18n-hint: Supported, meaning made available by the system */
|
|
s << XO("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 << XO("Selected recording device: %d - %s\n").Format( recDeviceNum, recDevice );
|
|
else
|
|
s << XO("No recording device found for '%s'.\n").Format( recDevice );
|
|
|
|
if (havePlayDevice)
|
|
s << XO("Selected playback device: %d - %s\n").Format( playDeviceNum, playDevice );
|
|
else
|
|
s << XO("No playback device found for '%s'.\n").Format( playDevice );
|
|
|
|
std::vector<long> supportedSampleRates;
|
|
|
|
if (havePlayDevice && haveRecDevice) {
|
|
supportedSampleRates = GetSupportedSampleRates(playDeviceNum, recDeviceNum);
|
|
|
|
s << XO("Supported Rates:\n");
|
|
for (int k = 0; k < (int) supportedSampleRates.size(); k++) {
|
|
s << wxT(" ") << (int)supportedSampleRates[k] << wxT("\n");
|
|
}
|
|
}
|
|
else {
|
|
s << XO("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 =
|
|
AudioIOLatencyCorrection.GetDefault()/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 =
|
|
AudioIOLatencyCorrection.GetDefault()/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 << XO("Received %d while opening devices\n").Format( error );
|
|
return o.GetString();
|
|
}
|
|
|
|
PxMixer *PortMixer = Px_OpenMixer(stream, 0);
|
|
|
|
if (!PortMixer) {
|
|
s << XO("Unable to open Portmixer\n");
|
|
Pa_CloseStream(stream);
|
|
return o.GetString();
|
|
}
|
|
|
|
s << wxT("==============================\n");
|
|
s << XO("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 << XO("%d - %s\n").Format( i, name );
|
|
}
|
|
|
|
s << wxT("==============================\n");
|
|
s << XO("Available recording sources:\n");
|
|
cnt = Px_GetNumInputSources(PortMixer);
|
|
for (int i = 0; i < cnt; i++) {
|
|
wxString name = wxSafeConvertMB2WX(Px_GetInputSourceName(PortMixer, i));
|
|
s << XO("%d - %s\n").Format( i, name );
|
|
}
|
|
|
|
s << wxT("==============================\n");
|
|
s << XO("Available playback volumes:\n");
|
|
cnt = Px_GetNumOutputVolumes(PortMixer);
|
|
for (int i = 0; i < cnt; i++) {
|
|
wxString name = wxSafeConvertMB2WX(Px_GetOutputVolumeName(PortMixer, i));
|
|
s << XO("%d - %s\n").Format( i, name );
|
|
}
|
|
|
|
// Determine mixer capabilities - if 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
|
|
? XO("Recording volume is emulated\n")
|
|
: XO("Recording volume is native\n") );
|
|
s << ( EmulateMixerOutputVol
|
|
? XO("Playback volume is emulated\n")
|
|
: XO("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 XO("Stream is active ... unable to gather information.\n")
|
|
.Translation();
|
|
}
|
|
|
|
|
|
// 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 << XO("Default recording device number: %d\n").Format( recDeviceNum );
|
|
s << XO("Default playback device number: %d\n").Format( 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 << XO("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 << XO("Device info unavailable for: %d\n").Format( i );
|
|
continue;
|
|
}
|
|
|
|
wxString name = wxSafeConvertMB2WX(info->name);
|
|
wxString hostName = wxSafeConvertMB2WX(info->interf);
|
|
|
|
s << XO("Device ID: %d\n").Format( i );
|
|
s << XO("Device name: %s\n").Format( name );
|
|
s << XO("Host name: %s\n").Format( hostName );
|
|
/* i18n-hint: Supported, meaning made available by the system */
|
|
s << XO("Supports output: %d\n").Format( info->output );
|
|
/* i18n-hint: Supported, meaning made available by the system */
|
|
s << XO("Supports input: %d\n").Format( info->input );
|
|
s << XO("Opened: %d\n").Format( 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 << XO("Selected MIDI recording device: %d - %s\n").Format( recDeviceNum, recDevice );
|
|
else
|
|
s << XO("No MIDI recording device found for '%s'.\n").Format( recDevice );
|
|
|
|
if (havePlayDevice)
|
|
s << XO("Selected MIDI playback device: %d - %s\n").Format( playDeviceNum, playDevice );
|
|
else
|
|
s << XO("No MIDI playback device found for '%s'.\n").Format( 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
|
|
|
|
StringSetting AudioIOHost{
|
|
L"/AudioIO/Host", L"" };
|
|
DoubleSetting AudioIOLatencyCorrection{
|
|
L"/AudioIO/LatencyCorrection", -130.0 };
|
|
DoubleSetting AudioIOLatencyDuration{
|
|
L"/AudioIO/LatencyDuration", 100.0 };
|
|
StringSetting AudioIOPlaybackDevice{
|
|
L"/AudioIO/PlaybackDevice", L"" };
|
|
IntSetting AudioIORecordChannels{
|
|
L"/AudioIO/RecordChannels", 2 };
|
|
StringSetting AudioIORecordingDevice{
|
|
L"/AudioIO/RecordingDevice", L"" };
|
|
StringSetting AudioIORecordingSource{
|
|
L"/AudioIO/RecordingSource", L"" };
|
|
IntSetting AudioIORecordingSourceIndex{
|
|
L"/AudioIO/RecordingSourceIndex", -1 };
|