Hi Jules,
Here are two suggested updates for juce_linux_ALSA.cpp and juce_linux_Midi.cpp
The ALSA update is a follow to this thread: http://www.rawmaterialsoftware.com/viewtopic.php?f=5&t=8143
The main change is that it uses the pcm devices as listed by aplay -L instead of the hardware devices as listed by aplay -l : this allows it to use the ‘default’ alsa output , which is now generally configured to output to pulseaudio. A drawback is that aplay -L is often a quite long list with long names, and sometimes includes some “devices” that simply do not work, and that pulseaudio is generally a pain when you’re trying to switch between devices.
Another change is that devices are not opened during the enumeration. This allows it to list devices that are current in use.
I think it is also slightly more robust that the previous version, with better error reporting.
I have kept the possibility to list hardware soundcards instead of listing the pcm devices, with the ‘listOnlySoundcard’ argument. However my application has been out for a few weeks with only pcm devices enabled, I have got no complaints
The proposed update to juce_linux_Midi.cpp has too objectives:
- in the current version, each time the midi device list is enumerated, an alsa midi client is created (snd_seq_open), the devices are enumerated and the client is closed (snd_seq_close). This causes other applications that monitor alsa clients and connections to consider that something has changed in the ALSA graph, each time. For example qjackctl prints a message each time my client polls the midi device list. My version reuses the same alsa handle to avoid that.
- in the current version for each opened midi device, a new client is created (snd_seq_open), with a single connection to the target midi device. With my change, a single client is created, and it creates a port for each connection to a midi device. This is much nicer in qjackctl. There is also now a single thread for all midi inputs, instead of one thread per connection. In order to use the same alsa client handle when enumerating devices, and for actual connections to other devices, I had to make a singleton from the AlsaClient class. Maybe this is something that will make you frown (the singleton)
Feel free to do whatever you want with these modifications, you can use them, or ignore them if you do not find those changes relevant, or if you think they are too invasive (this is more than a small patch, especially for the midi code I had to shuffle a lot of the original code)
juce_linux_ALSA.cpp:
[code]/*
This file is part of the JUCE library - "Jules’ Utility Class Extensions"
Copyright 2004-11 by Raw Material Software Ltd.
JUCE can be redistributed and/or modified under the terms of the GNU General
Public License (Version 2), as published by the Free Software Foundation.
A copy of the license is included in the JUCE distribution, or can be found
online 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.rawmaterialsoftware.com/juce for more information.
==============================================================================
*/
#include
using std::cerr;
namespace
{
int alsa_verbose = 0;
#define IF_ALSA_VERBOSE(x) if (alsa_verbose) std::cerr << x << “\n”;
#define CHECKED_ALSA(x) (checked_alsa(x, #x, LINE, PRETTY_FUNCTION))
int checked_alsa(int err, const char *what, int lnum, const char *fname)
{
if (err < 0 && alsa_verbose)
{
std::cerr << fname << ":" << lnum << " ALSA called failed: " << what << "; returned " << err << " (" << snd_strerror(err) << ")\n";
}
return err;
}
void getDeviceSampleRates (snd_pcm_t* handle, Array <int>& 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 (ratesToTry[i]);
}
}
}
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);
IF_ALSA_VERBOSE("getDeviceNumChannels: " << *minChans << " " << *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 IF_ALSA_VERBOSE("getDeviceNumChannels failed");
}
void getDeviceProperties (const String& deviceID,
unsigned int& minChansOut,
unsigned int& maxChansOut,
unsigned int& minChansIn,
unsigned int& maxChansIn,
Array <int>& rates,
bool testOutput=true, bool testInput=true)
{
minChansOut = maxChansOut = minChansIn = maxChansIn = 0;
if (deviceID.isEmpty())
return;
IF_ALSA_VERBOSE("getDeviceProperties(" << deviceID.toUTF8().getAddress() << ")");
snd_pcm_info_t* info;
snd_pcm_info_alloca (&info);
if (testOutput) {
snd_pcm_t* pcmHandle;
if (CHECKED_ALSA(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 (CHECKED_ALSA(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);
}
}
}
//==============================================================================
class ALSADevice
{
String devid;
public:
#define failed(x) failed_(x, #x, LINE)
ALSADevice (const String& deviceID, bool forInput)
: handle (0),
bitDepth (16),
numChannelsRunning (0),
latency (0),
isInput (forInput),
isInterleaved (true)
{
devid = deviceID;
IF_ALSA_VERBOSE("\nnew ALSADevice, calling 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) {
// try to improve the ALSA error message for the common cases
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 << “)”;
}
IF_ALSA_VERBOSE("snd_pcm_open failed; " << error);
}
}
~ALSADevice()
{
if (handle != 0)
{
snd_pcm_close(handle);
}
}
void closeNow() { if (handle) { snd_pcm_close(handle); handle = 0; } }
bool setParameters (unsigned int sampleRate, int numChannels, int bufferSize)
{
if (handle == 0)
return false;
IF_ALSA_VERBOSE("ALSADevice::setParameters(" << devid.toUTF8().getAddress() << ", " << 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 };
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_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)
{
bitDepth = formatsToTry [i + 1] & 255;
const bool isFloat = (formatsToTry [i + 1] & isFloatBit) != 0;
const bool isLittleEndian = (formatsToTry [i + 1] & isLittleEndianBit) != 0;
converter = createConverter (isInput, bitDepth, isFloat, isLittleEndian, numChannels);
IF_ALSA_VERBOSE(" ALSA format: bitDepth=" << bitDepth << ", isFloat=" << isFloat << ", isLittleEndian=" << isLittleEndian << ", numChannels=" << numChannels);
break;
}
}
if (bitDepth == 0)
{
error = "device doesn't support a compatible PCM format";
DBG ("ALSA error: " + error + "\n");
return false;
}
int dir = 0;
unsigned int periods = 4;
snd_pcm_uframes_t samplesPerPeriod = bufferSize;
if (failed (snd_pcm_hw_params_set_rate_near (handle, hwParams, &sampleRate, 0))
|| failed (snd_pcm_hw_params_set_channels (handle, hwParams, numChannels))
|| failed (snd_pcm_hw_params_set_periods_near (handle, hwParams, &periods, &dir))
|| failed (snd_pcm_hw_params_set_period_size_near (handle, hwParams, &samplesPerPeriod, &dir))
|| failed (snd_pcm_hw_params (handle, hwParams)))
{
return false;
}
snd_pcm_uframes_t frames = 0;
if (failed (snd_pcm_hw_params_get_period_size (hwParams, &frames, &dir))
|| 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..)
IF_ALSA_VERBOSE("frames=" << frames << ", periods=" << periods << ", samplesPerPeriod=" << samplesPerPeriod);
snd_pcm_sw_params_t* swParams;
snd_pcm_sw_params_alloca (&swParams);
snd_pcm_uframes_t boundary;
if (failed (snd_pcm_sw_params_current (handle, swParams))
|| failed (snd_pcm_sw_params_get_boundary (swParams, &boundary))
|| failed (snd_pcm_sw_params_set_silence_threshold (handle, swParams, 0))
|| failed (snd_pcm_sw_params_set_silence_size (handle, swParams, boundary))
|| failed (snd_pcm_sw_params_set_start_threshold (handle, swParams, samplesPerPeriod))
|| failed (snd_pcm_sw_params_set_stop_threshold (handle, swParams, boundary))
|| failed (snd_pcm_sw_params (handle, swParams)))
{
return false;
}
if (alsa_verbose) {
// 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);
}
numChannelsRunning = numChannels;
IF_ALSA_VERBOSE(" end of setParameters, numChannelsRunning=" << numChannelsRunning);
return true;
}
//==============================================================================
bool writeToOutputDevice (AudioSampleBuffer& outputChannelBuffer, const int numSamples)
{
jassert (numChannelsRunning <= outputChannelBuffer.getNumChannels());
float** const data = outputChannelBuffer.getArrayOfChannels();
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 && failed (snd_pcm_recover (handle, numDone, 1 /* silent */)))
{
return false;
}
else if (numDone < numSamples)
{
IF_ALSA_VERBOSE("Did not write all samples !? numDone=" << numDone << ", numSamples=" << numSamples);
}
return true;
}
bool readFromInputDevice (AudioSampleBuffer& inputChannelBuffer, const int numSamples)
{
jassert (numChannelsRunning <= inputChannelBuffer.getNumChannels());
float** const data = inputChannelBuffer.getArrayOfChannels();
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 && failed (snd_pcm_recover (handle, num, 1 /* silent */)))
{
return false;
}
else if (num < numSamples)
{
IF_ALSA_VERBOSE("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 && failed (snd_pcm_recover (handle, num, 1 /* silent */)))
return false;
else if (num < numSamples)
{
IF_ALSA_VERBOSE("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:
const bool isInput;
bool isInterleaved;
MemoryBlock scratch;
ScopedPointerAudioData::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);
else
return new AudioData::ConverterInstance <AudioData::Pointer <SampleType, AudioData::BigEndian, AudioData::Interleaved, AudioData::Const>, DestType> (numInterleavedChannels, 1);
}
else
{
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);
else
return new AudioData::ConverterInstance <SourceType, AudioData::Pointer <SampleType, AudioData::BigEndian, AudioData::Interleaved, AudioData::NonConst> > (1, numInterleavedChannels);
}
}
};
static AudioData::Converter* createConverter (const bool forInput, const int bitDepth, const bool isFloat, const bool isLittleEndian, const int numInterleavedChannels)
{
switch (bitDepth)
{
case 16: return ConverterHelper <AudioData::Int16>::createConverter (forInput, isLittleEndian, numInterleavedChannels);
case 24: return ConverterHelper <AudioData::Int24>::createConverter (forInput, isLittleEndian, numInterleavedChannels);
case 32: return isFloat ? ConverterHelper <AudioData::Float32>::createConverter (forInput, isLittleEndian, numInterleavedChannels)
: ConverterHelper <AudioData::Int32>::createConverter (forInput, isLittleEndian, numInterleavedChannels);
default: jassertfalse; break; // unsupported format!
}
return nullptr;
}
//==============================================================================
bool failed_ (const int errorNum, const char *what, int lnum)
{
if (errorNum >= 0)
return false;
error = snd_strerror (errorNum);
DBG ("ALSA error: " + error + “\n”);
IF_ALSA_VERBOSE("ALSA error " << errorNum << ", " << what << “:” << lnum);
return true;
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ALSADevice);
};
//==============================================================================
class ALSAThread : public Thread
{
public:
ALSAThread (const String& inputId_,
const String& outputId_)
: Thread (“Juce ALSA”),
sampleRate (0),
bufferSize (0),
outputLatency (0),
inputLatency (0),
callback (0),
inputId (inputId_),
outputId (outputId_),
numCallbacks (0),
audioIoInProgress(false),
inputChannelBuffer (1, 1),
outputChannelBuffer (1, 1)
{
initialiseRatesAndChannels();
}
~ALSAThread()
{
close();
}
void open (BigInteger inputChannels,
BigInteger outputChannels,
const double sampleRate_,
const int bufferSize_)
{
close();
error = String::empty;
sampleRate = sampleRate_;
bufferSize = bufferSize_;
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.getSampleData (i));
currentInputChans.setBit (i);
}
}
}
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.getSampleData (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;
}
currentOutputChans.setRange (0, minChansOut, true);
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;
}
currentInputChans.setRange (0, minChansIn, true);
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 && failed (snd_pcm_prepare (inputDevice->handle)))
return;
if (outputDevice != nullptr && 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();
int numCallbacks0 = numCallbacks;
bool exited = waitForThreadToExit(400);
if (!exited && audioIoInProgress && numCallbacks == numCallbacks0) {
IF_ALSA_VERBOSE("ALSA thread is stuck in a I/O.. Is pulseaudio suspended ? Now trying to wake it up a bit rudely");
if (outputDevice) outputDevice->closeNow();
if (inputDevice) 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()
{
while (! threadShouldExit())
{
if (inputDevice != nullptr && inputDevice->handle)
{
audioIoInProgress = true;
if (! inputDevice->readFromInputDevice (inputChannelBuffer, bufferSize))
{
DBG ("ALSA: read failure");
break;
}
audioIoInProgress = false;
}
if (threadShouldExit())
break;
{
const ScopedLock sl (callbackLock);
++numCallbacks;
if (callback != nullptr)
{
callback->audioDeviceIOCallback ((const float**) 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)
{
failed (snd_pcm_wait (outputDevice->handle, 2000));
if (threadShouldExit())
break;
snd_pcm_sframes_t avail = snd_pcm_avail_update (outputDevice->handle);
if (avail < 0) { failed (snd_pcm_recover(outputDevice->handle, avail, 0)); }
audioIoInProgress = true;
if (! outputDevice->writeToOutputDevice (outputChannelBuffer, bufferSize))
{
DBG ("ALSA: 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 <int> sampleRates;
StringArray channelNamesOut, channelNamesIn;
AudioIODeviceCallback* callback;
private:
//==============================================================================
const String inputId, outputId;
ScopedPointer outputDevice, inputDevice;
int numCallbacks;
bool audioIoInProgress;
CriticalSection callbackLock;
AudioSampleBuffer inputChannelBuffer, outputChannelBuffer;
Array<float*> inputChannelDataForCallback, outputChannelDataForCallback;
unsigned int minChansOut, maxChansOut;
unsigned int minChansIn, maxChansIn;
bool failed_ (const int errorNum, const char *what, int lnum)
{
if (errorNum >= 0)
return false;
error = snd_strerror (errorNum);
DBG ("ALSA error: " + error + "\n");
IF_ALSA_VERBOSE("ALSA error " << errorNum << ", " << what << ":" << lnum);
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);
unsigned int i;
for (i = 0; i < maxChansOut; ++i)
channelNamesOut.add ("channel " + String ((int) i + 1));
for (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& inputId_,
const String& outputId_)
: AudioIODevice (deviceName, typeName),
inputId (inputId_),
outputId (outputId_),
isOpen_ (false),
isStarted (false),
internal (inputId_, outputId_)
{
}
~ALSAAudioIODevice()
{
close();
}
StringArray getOutputChannelNames() { return internal.channelNamesOut; }
StringArray getInputChannelNames() { return internal.channelNamesIn; }
int getNumSampleRates() { return internal.sampleRates.size(); }
double getSampleRate (int index) { return internal.sampleRates [index]; }
int getDefaultBufferSize() { return 512; }
int getNumBufferSizesAvailable() { return 50; }
int getBufferSizeSamples (int index)
{
int n = 16;
for (int i = 0; i < index; ++i)
n += n < 64 ? 16
: (n < 512 ? 32
: (n < 1024 ? 64
: (n < 2048 ? 128 : 256)));
return n;
}
String open (const BigInteger& inputChannels,
const BigInteger& outputChannels,
double sampleRate,
int bufferSizeSamples)
{
close();
if (bufferSizeSamples <= 0)
bufferSizeSamples = getDefaultBufferSize();
if (sampleRate <= 0)
{
for (int i = 0; i < getNumSampleRates(); ++i)
{
if (getSampleRate (i) >= 44100)
{
sampleRate = getSampleRate (i);
break;
}
}
}
internal.open (inputChannels, outputChannels,
sampleRate, bufferSizeSamples);
isOpen_ = internal.error.isEmpty();
return internal.error;
}
void close()
{
stop();
internal.close();
isOpen_ = false;
}
bool isOpen() { return isOpen_; }
bool isPlaying() { return isStarted && internal.error.isEmpty(); }
String getLastError() { return internal.error; }
int getCurrentBufferSizeSamples() { return internal.bufferSize; }
double getCurrentSampleRate() { return internal.sampleRate; }
int getCurrentBitDepth() { return internal.getBitDepth(); }
BigInteger getActiveOutputChannels() const { return internal.currentOutputChans; }
BigInteger getActiveInputChannels() const { return internal.currentInputChans; }
int getOutputLatencyInSamples() { return internal.outputLatency; }
int getInputLatencyInSamples() { return internal.inputLatency; }
void start (AudioIODeviceCallback* callback)
{
if (! isOpen_)
callback = nullptr;
if (callback != nullptr)
callback->audioDeviceAboutToStart (this);
internal.setCallback (callback);
isStarted = (callback != nullptr);
}
void stop()
{
AudioIODeviceCallback* const oldCallback = internal.callback;
start (0);
if (oldCallback != nullptr)
oldCallback->audioDeviceStopped();
}
String inputId, outputId;
private:
bool isOpen_, isStarted;
ALSAThread internal;
};
//==============================================================================
class ALSAAudioIODeviceType : public AudioIODeviceType
{
static void alsaSilentErrorHandler(const char */file/, int /line/, const char */function/, int /err/, const char */fmt/,…) {
}
public:
//==============================================================================
ALSAAudioIODeviceType(bool listOnlySoundcards_, const String &typeName)
: AudioIODeviceType (typeName),
hasScanned (false), listOnlySoundcards(listOnlySoundcards_)
{
const char *v = getenv(“ALSA_VERBOSE”);
if (v && v[0] == ‘1’) alsa_verbose = 1;
if (!alsa_verbose)
{
snd_lib_error_set_handler (&ALSAAudioIODeviceType::alsaSilentErrorHandler);
}
}
~ALSAAudioIODeviceType()
{
if (alsa_verbose)
{
snd_lib_error_set_handler(0);
}
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();
IF_ALSA_VERBOSE("ALSAAudioIODeviceType::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
{
int idx = 0;
if (forInput) idx = inputIds.indexOf("default"); else idx = outputIds.indexOf("default");
jassert (hasScanned); // need to call scanForDevices() before doing this
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
ALSAAudioIODevice* d = dynamic_cast <ALSAAudioIODevice*> (device);
if (d == nullptr)
return -1;
return asInput ? inputIds.indexOf (d->inputId)
: outputIds.indexOf (d->outputId);
}
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 <int> 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)
{
IF_ALSA_VERBOSE("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);
} else 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 (CHECKED_ALSA(snd_ctl_open (&handle, ("hw:" + String (cardNum)).toUTF8(), SND_CTL_NONBLOCK)) >= 0)
{
if (CHECKED_ALSA(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);
bool isInput = (snd_ctl_pcm_info (handle, pcmInfo) >= 0);
snd_pcm_info_set_stream (pcmInfo, SND_PCM_STREAM_PLAYBACK);
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) << "}";
}
IF_ALSA_VERBOSE("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);
}
}
}
}
CHECKED_ALSA(snd_ctl_close (handle));
}
}
}
/* enumerate all ALSA output devices (as output by the command aplay -L)
the enumerate does not try to open the devices (with "testDevice" for example),
that way we also list the devices that are busy and not yet available */
void enumerateAlsaPcmDevices() {
void **hints = 0;
if (CHECKED_ALSA(snd_device_name_hint(-1, "pcm", &hints)) == 0)
{
for (char **h = (char**)hints; *h; ++h)
{
String id, description, ioid;
{
char *aid = 0, *adesc = 0, *aioid = 0;
aid = snd_device_name_get_hint(*h, "NAME");
adesc = snd_device_name_get_hint(*h, "DESC");
aioid = snd_device_name_get_hint(*h, "IOID"); // NULL, or Input or Output; NULL means Input+Output
id << aid;
description << adesc;
ioid << aioid;
free(aid); free(adesc); free(aioid);
}
IF_ALSA_VERBOSE("ID: " << id << "; desc: " << description << "; ioid: " << ioid);
String ss = id.fromFirstOccurrenceOf("=", false, false).upToFirstOccurrenceOf(",", false, false);
/* skip uninteresting entries */
if (id.isEmpty() ||
id.startsWith("default:") ||
id.startsWith("sysdefault:") ||
id.startsWith("plughw:") ||
id == "null") continue;
String name = String(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, and while opening
dmix as input, or dsnoop as output will cause it to burp errors in the terminal.. */
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;
idx = outputIds.indexOf("pulse");
moveToFront(outputIds, idx); moveToFront(outputNames, idx);
idx = inputIds.indexOf("pulse");
moveToFront(inputIds, idx); moveToFront(inputNames, idx);
idx = outputIds.indexOf("default");
moveToFront(outputIds, idx); moveToFront(outputNames, idx);
idx = inputIds.indexOf("default");
moveToFront(inputIds, idx); moveToFront(inputNames, idx);
}
static void moveToFront(StringArray &a, int idx)
{
if (idx > 0)
{
String s = a[idx]; a.remove(idx); a.insert(0, 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();
}
[/code]