mirror of
https://github.com/cookiengineer/audacity
synced 2025-05-06 23:02:42 +02:00
1362 lines
42 KiB
C++
1362 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 {
|
|
if (scrubOptions.isPlayingAtSpeed)
|
|
mPlayMode = PLAY_AT_SPEED;
|
|
else if (scrubOptions.isKeyboardScrubbing)
|
|
mPlayMode = PLAY_KEYBOARD_SCRUB;
|
|
else
|
|
mPlayMode = 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 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);
|
|
|
|
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 << 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 = 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 << 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
|
|
|
|
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 * fabs(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() ) );
|
|
}
|