mirror of https://github.com/dcoredump/dexed.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1243 lines
44 KiB
1243 lines
44 KiB
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE library.
|
|
Copyright (c) 2013 - Raw Material Software Ltd.
|
|
|
|
Permission is granted to use this software under the terms of either:
|
|
a) the GPL v2 (or any later version)
|
|
b) the Affero GPL v3
|
|
|
|
Details of these licenses can be found at: www.gnu.org/licenses
|
|
|
|
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
------------------------------------------------------------------------------
|
|
|
|
To release a closed-source product which uses JUCE, commercial licenses are
|
|
available: visit www.juce.com for more information.
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
namespace
|
|
{
|
|
|
|
#ifndef JUCE_ALSA_LOGGING
|
|
#define JUCE_ALSA_LOGGING 0
|
|
#endif
|
|
|
|
#if JUCE_ALSA_LOGGING
|
|
#define JUCE_ALSA_LOG(dbgtext) { juce::String tempDbgBuf ("ALSA: "); tempDbgBuf << dbgtext; Logger::writeToLog (tempDbgBuf); DBG (tempDbgBuf) }
|
|
#define JUCE_CHECKED_RESULT(x) (logErrorMessage (x, __LINE__))
|
|
|
|
static int logErrorMessage (int err, int lineNum)
|
|
{
|
|
if (err < 0)
|
|
JUCE_ALSA_LOG ("Error: line " << lineNum << ": code " << err << " (" << snd_strerror (err) << ")");
|
|
|
|
return err;
|
|
}
|
|
#else
|
|
#define JUCE_ALSA_LOG(x) {}
|
|
#define JUCE_CHECKED_RESULT(x) (x)
|
|
#endif
|
|
|
|
#define JUCE_ALSA_FAILED(x) failed (x)
|
|
|
|
static void getDeviceSampleRates (snd_pcm_t* handle, Array<double>& rates)
|
|
{
|
|
const int ratesToTry[] = { 22050, 32000, 44100, 48000, 88200, 96000, 176400, 192000, 0 };
|
|
|
|
snd_pcm_hw_params_t* hwParams;
|
|
snd_pcm_hw_params_alloca (&hwParams);
|
|
|
|
for (int i = 0; ratesToTry[i] != 0; ++i)
|
|
{
|
|
if (snd_pcm_hw_params_any (handle, hwParams) >= 0
|
|
&& snd_pcm_hw_params_test_rate (handle, hwParams, ratesToTry[i], 0) == 0)
|
|
{
|
|
rates.addIfNotAlreadyThere ((double) ratesToTry[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void getDeviceNumChannels (snd_pcm_t* handle, unsigned int* minChans, unsigned int* maxChans)
|
|
{
|
|
snd_pcm_hw_params_t *params;
|
|
snd_pcm_hw_params_alloca (¶ms);
|
|
|
|
if (snd_pcm_hw_params_any (handle, params) >= 0)
|
|
{
|
|
snd_pcm_hw_params_get_channels_min (params, minChans);
|
|
snd_pcm_hw_params_get_channels_max (params, maxChans);
|
|
|
|
JUCE_ALSA_LOG ("getDeviceNumChannels: " << (int) *minChans << " " << (int) *maxChans);
|
|
|
|
// some virtual devices (dmix for example) report 10000 channels , we have to clamp these values
|
|
*maxChans = jmin (*maxChans, 32u);
|
|
*minChans = jmin (*minChans, *maxChans);
|
|
}
|
|
else
|
|
{
|
|
JUCE_ALSA_LOG ("getDeviceNumChannels failed");
|
|
}
|
|
}
|
|
|
|
static void getDeviceProperties (const String& deviceID,
|
|
unsigned int& minChansOut,
|
|
unsigned int& maxChansOut,
|
|
unsigned int& minChansIn,
|
|
unsigned int& maxChansIn,
|
|
Array<double>& rates,
|
|
bool testOutput,
|
|
bool testInput)
|
|
{
|
|
minChansOut = maxChansOut = minChansIn = maxChansIn = 0;
|
|
|
|
if (deviceID.isEmpty())
|
|
return;
|
|
|
|
JUCE_ALSA_LOG ("getDeviceProperties(" << deviceID.toUTF8().getAddress() << ")");
|
|
|
|
snd_pcm_info_t* info;
|
|
snd_pcm_info_alloca (&info);
|
|
|
|
if (testOutput)
|
|
{
|
|
snd_pcm_t* pcmHandle;
|
|
|
|
if (JUCE_CHECKED_RESULT (snd_pcm_open (&pcmHandle, deviceID.toUTF8().getAddress(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK)) >= 0)
|
|
{
|
|
getDeviceNumChannels (pcmHandle, &minChansOut, &maxChansOut);
|
|
getDeviceSampleRates (pcmHandle, rates);
|
|
|
|
snd_pcm_close (pcmHandle);
|
|
}
|
|
}
|
|
|
|
if (testInput)
|
|
{
|
|
snd_pcm_t* pcmHandle;
|
|
|
|
if (JUCE_CHECKED_RESULT (snd_pcm_open (&pcmHandle, deviceID.toUTF8(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK) >= 0))
|
|
{
|
|
getDeviceNumChannels (pcmHandle, &minChansIn, &maxChansIn);
|
|
|
|
if (rates.size() == 0)
|
|
getDeviceSampleRates (pcmHandle, rates);
|
|
|
|
snd_pcm_close (pcmHandle);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void ensureMinimumNumBitsSet (BigInteger& chans, int minNumChans)
|
|
{
|
|
int i = 0;
|
|
|
|
while (chans.countNumberOfSetBits() < minNumChans)
|
|
chans.setBit (i++);
|
|
}
|
|
|
|
static void silentErrorHandler (const char*, int, const char*, int, const char*,...) {}
|
|
|
|
//==============================================================================
|
|
class ALSADevice
|
|
{
|
|
public:
|
|
ALSADevice (const String& devID, bool forInput)
|
|
: handle (0),
|
|
bitDepth (16),
|
|
numChannelsRunning (0),
|
|
latency (0),
|
|
deviceID (devID),
|
|
isInput (forInput),
|
|
isInterleaved (true)
|
|
{
|
|
JUCE_ALSA_LOG ("snd_pcm_open (" << deviceID.toUTF8().getAddress() << ", forInput=" << forInput << ")");
|
|
|
|
int err = snd_pcm_open (&handle, deviceID.toUTF8(),
|
|
forInput ? SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK,
|
|
SND_PCM_ASYNC);
|
|
if (err < 0)
|
|
{
|
|
if (-err == EBUSY)
|
|
error << "The device \"" << deviceID << "\" is busy (another application is using it).";
|
|
else if (-err == ENOENT)
|
|
error << "The device \"" << deviceID << "\" is not available.";
|
|
else
|
|
error << "Could not open " << (forInput ? "input" : "output") << " device \"" << deviceID
|
|
<< "\": " << snd_strerror(err) << " (" << err << ")";
|
|
|
|
JUCE_ALSA_LOG ("snd_pcm_open failed; " << error);
|
|
}
|
|
}
|
|
|
|
~ALSADevice()
|
|
{
|
|
closeNow();
|
|
}
|
|
|
|
void closeNow()
|
|
{
|
|
if (handle != 0)
|
|
{
|
|
snd_pcm_close (handle);
|
|
handle = 0;
|
|
}
|
|
}
|
|
|
|
bool setParameters (unsigned int sampleRate, int numChannels, int bufferSize)
|
|
{
|
|
if (handle == 0)
|
|
return false;
|
|
|
|
JUCE_ALSA_LOG ("ALSADevice::setParameters(" << deviceID << ", "
|
|
<< (int) sampleRate << ", " << numChannels << ", " << bufferSize << ")");
|
|
|
|
snd_pcm_hw_params_t* hwParams;
|
|
snd_pcm_hw_params_alloca (&hwParams);
|
|
|
|
if (snd_pcm_hw_params_any (handle, hwParams) < 0)
|
|
{
|
|
// this is the error message that aplay returns when an error happens here,
|
|
// it is a bit more explicit that "Invalid parameter"
|
|
error = "Broken configuration for this PCM: no configurations available";
|
|
return false;
|
|
}
|
|
|
|
if (snd_pcm_hw_params_set_access (handle, hwParams, SND_PCM_ACCESS_RW_INTERLEAVED) >= 0) // works better for plughw..
|
|
isInterleaved = true;
|
|
else if (snd_pcm_hw_params_set_access (handle, hwParams, SND_PCM_ACCESS_RW_NONINTERLEAVED) >= 0)
|
|
isInterleaved = false;
|
|
else
|
|
{
|
|
jassertfalse;
|
|
return false;
|
|
}
|
|
|
|
enum { isFloatBit = 1 << 16, isLittleEndianBit = 1 << 17, onlyUseLower24Bits = 1 << 18 };
|
|
|
|
const int formatsToTry[] = { SND_PCM_FORMAT_FLOAT_LE, 32 | isFloatBit | isLittleEndianBit,
|
|
SND_PCM_FORMAT_FLOAT_BE, 32 | isFloatBit,
|
|
SND_PCM_FORMAT_S32_LE, 32 | isLittleEndianBit,
|
|
SND_PCM_FORMAT_S32_BE, 32,
|
|
SND_PCM_FORMAT_S24_3LE, 24 | isLittleEndianBit,
|
|
SND_PCM_FORMAT_S24_3BE, 24,
|
|
SND_PCM_FORMAT_S24_LE, 32 | isLittleEndianBit | onlyUseLower24Bits,
|
|
SND_PCM_FORMAT_S16_LE, 16 | isLittleEndianBit,
|
|
SND_PCM_FORMAT_S16_BE, 16 };
|
|
bitDepth = 0;
|
|
|
|
for (int i = 0; i < numElementsInArray (formatsToTry); i += 2)
|
|
{
|
|
if (snd_pcm_hw_params_set_format (handle, hwParams, (_snd_pcm_format) formatsToTry [i]) >= 0)
|
|
{
|
|
const int type = formatsToTry [i + 1];
|
|
bitDepth = type & 255;
|
|
|
|
converter = createConverter (isInput, bitDepth,
|
|
(type & isFloatBit) != 0,
|
|
(type & isLittleEndianBit) != 0,
|
|
(type & onlyUseLower24Bits) != 0,
|
|
numChannels);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (bitDepth == 0)
|
|
{
|
|
error = "device doesn't support a compatible PCM format";
|
|
JUCE_ALSA_LOG ("Error: " + error);
|
|
return false;
|
|
}
|
|
|
|
int dir = 0;
|
|
unsigned int periods = 4;
|
|
snd_pcm_uframes_t samplesPerPeriod = bufferSize;
|
|
|
|
if (JUCE_ALSA_FAILED (snd_pcm_hw_params_set_rate_near (handle, hwParams, &sampleRate, 0))
|
|
|| JUCE_ALSA_FAILED (snd_pcm_hw_params_set_channels (handle, hwParams, numChannels))
|
|
|| JUCE_ALSA_FAILED (snd_pcm_hw_params_set_periods_near (handle, hwParams, &periods, &dir))
|
|
|| JUCE_ALSA_FAILED (snd_pcm_hw_params_set_period_size_near (handle, hwParams, &samplesPerPeriod, &dir))
|
|
|| JUCE_ALSA_FAILED (snd_pcm_hw_params (handle, hwParams)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
snd_pcm_uframes_t frames = 0;
|
|
|
|
if (JUCE_ALSA_FAILED (snd_pcm_hw_params_get_period_size (hwParams, &frames, &dir))
|
|
|| JUCE_ALSA_FAILED (snd_pcm_hw_params_get_periods (hwParams, &periods, &dir)))
|
|
latency = 0;
|
|
else
|
|
latency = frames * (periods - 1); // (this is the method JACK uses to guess the latency..)
|
|
|
|
JUCE_ALSA_LOG ("frames: " << (int) frames << ", periods: " << (int) periods
|
|
<< ", samplesPerPeriod: " << (int) samplesPerPeriod);
|
|
|
|
snd_pcm_sw_params_t* swParams;
|
|
snd_pcm_sw_params_alloca (&swParams);
|
|
snd_pcm_uframes_t boundary;
|
|
|
|
if (JUCE_ALSA_FAILED (snd_pcm_sw_params_current (handle, swParams))
|
|
|| JUCE_ALSA_FAILED (snd_pcm_sw_params_get_boundary (swParams, &boundary))
|
|
|| JUCE_ALSA_FAILED (snd_pcm_sw_params_set_silence_threshold (handle, swParams, 0))
|
|
|| JUCE_ALSA_FAILED (snd_pcm_sw_params_set_silence_size (handle, swParams, boundary))
|
|
|| JUCE_ALSA_FAILED (snd_pcm_sw_params_set_start_threshold (handle, swParams, samplesPerPeriod))
|
|
|| JUCE_ALSA_FAILED (snd_pcm_sw_params_set_stop_threshold (handle, swParams, boundary))
|
|
|| JUCE_ALSA_FAILED (snd_pcm_sw_params (handle, swParams)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
#if JUCE_ALSA_LOGGING
|
|
// enable this to dump the config of the devices that get opened
|
|
snd_output_t* out;
|
|
snd_output_stdio_attach (&out, stderr, 0);
|
|
snd_pcm_hw_params_dump (hwParams, out);
|
|
snd_pcm_sw_params_dump (swParams, out);
|
|
#endif
|
|
|
|
numChannelsRunning = numChannels;
|
|
|
|
return true;
|
|
}
|
|
|
|
//==============================================================================
|
|
bool writeToOutputDevice (AudioSampleBuffer& outputChannelBuffer, const int numSamples)
|
|
{
|
|
jassert (numChannelsRunning <= outputChannelBuffer.getNumChannels());
|
|
float* const* const data = outputChannelBuffer.getArrayOfWritePointers();
|
|
snd_pcm_sframes_t numDone = 0;
|
|
|
|
if (isInterleaved)
|
|
{
|
|
scratch.ensureSize (sizeof (float) * numSamples * numChannelsRunning, false);
|
|
|
|
for (int i = 0; i < numChannelsRunning; ++i)
|
|
converter->convertSamples (scratch.getData(), i, data[i], 0, numSamples);
|
|
|
|
numDone = snd_pcm_writei (handle, scratch.getData(), numSamples);
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < numChannelsRunning; ++i)
|
|
converter->convertSamples (data[i], data[i], numSamples);
|
|
|
|
numDone = snd_pcm_writen (handle, (void**) data, numSamples);
|
|
}
|
|
|
|
if (numDone < 0 && JUCE_ALSA_FAILED (snd_pcm_recover (handle, numDone, 1 /* silent */)))
|
|
return false;
|
|
|
|
if (numDone < numSamples)
|
|
JUCE_ALSA_LOG ("Did not write all samples: numDone: " << numDone << ", numSamples: " << numSamples);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool readFromInputDevice (AudioSampleBuffer& inputChannelBuffer, const int numSamples)
|
|
{
|
|
jassert (numChannelsRunning <= inputChannelBuffer.getNumChannels());
|
|
float* const* const data = inputChannelBuffer.getArrayOfWritePointers();
|
|
|
|
if (isInterleaved)
|
|
{
|
|
scratch.ensureSize (sizeof (float) * numSamples * numChannelsRunning, false);
|
|
scratch.fillWith (0); // (not clearing this data causes warnings in valgrind)
|
|
|
|
snd_pcm_sframes_t num = snd_pcm_readi (handle, scratch.getData(), numSamples);
|
|
|
|
if (num < 0 && JUCE_ALSA_FAILED (snd_pcm_recover (handle, num, 1 /* silent */)))
|
|
return false;
|
|
|
|
if (num < numSamples)
|
|
JUCE_ALSA_LOG ("Did not read all samples: num: " << num << ", numSamples: " << numSamples);
|
|
|
|
for (int i = 0; i < numChannelsRunning; ++i)
|
|
converter->convertSamples (data[i], 0, scratch.getData(), i, numSamples);
|
|
}
|
|
else
|
|
{
|
|
snd_pcm_sframes_t num = snd_pcm_readn (handle, (void**) data, numSamples);
|
|
|
|
if (num < 0 && JUCE_ALSA_FAILED (snd_pcm_recover (handle, num, 1 /* silent */)))
|
|
return false;
|
|
|
|
if (num < numSamples)
|
|
JUCE_ALSA_LOG ("Did not read all samples: num: " << num << ", numSamples: " << numSamples);
|
|
|
|
for (int i = 0; i < numChannelsRunning; ++i)
|
|
converter->convertSamples (data[i], data[i], numSamples);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//==============================================================================
|
|
snd_pcm_t* handle;
|
|
String error;
|
|
int bitDepth, numChannelsRunning, latency;
|
|
|
|
private:
|
|
//==============================================================================
|
|
String deviceID;
|
|
const bool isInput;
|
|
bool isInterleaved;
|
|
MemoryBlock scratch;
|
|
ScopedPointer<AudioData::Converter> converter;
|
|
|
|
//==============================================================================
|
|
template <class SampleType>
|
|
struct ConverterHelper
|
|
{
|
|
static AudioData::Converter* createConverter (const bool forInput, const bool isLittleEndian, const int numInterleavedChannels)
|
|
{
|
|
if (forInput)
|
|
{
|
|
typedef AudioData::Pointer <AudioData::Float32, AudioData::NativeEndian, AudioData::NonInterleaved, AudioData::NonConst> DestType;
|
|
|
|
if (isLittleEndian)
|
|
return new AudioData::ConverterInstance <AudioData::Pointer <SampleType, AudioData::LittleEndian, AudioData::Interleaved, AudioData::Const>, DestType> (numInterleavedChannels, 1);
|
|
|
|
return new AudioData::ConverterInstance <AudioData::Pointer <SampleType, AudioData::BigEndian, AudioData::Interleaved, AudioData::Const>, DestType> (numInterleavedChannels, 1);
|
|
}
|
|
|
|
typedef AudioData::Pointer <AudioData::Float32, AudioData::NativeEndian, AudioData::NonInterleaved, AudioData::Const> SourceType;
|
|
|
|
if (isLittleEndian)
|
|
return new AudioData::ConverterInstance <SourceType, AudioData::Pointer <SampleType, AudioData::LittleEndian, AudioData::Interleaved, AudioData::NonConst> > (1, numInterleavedChannels);
|
|
|
|
return new AudioData::ConverterInstance <SourceType, AudioData::Pointer <SampleType, AudioData::BigEndian, AudioData::Interleaved, AudioData::NonConst> > (1, numInterleavedChannels);
|
|
}
|
|
};
|
|
|
|
static AudioData::Converter* createConverter (bool forInput, int bitDepth,
|
|
bool isFloat, bool isLittleEndian, bool useOnlyLower24Bits,
|
|
int numInterleavedChannels)
|
|
{
|
|
JUCE_ALSA_LOG ("format: bitDepth=" << bitDepth << ", isFloat=" << isFloat
|
|
<< ", isLittleEndian=" << isLittleEndian << ", numChannels=" << numInterleavedChannels);
|
|
|
|
if (isFloat) return ConverterHelper <AudioData::Float32>::createConverter (forInput, isLittleEndian, numInterleavedChannels);
|
|
if (bitDepth == 16) return ConverterHelper <AudioData::Int16> ::createConverter (forInput, isLittleEndian, numInterleavedChannels);
|
|
if (bitDepth == 24) return ConverterHelper <AudioData::Int24> ::createConverter (forInput, isLittleEndian, numInterleavedChannels);
|
|
|
|
jassert (bitDepth == 32);
|
|
|
|
if (useOnlyLower24Bits)
|
|
return ConverterHelper <AudioData::Int24in32>::createConverter (forInput, isLittleEndian, numInterleavedChannels);
|
|
|
|
return ConverterHelper <AudioData::Int32>::createConverter (forInput, isLittleEndian, numInterleavedChannels);
|
|
}
|
|
|
|
//==============================================================================
|
|
bool failed (const int errorNum)
|
|
{
|
|
if (errorNum >= 0)
|
|
return false;
|
|
|
|
error = snd_strerror (errorNum);
|
|
JUCE_ALSA_LOG ("ALSA error: " << error);
|
|
return true;
|
|
}
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ALSADevice)
|
|
};
|
|
|
|
//==============================================================================
|
|
class ALSAThread : public Thread
|
|
{
|
|
public:
|
|
ALSAThread (const String& inputDeviceID, const String& outputDeviceID)
|
|
: Thread ("Juce ALSA"),
|
|
sampleRate (0),
|
|
bufferSize (0),
|
|
outputLatency (0),
|
|
inputLatency (0),
|
|
callback (0),
|
|
inputId (inputDeviceID),
|
|
outputId (outputDeviceID),
|
|
numCallbacks (0),
|
|
audioIoInProgress (false),
|
|
inputChannelBuffer (1, 1),
|
|
outputChannelBuffer (1, 1)
|
|
{
|
|
initialiseRatesAndChannels();
|
|
}
|
|
|
|
~ALSAThread()
|
|
{
|
|
close();
|
|
}
|
|
|
|
void open (BigInteger inputChannels,
|
|
BigInteger outputChannels,
|
|
const double newSampleRate,
|
|
const int newBufferSize)
|
|
{
|
|
close();
|
|
|
|
error.clear();
|
|
sampleRate = newSampleRate;
|
|
bufferSize = newBufferSize;
|
|
|
|
inputChannelBuffer.setSize (jmax ((int) minChansIn, inputChannels.getHighestBit()) + 1, bufferSize);
|
|
inputChannelBuffer.clear();
|
|
inputChannelDataForCallback.clear();
|
|
currentInputChans.clear();
|
|
|
|
if (inputChannels.getHighestBit() >= 0)
|
|
{
|
|
for (int i = 0; i <= jmax (inputChannels.getHighestBit(), (int) minChansIn); ++i)
|
|
{
|
|
if (inputChannels[i])
|
|
{
|
|
inputChannelDataForCallback.add (inputChannelBuffer.getReadPointer (i));
|
|
currentInputChans.setBit (i);
|
|
}
|
|
}
|
|
}
|
|
|
|
ensureMinimumNumBitsSet (outputChannels, minChansOut);
|
|
|
|
outputChannelBuffer.setSize (jmax ((int) minChansOut, outputChannels.getHighestBit()) + 1, bufferSize);
|
|
outputChannelBuffer.clear();
|
|
outputChannelDataForCallback.clear();
|
|
currentOutputChans.clear();
|
|
|
|
if (outputChannels.getHighestBit() >= 0)
|
|
{
|
|
for (int i = 0; i <= jmax (outputChannels.getHighestBit(), (int) minChansOut); ++i)
|
|
{
|
|
if (outputChannels[i])
|
|
{
|
|
outputChannelDataForCallback.add (outputChannelBuffer.getWritePointer (i));
|
|
currentOutputChans.setBit (i);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (outputChannelDataForCallback.size() > 0 && outputId.isNotEmpty())
|
|
{
|
|
outputDevice = new ALSADevice (outputId, false);
|
|
|
|
if (outputDevice->error.isNotEmpty())
|
|
{
|
|
error = outputDevice->error;
|
|
outputDevice = nullptr;
|
|
return;
|
|
}
|
|
|
|
if (! outputDevice->setParameters ((unsigned int) sampleRate,
|
|
jlimit ((int) minChansOut, (int) maxChansOut,
|
|
currentOutputChans.getHighestBit() + 1),
|
|
bufferSize))
|
|
{
|
|
error = outputDevice->error;
|
|
outputDevice = nullptr;
|
|
return;
|
|
}
|
|
|
|
outputLatency = outputDevice->latency;
|
|
}
|
|
|
|
if (inputChannelDataForCallback.size() > 0 && inputId.isNotEmpty())
|
|
{
|
|
inputDevice = new ALSADevice (inputId, true);
|
|
|
|
if (inputDevice->error.isNotEmpty())
|
|
{
|
|
error = inputDevice->error;
|
|
inputDevice = nullptr;
|
|
return;
|
|
}
|
|
|
|
ensureMinimumNumBitsSet (currentInputChans, minChansIn);
|
|
|
|
if (! inputDevice->setParameters ((unsigned int) sampleRate,
|
|
jlimit ((int) minChansIn, (int) maxChansIn, currentInputChans.getHighestBit() + 1),
|
|
bufferSize))
|
|
{
|
|
error = inputDevice->error;
|
|
inputDevice = nullptr;
|
|
return;
|
|
}
|
|
|
|
inputLatency = inputDevice->latency;
|
|
}
|
|
|
|
if (outputDevice == nullptr && inputDevice == nullptr)
|
|
{
|
|
error = "no channels";
|
|
return;
|
|
}
|
|
|
|
if (outputDevice != nullptr && inputDevice != nullptr)
|
|
snd_pcm_link (outputDevice->handle, inputDevice->handle);
|
|
|
|
if (inputDevice != nullptr && JUCE_ALSA_FAILED (snd_pcm_prepare (inputDevice->handle)))
|
|
return;
|
|
|
|
if (outputDevice != nullptr && JUCE_ALSA_FAILED (snd_pcm_prepare (outputDevice->handle)))
|
|
return;
|
|
|
|
startThread (9);
|
|
|
|
int count = 1000;
|
|
|
|
while (numCallbacks == 0)
|
|
{
|
|
sleep (5);
|
|
|
|
if (--count < 0 || ! isThreadRunning())
|
|
{
|
|
error = "device didn't start";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void close()
|
|
{
|
|
if (isThreadRunning())
|
|
{
|
|
// problem: when pulseaudio is suspended (with pasuspend) , the ALSAThread::run is just stuck in
|
|
// snd_pcm_writei -- no error, no nothing it just stays stuck. So the only way I found to exit "nicely"
|
|
// (that is without the "killing thread by force" of stopThread) , is to just call snd_pcm_close from
|
|
// here which will cause the thread to resume, and exit
|
|
signalThreadShouldExit();
|
|
|
|
const int callbacksToStop = numCallbacks;
|
|
|
|
if ((! waitForThreadToExit (400)) && audioIoInProgress && numCallbacks == callbacksToStop)
|
|
{
|
|
JUCE_ALSA_LOG ("Thread is stuck in i/o.. Is pulseaudio suspended?");
|
|
|
|
if (outputDevice != nullptr) outputDevice->closeNow();
|
|
if (inputDevice != nullptr) inputDevice->closeNow();
|
|
}
|
|
}
|
|
|
|
stopThread (6000);
|
|
|
|
inputDevice = nullptr;
|
|
outputDevice = nullptr;
|
|
|
|
inputChannelBuffer.setSize (1, 1);
|
|
outputChannelBuffer.setSize (1, 1);
|
|
|
|
numCallbacks = 0;
|
|
}
|
|
|
|
void setCallback (AudioIODeviceCallback* const newCallback) noexcept
|
|
{
|
|
const ScopedLock sl (callbackLock);
|
|
callback = newCallback;
|
|
}
|
|
|
|
void run() override
|
|
{
|
|
while (! threadShouldExit())
|
|
{
|
|
if (inputDevice != nullptr && inputDevice->handle)
|
|
{
|
|
audioIoInProgress = true;
|
|
|
|
if (! inputDevice->readFromInputDevice (inputChannelBuffer, bufferSize))
|
|
{
|
|
JUCE_ALSA_LOG ("Read failure");
|
|
break;
|
|
}
|
|
|
|
audioIoInProgress = false;
|
|
}
|
|
|
|
if (threadShouldExit())
|
|
break;
|
|
|
|
{
|
|
const ScopedLock sl (callbackLock);
|
|
++numCallbacks;
|
|
|
|
if (callback != nullptr)
|
|
{
|
|
callback->audioDeviceIOCallback (inputChannelDataForCallback.getRawDataPointer(),
|
|
inputChannelDataForCallback.size(),
|
|
outputChannelDataForCallback.getRawDataPointer(),
|
|
outputChannelDataForCallback.size(),
|
|
bufferSize);
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < outputChannelDataForCallback.size(); ++i)
|
|
zeromem (outputChannelDataForCallback[i], sizeof (float) * bufferSize);
|
|
}
|
|
}
|
|
|
|
if (outputDevice != nullptr && outputDevice->handle)
|
|
{
|
|
JUCE_ALSA_FAILED (snd_pcm_wait (outputDevice->handle, 2000));
|
|
|
|
if (threadShouldExit())
|
|
break;
|
|
|
|
snd_pcm_sframes_t avail = snd_pcm_avail_update (outputDevice->handle);
|
|
|
|
if (avail < 0)
|
|
JUCE_ALSA_FAILED (snd_pcm_recover (outputDevice->handle, avail, 0));
|
|
|
|
audioIoInProgress = true;
|
|
|
|
if (! outputDevice->writeToOutputDevice (outputChannelBuffer, bufferSize))
|
|
{
|
|
JUCE_ALSA_LOG ("write failure");
|
|
break;
|
|
}
|
|
|
|
audioIoInProgress = false;
|
|
}
|
|
}
|
|
audioIoInProgress = false;
|
|
}
|
|
|
|
int getBitDepth() const noexcept
|
|
{
|
|
if (outputDevice != nullptr)
|
|
return outputDevice->bitDepth;
|
|
|
|
if (inputDevice != nullptr)
|
|
return inputDevice->bitDepth;
|
|
|
|
return 16;
|
|
}
|
|
|
|
//==============================================================================
|
|
String error;
|
|
double sampleRate;
|
|
int bufferSize, outputLatency, inputLatency;
|
|
BigInteger currentInputChans, currentOutputChans;
|
|
|
|
Array<double> sampleRates;
|
|
StringArray channelNamesOut, channelNamesIn;
|
|
AudioIODeviceCallback* callback;
|
|
|
|
private:
|
|
//==============================================================================
|
|
const String inputId, outputId;
|
|
ScopedPointer<ALSADevice> outputDevice, inputDevice;
|
|
int numCallbacks;
|
|
bool audioIoInProgress;
|
|
|
|
CriticalSection callbackLock;
|
|
|
|
AudioSampleBuffer inputChannelBuffer, outputChannelBuffer;
|
|
Array<const float*> inputChannelDataForCallback;
|
|
Array<float*> outputChannelDataForCallback;
|
|
|
|
unsigned int minChansOut, maxChansOut;
|
|
unsigned int minChansIn, maxChansIn;
|
|
|
|
bool failed (const int errorNum)
|
|
{
|
|
if (errorNum >= 0)
|
|
return false;
|
|
|
|
error = snd_strerror (errorNum);
|
|
JUCE_ALSA_LOG ("ALSA error: " << error);
|
|
return true;
|
|
}
|
|
|
|
void initialiseRatesAndChannels()
|
|
{
|
|
sampleRates.clear();
|
|
channelNamesOut.clear();
|
|
channelNamesIn.clear();
|
|
minChansOut = 0;
|
|
maxChansOut = 0;
|
|
minChansIn = 0;
|
|
maxChansIn = 0;
|
|
unsigned int dummy = 0;
|
|
|
|
getDeviceProperties (inputId, dummy, dummy, minChansIn, maxChansIn, sampleRates, false, true);
|
|
getDeviceProperties (outputId, minChansOut, maxChansOut, dummy, dummy, sampleRates, true, false);
|
|
|
|
for (unsigned int i = 0; i < maxChansOut; ++i)
|
|
channelNamesOut.add ("channel " + String ((int) i + 1));
|
|
|
|
for (unsigned int i = 0; i < maxChansIn; ++i)
|
|
channelNamesIn.add ("channel " + String ((int) i + 1));
|
|
}
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ALSAThread)
|
|
};
|
|
|
|
|
|
//==============================================================================
|
|
class ALSAAudioIODevice : public AudioIODevice
|
|
{
|
|
public:
|
|
ALSAAudioIODevice (const String& deviceName,
|
|
const String& typeName,
|
|
const String& inputDeviceID,
|
|
const String& outputDeviceID)
|
|
: AudioIODevice (deviceName, typeName),
|
|
inputId (inputDeviceID),
|
|
outputId (outputDeviceID),
|
|
isOpen_ (false),
|
|
isStarted (false),
|
|
internal (inputDeviceID, outputDeviceID)
|
|
{
|
|
}
|
|
|
|
~ALSAAudioIODevice()
|
|
{
|
|
close();
|
|
}
|
|
|
|
StringArray getOutputChannelNames() override { return internal.channelNamesOut; }
|
|
StringArray getInputChannelNames() override { return internal.channelNamesIn; }
|
|
|
|
Array<double> getAvailableSampleRates() override { return internal.sampleRates; }
|
|
|
|
Array<int> getAvailableBufferSizes() override
|
|
{
|
|
Array<int> r;
|
|
int n = 16;
|
|
|
|
for (int i = 0; i < 50; ++i)
|
|
{
|
|
r.add (n);
|
|
n += n < 64 ? 16
|
|
: (n < 512 ? 32
|
|
: (n < 1024 ? 64
|
|
: (n < 2048 ? 128 : 256)));
|
|
}
|
|
|
|
return r;
|
|
}
|
|
|
|
int getDefaultBufferSize() override { return 512; }
|
|
|
|
String open (const BigInteger& inputChannels,
|
|
const BigInteger& outputChannels,
|
|
double sampleRate,
|
|
int bufferSizeSamples) override
|
|
{
|
|
close();
|
|
|
|
if (bufferSizeSamples <= 0)
|
|
bufferSizeSamples = getDefaultBufferSize();
|
|
|
|
if (sampleRate <= 0)
|
|
{
|
|
for (int i = 0; i < internal.sampleRates.size(); ++i)
|
|
{
|
|
double rate = internal.sampleRates[i];
|
|
|
|
if (rate >= 44100)
|
|
{
|
|
sampleRate = rate;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal.open (inputChannels, outputChannels,
|
|
sampleRate, bufferSizeSamples);
|
|
|
|
isOpen_ = internal.error.isEmpty();
|
|
return internal.error;
|
|
}
|
|
|
|
void close() override
|
|
{
|
|
stop();
|
|
internal.close();
|
|
isOpen_ = false;
|
|
}
|
|
|
|
bool isOpen() override { return isOpen_; }
|
|
bool isPlaying() override { return isStarted && internal.error.isEmpty(); }
|
|
String getLastError() override { return internal.error; }
|
|
|
|
int getCurrentBufferSizeSamples() override { return internal.bufferSize; }
|
|
double getCurrentSampleRate() override { return internal.sampleRate; }
|
|
int getCurrentBitDepth() override { return internal.getBitDepth(); }
|
|
|
|
BigInteger getActiveOutputChannels() const override { return internal.currentOutputChans; }
|
|
BigInteger getActiveInputChannels() const override { return internal.currentInputChans; }
|
|
|
|
int getOutputLatencyInSamples() override { return internal.outputLatency; }
|
|
int getInputLatencyInSamples() override { return internal.inputLatency; }
|
|
|
|
void start (AudioIODeviceCallback* callback) override
|
|
{
|
|
if (! isOpen_)
|
|
callback = nullptr;
|
|
|
|
if (callback != nullptr)
|
|
callback->audioDeviceAboutToStart (this);
|
|
|
|
internal.setCallback (callback);
|
|
|
|
isStarted = (callback != nullptr);
|
|
}
|
|
|
|
void stop() override
|
|
{
|
|
AudioIODeviceCallback* const oldCallback = internal.callback;
|
|
|
|
start (nullptr);
|
|
|
|
if (oldCallback != nullptr)
|
|
oldCallback->audioDeviceStopped();
|
|
}
|
|
|
|
String inputId, outputId;
|
|
|
|
private:
|
|
bool isOpen_, isStarted;
|
|
ALSAThread internal;
|
|
};
|
|
|
|
|
|
//==============================================================================
|
|
class ALSAAudioIODeviceType : public AudioIODeviceType
|
|
{
|
|
public:
|
|
ALSAAudioIODeviceType (bool onlySoundcards, const String &typeName)
|
|
: AudioIODeviceType (typeName),
|
|
hasScanned (false),
|
|
listOnlySoundcards (onlySoundcards)
|
|
{
|
|
#if ! JUCE_ALSA_LOGGING
|
|
snd_lib_error_set_handler (&silentErrorHandler);
|
|
#endif
|
|
}
|
|
|
|
~ALSAAudioIODeviceType()
|
|
{
|
|
#if ! JUCE_ALSA_LOGGING
|
|
snd_lib_error_set_handler (nullptr);
|
|
#endif
|
|
|
|
snd_config_update_free_global(); // prevent valgrind from screaming about alsa leaks
|
|
}
|
|
|
|
//==============================================================================
|
|
void scanForDevices()
|
|
{
|
|
if (hasScanned)
|
|
return;
|
|
|
|
hasScanned = true;
|
|
inputNames.clear();
|
|
inputIds.clear();
|
|
outputNames.clear();
|
|
outputIds.clear();
|
|
|
|
JUCE_ALSA_LOG ("scanForDevices()");
|
|
|
|
if (listOnlySoundcards)
|
|
enumerateAlsaSoundcards();
|
|
else
|
|
enumerateAlsaPCMDevices();
|
|
|
|
inputNames.appendNumbersToDuplicates (false, true);
|
|
outputNames.appendNumbersToDuplicates (false, true);
|
|
}
|
|
|
|
StringArray getDeviceNames (bool wantInputNames) const
|
|
{
|
|
jassert (hasScanned); // need to call scanForDevices() before doing this
|
|
|
|
return wantInputNames ? inputNames : outputNames;
|
|
}
|
|
|
|
int getDefaultDeviceIndex (bool forInput) const
|
|
{
|
|
jassert (hasScanned); // need to call scanForDevices() before doing this
|
|
|
|
const int idx = (forInput ? inputIds : outputIds).indexOf ("default");
|
|
return idx >= 0 ? idx : 0;
|
|
}
|
|
|
|
bool hasSeparateInputsAndOutputs() const { return true; }
|
|
|
|
int getIndexOfDevice (AudioIODevice* device, bool asInput) const
|
|
{
|
|
jassert (hasScanned); // need to call scanForDevices() before doing this
|
|
|
|
if (ALSAAudioIODevice* d = dynamic_cast <ALSAAudioIODevice*> (device))
|
|
return asInput ? inputIds.indexOf (d->inputId)
|
|
: outputIds.indexOf (d->outputId);
|
|
|
|
return -1;
|
|
}
|
|
|
|
AudioIODevice* createDevice (const String& outputDeviceName,
|
|
const String& inputDeviceName)
|
|
{
|
|
jassert (hasScanned); // need to call scanForDevices() before doing this
|
|
|
|
const int inputIndex = inputNames.indexOf (inputDeviceName);
|
|
const int outputIndex = outputNames.indexOf (outputDeviceName);
|
|
|
|
String deviceName (outputIndex >= 0 ? outputDeviceName
|
|
: inputDeviceName);
|
|
|
|
if (inputIndex >= 0 || outputIndex >= 0)
|
|
return new ALSAAudioIODevice (deviceName, getTypeName(),
|
|
inputIds [inputIndex],
|
|
outputIds [outputIndex]);
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
private:
|
|
//==============================================================================
|
|
StringArray inputNames, outputNames, inputIds, outputIds;
|
|
bool hasScanned, listOnlySoundcards;
|
|
|
|
bool testDevice (const String &id, const String &outputName, const String &inputName)
|
|
{
|
|
unsigned int minChansOut = 0, maxChansOut = 0;
|
|
unsigned int minChansIn = 0, maxChansIn = 0;
|
|
Array<double> rates;
|
|
|
|
bool isInput = inputName.isNotEmpty(), isOutput = outputName.isNotEmpty();
|
|
getDeviceProperties (id, minChansOut, maxChansOut, minChansIn, maxChansIn, rates, isOutput, isInput);
|
|
|
|
isInput = maxChansIn > 0;
|
|
isOutput = maxChansOut > 0;
|
|
|
|
if ((isInput || isOutput) && rates.size() > 0)
|
|
{
|
|
JUCE_ALSA_LOG ("testDevice: '" << id.toUTF8().getAddress() << "' -> isInput: "
|
|
<< isInput << ", isOutput: " << isOutput);
|
|
|
|
if (isInput)
|
|
{
|
|
inputNames.add (inputName);
|
|
inputIds.add (id);
|
|
}
|
|
|
|
if (isOutput)
|
|
{
|
|
outputNames.add (outputName);
|
|
outputIds.add (id);
|
|
}
|
|
|
|
return isInput || isOutput;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void enumerateAlsaSoundcards()
|
|
{
|
|
snd_ctl_t* handle = nullptr;
|
|
snd_ctl_card_info_t* info = nullptr;
|
|
snd_ctl_card_info_alloca (&info);
|
|
|
|
int cardNum = -1;
|
|
|
|
while (outputIds.size() + inputIds.size() <= 64)
|
|
{
|
|
snd_card_next (&cardNum);
|
|
|
|
if (cardNum < 0)
|
|
break;
|
|
|
|
if (JUCE_CHECKED_RESULT (snd_ctl_open (&handle, ("hw:" + String (cardNum)).toUTF8(), SND_CTL_NONBLOCK)) >= 0)
|
|
{
|
|
if (JUCE_CHECKED_RESULT (snd_ctl_card_info (handle, info)) >= 0)
|
|
{
|
|
String cardId (snd_ctl_card_info_get_id (info));
|
|
|
|
if (cardId.removeCharacters ("0123456789").isEmpty())
|
|
cardId = String (cardNum);
|
|
|
|
String cardName = snd_ctl_card_info_get_name (info);
|
|
|
|
if (cardName.isEmpty())
|
|
cardName = cardId;
|
|
|
|
int device = -1;
|
|
|
|
snd_pcm_info_t* pcmInfo;
|
|
snd_pcm_info_alloca (&pcmInfo);
|
|
|
|
for (;;)
|
|
{
|
|
if (snd_ctl_pcm_next_device (handle, &device) < 0 || device < 0)
|
|
break;
|
|
|
|
snd_pcm_info_set_device (pcmInfo, device);
|
|
|
|
for (int subDevice = 0, nbSubDevice = 1; subDevice < nbSubDevice; ++subDevice)
|
|
{
|
|
snd_pcm_info_set_subdevice (pcmInfo, subDevice);
|
|
snd_pcm_info_set_stream (pcmInfo, SND_PCM_STREAM_CAPTURE);
|
|
const bool isInput = (snd_ctl_pcm_info (handle, pcmInfo) >= 0);
|
|
|
|
snd_pcm_info_set_stream (pcmInfo, SND_PCM_STREAM_PLAYBACK);
|
|
const bool isOutput = (snd_ctl_pcm_info (handle, pcmInfo) >= 0);
|
|
|
|
if (! (isInput || isOutput))
|
|
continue;
|
|
|
|
if (nbSubDevice == 1)
|
|
nbSubDevice = snd_pcm_info_get_subdevices_count (pcmInfo);
|
|
|
|
String id, name;
|
|
|
|
if (nbSubDevice == 1)
|
|
{
|
|
id << "hw:" << cardId << "," << device;
|
|
name << cardName << ", " << snd_pcm_info_get_name (pcmInfo);
|
|
}
|
|
else
|
|
{
|
|
id << "hw:" << cardId << "," << device << "," << subDevice;
|
|
name << cardName << ", " << snd_pcm_info_get_name (pcmInfo)
|
|
<< " {" << snd_pcm_info_get_subdevice_name (pcmInfo) << "}";
|
|
}
|
|
|
|
JUCE_ALSA_LOG ("Soundcard ID: " << id << ", name: '" << name
|
|
<< ", isInput:" << isInput << ", isOutput:" << isOutput << "\n");
|
|
|
|
if (isInput)
|
|
{
|
|
inputNames.add (name);
|
|
inputIds.add (id);
|
|
}
|
|
|
|
if (isOutput)
|
|
{
|
|
outputNames.add (name);
|
|
outputIds.add (id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
JUCE_CHECKED_RESULT (snd_ctl_close (handle));
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Enumerates all ALSA output devices (as output by the command aplay -L)
|
|
Does not try to open the devices (with "testDevice" for example),
|
|
so that it also finds devices that are busy and not yet available.
|
|
*/
|
|
void enumerateAlsaPCMDevices()
|
|
{
|
|
void** hints = nullptr;
|
|
|
|
if (JUCE_CHECKED_RESULT (snd_device_name_hint (-1, "pcm", &hints)) == 0)
|
|
{
|
|
for (char** h = (char**) hints; *h; ++h)
|
|
{
|
|
const String id (hintToString (*h, "NAME"));
|
|
const String description (hintToString (*h, "DESC"));
|
|
const String ioid (hintToString (*h, "IOID"));
|
|
|
|
JUCE_ALSA_LOG ("ID: " << id << "; desc: " << description << "; ioid: " << ioid);
|
|
|
|
String ss = id.fromFirstOccurrenceOf ("=", false, false)
|
|
.upToFirstOccurrenceOf (",", false, false);
|
|
|
|
if (id.isEmpty()
|
|
|| id.startsWith ("default:") || id.startsWith ("sysdefault:")
|
|
|| id.startsWith ("plughw:") || id == "null")
|
|
continue;
|
|
|
|
String name (description.replace ("\n", "; "));
|
|
|
|
if (name.isEmpty())
|
|
name = id;
|
|
|
|
bool isOutput = (ioid != "Input");
|
|
bool isInput = (ioid != "Output");
|
|
|
|
// alsa is stupid here, it advertises dmix and dsnoop as input/output devices, but
|
|
// opening dmix as input, or dsnoop as output will trigger errors..
|
|
isInput = isInput && ! id.startsWith ("dmix");
|
|
isOutput = isOutput && ! id.startsWith ("dsnoop");
|
|
|
|
if (isInput)
|
|
{
|
|
inputNames.add (name);
|
|
inputIds.add (id);
|
|
}
|
|
|
|
if (isOutput)
|
|
{
|
|
outputNames.add (name);
|
|
outputIds.add (id);
|
|
}
|
|
}
|
|
|
|
snd_device_name_free_hint (hints);
|
|
}
|
|
|
|
// sometimes the "default" device is not listed, but it is nice to see it explicitely in the list
|
|
if (! outputIds.contains ("default"))
|
|
testDevice ("default", "Default ALSA Output", "Default ALSA Input");
|
|
|
|
// same for the pulseaudio plugin
|
|
if (! outputIds.contains ("pulse"))
|
|
testDevice ("pulse", "Pulseaudio output", "Pulseaudio input");
|
|
|
|
// make sure the default device is listed first, and followed by the pulse device (if present)
|
|
int idx = outputIds.indexOf ("pulse");
|
|
outputIds.move (idx, 0);
|
|
outputNames.move (idx, 0);
|
|
|
|
idx = inputIds.indexOf ("pulse");
|
|
inputIds.move (idx, 0);
|
|
inputNames.move (idx, 0);
|
|
|
|
idx = outputIds.indexOf ("default");
|
|
outputIds.move (idx, 0);
|
|
outputNames.move (idx, 0);
|
|
|
|
idx = inputIds.indexOf ("default");
|
|
inputIds.move (idx, 0);
|
|
inputNames.move (idx, 0);
|
|
}
|
|
|
|
static String hintToString (const void* hints, const char* type)
|
|
{
|
|
char* const hint = snd_device_name_get_hint (hints, type);
|
|
const String s (String::fromUTF8 (hint));
|
|
::free (hint);
|
|
return s;
|
|
}
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ALSAAudioIODeviceType)
|
|
};
|
|
|
|
}
|
|
|
|
//==============================================================================
|
|
AudioIODeviceType* createAudioIODeviceType_ALSA_Soundcards()
|
|
{
|
|
return new ALSAAudioIODeviceType (true, "ALSA HW");
|
|
}
|
|
|
|
AudioIODeviceType* createAudioIODeviceType_ALSA_PCMDevices()
|
|
{
|
|
return new ALSAAudioIODeviceType (false, "ALSA");
|
|
}
|
|
|
|
AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_ALSA()
|
|
{
|
|
return createAudioIODeviceType_ALSA_PCMDevices();
|
|
}
|
|
|