mirror of
https://github.com/cookiengineer/audacity
synced 2025-05-06 23:02:42 +02:00
... 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.
1355 lines
42 KiB
C++
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() ) );
|
|
}
|