/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2015 - ROLI 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& 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, (unsigned int) 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& 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 (nullptr), 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 != nullptr) { snd_pcm_close (handle); handle = nullptr; } } bool setParameters (unsigned int sampleRate, int numChannels, int bufferSize) { if (handle == nullptr) 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 = (snd_pcm_uframes_t) 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, (unsigned int ) 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 = (int) frames * ((int) 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 ((size_t) ((int) 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(), (snd_pcm_uframes_t) numSamples); } else { for (int i = 0; i < numChannelsRunning; ++i) converter->convertSamples (data[i], data[i], numSamples); numDone = snd_pcm_writen (handle, (void**) data, (snd_pcm_uframes_t) numSamples); } if (numDone < 0 && JUCE_ALSA_FAILED (snd_pcm_recover (handle, (int) 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 ((size_t) ((int) 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(), (snd_pcm_uframes_t) numSamples); if (num < 0 && JUCE_ALSA_FAILED (snd_pcm_recover (handle, (int) 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, (snd_pcm_uframes_t) numSamples); if (num < 0 && JUCE_ALSA_FAILED (snd_pcm_recover (handle, (int) 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 converter; //============================================================================== template struct ConverterHelper { static AudioData::Converter* createConverter (const bool forInput, const bool isLittleEndian, const int numInterleavedChannels) { if (forInput) { typedef AudioData::Pointer DestType; if (isLittleEndian) return new AudioData::ConverterInstance , DestType> (numInterleavedChannels, 1); return new AudioData::ConverterInstance , DestType> (numInterleavedChannels, 1); } typedef AudioData::Pointer SourceType; if (isLittleEndian) return new AudioData::ConverterInstance > (1, numInterleavedChannels); return new AudioData::ConverterInstance > (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 ::createConverter (forInput, isLittleEndian, numInterleavedChannels); if (bitDepth == 16) return ConverterHelper ::createConverter (forInput, isLittleEndian, numInterleavedChannels); if (bitDepth == 24) return ConverterHelper ::createConverter (forInput, isLittleEndian, numInterleavedChannels); jassert (bitDepth == 32); if (useOnlyLower24Bits) return ConverterHelper ::createConverter (forInput, isLittleEndian, numInterleavedChannels); return ConverterHelper ::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; int maxInputsRequested = inputChannels.getHighestBit() + 1; maxInputsRequested = jmax ((int) minChansIn, jmin ((int) maxChansIn, maxInputsRequested)); inputChannelBuffer.setSize (maxInputsRequested, bufferSize); inputChannelBuffer.clear(); inputChannelDataForCallback.clear(); currentInputChans.clear(); if (inputChannels.getHighestBit() >= 0) { for (int i = 0; i < maxInputsRequested; ++i) { if (inputChannels[i]) { inputChannelDataForCallback.add (inputChannelBuffer.getReadPointer (i)); currentInputChans.setBit (i); } } } ensureMinimumNumBitsSet (outputChannels, (int) minChansOut); int maxOutputsRequested = outputChannels.getHighestBit() + 1; maxOutputsRequested = jmax ((int) minChansOut, jmin ((int) maxChansOut, maxOutputsRequested)); outputChannelBuffer.setSize (maxOutputsRequested, bufferSize); outputChannelBuffer.clear(); outputChannelDataForCallback.clear(); currentOutputChans.clear(); if (outputChannels.getHighestBit() >= 0) { for (int i = 0; i < maxOutputsRequested; ++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, (int) 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 != nullptr) { if (outputDevice == nullptr || outputDevice->handle == nullptr) { JUCE_ALSA_FAILED (snd_pcm_wait (inputDevice->handle, 2000)); if (threadShouldExit()) break; snd_pcm_sframes_t avail = snd_pcm_avail_update (inputDevice->handle); if (avail < 0) JUCE_ALSA_FAILED (snd_pcm_recover (inputDevice->handle, (int) avail, 0)); } 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) * (size_t) bufferSize); } } if (outputDevice != nullptr && outputDevice->handle != nullptr) { 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, (int) 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 sampleRates; StringArray channelNamesOut, channelNamesIn; AudioIODeviceCallback* callback; private: //============================================================================== const String inputId, outputId; ScopedPointer outputDevice, inputDevice; int numCallbacks; bool audioIoInProgress; CriticalSection callbackLock; AudioSampleBuffer inputChannelBuffer, outputChannelBuffer; Array inputChannelDataForCallback; Array 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& deviceTypeName, const String& inputDeviceID, const String& outputDeviceID) : AudioIODevice (deviceName, deviceTypeName), 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 getAvailableSampleRates() override { return internal.sampleRates; } Array getAvailableBufferSizes() override { Array 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 &deviceTypeName) : AudioIODeviceType (deviceTypeName), 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 (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 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, (unsigned int) device); for (unsigned 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 << "," << (int) 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(); }