diff --git a/modules/juce_audio_devices/native/juce_linux_ALSA.cpp b/modules/juce_audio_devices/native/juce_linux_ALSA.cpp index 782a749f53..151d47dd7f 100644 --- a/modules/juce_audio_devices/native/juce_linux_ALSA.cpp +++ b/modules/juce_audio_devices/native/juce_linux_ALSA.cpp @@ -25,110 +25,162 @@ namespace { - 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); +#ifndef JUCE_ALSA_LOGGING + #define JUCE_ALSA_LOGGING 1 +#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__)) - for (int i = 0; ratesToTry[i] != 0; ++i) + 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) + +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, ratesToTry[i], 0) == 0) { - 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]); - } + 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); - void getDeviceNumChannels (snd_pcm_t* handle, unsigned int* minChans, unsigned int* maxChans) + if (snd_pcm_hw_params_any (handle, params) >= 0) { - snd_pcm_hw_params_t *params; - snd_pcm_hw_params_alloca (¶ms); + snd_pcm_hw_params_get_channels_min (params, minChans); + snd_pcm_hw_params_get_channels_max (params, maxChans); - 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); - void getDeviceProperties (const String& deviceID, - unsigned int& minChansOut, - unsigned int& maxChansOut, - unsigned int& minChansIn, - unsigned int& maxChansIn, - Array & rates) + // 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 (deviceID.isEmpty()) - return; + JUCE_ALSA_LOG ("getDeviceNumChannels failed"); + } +} - snd_ctl_t* handle; +void getDeviceProperties (const String& deviceID, + unsigned int& minChansOut, + unsigned int& maxChansOut, + unsigned int& minChansIn, + unsigned int& maxChansIn, + Array & rates, + bool testOutput = true, + bool testInput = true) +{ + minChansOut = maxChansOut = minChansIn = maxChansIn = 0; - if (snd_ctl_open (&handle, deviceID.upToLastOccurrenceOf (",", false, false).toUTF8(), SND_CTL_NONBLOCK) >= 0) - { - snd_pcm_info_t* info; - snd_pcm_info_alloca (&info); + if (deviceID.isEmpty()) + return; - snd_pcm_info_set_stream (info, SND_PCM_STREAM_PLAYBACK); - snd_pcm_info_set_device (info, deviceID.fromLastOccurrenceOf (",", false, false).getIntValue()); - snd_pcm_info_set_subdevice (info, 0); + JUCE_ALSA_LOG ("getDeviceProperties(" << deviceID.toUTF8().getAddress() << ")"); - if (snd_ctl_pcm_info (handle, info) >= 0) - { - snd_pcm_t* pcmHandle; - if (snd_pcm_open (&pcmHandle, deviceID.toUTF8(), SND_PCM_STREAM_PLAYBACK, SND_PCM_ASYNC | SND_PCM_NONBLOCK) >= 0) - { - getDeviceNumChannels (pcmHandle, &minChansOut, &maxChansOut); - getDeviceSampleRates (pcmHandle, rates); + snd_pcm_info_t* info; + snd_pcm_info_alloca (&info); - snd_pcm_close (pcmHandle); - } - } + if (testOutput) + { + snd_pcm_t* pcmHandle; - snd_pcm_info_set_stream (info, SND_PCM_STREAM_CAPTURE); + 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); - if (snd_ctl_pcm_info (handle, info) >= 0) - { - snd_pcm_t* pcmHandle; - if (snd_pcm_open (&pcmHandle, deviceID.toUTF8(), SND_PCM_STREAM_CAPTURE, SND_PCM_ASYNC | SND_PCM_NONBLOCK) >= 0) - { - getDeviceNumChannels (pcmHandle, &minChansIn, &maxChansIn); + snd_pcm_close (pcmHandle); + } + } - if (rates.size() == 0) - getDeviceSampleRates (pcmHandle, rates); + if (testInput) + { + snd_pcm_t* pcmHandle; - snd_pcm_close (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_ctl_close (handle); + snd_pcm_close (pcmHandle); } } } +static void silentErrorHandler (const char*, int, const char*, int, const char*,...) {} + //============================================================================== class ALSADevice { public: - ALSADevice (const String& deviceID, bool forInput) + ALSADevice (const String& devID, bool forInput) : handle (0), bitDepth (16), numChannelsRunning (0), latency (0), + deviceID (devID), isInput (forInput), isInterleaved (true) { - failed (snd_pcm_open (&handle, deviceID.toUTF8(), - forInput ? SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK, - SND_PCM_ASYNC)); + 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) @@ -136,16 +188,24 @@ public: 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 (failed (snd_pcm_hw_params_any (handle, 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_NONINTERLEAVED) >= 0) - isInterleaved = false; - else if (snd_pcm_hw_params_set_access (handle, hwParams, SND_PCM_ACCESS_RW_INTERLEAVED) >= 0) + 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; @@ -172,6 +232,10 @@ public: const bool isFloat = (formatsToTry [i + 1] & isFloatBit) != 0; const bool isLittleEndian = (formatsToTry [i + 1] & isLittleEndianBit) != 0; converter = createConverter (isInput, bitDepth, isFloat, isLittleEndian, numChannels); + + JUCE_ALSA_LOG ("format: bitDepth=" << bitDepth << ", isFloat=" + << isFloat << ", isLittleEndian=" << isLittleEndian + << ", numChannels=" << numChannels); break; } } @@ -179,7 +243,7 @@ public: if (bitDepth == 0) { error = "device doesn't support a compatible PCM format"; - DBG ("ALSA error: " + error + "\n"); + JUCE_ALSA_LOG ("Error: " + error); return false; } @@ -187,45 +251,48 @@ public: 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))) + 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 (failed (snd_pcm_hw_params_get_period_size (hwParams, &frames, &dir)) - || failed (snd_pcm_hw_params_get_periods (hwParams, &periods, &dir))) + 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 (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))) + 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 0 + #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 + #endif numChannelsRunning = numChannels; @@ -256,16 +323,11 @@ public: numDone = snd_pcm_writen (handle, (void**) data, numSamples); } - if (failed (numDone)) - { - if (numDone == -EPIPE) - { - if (failed (snd_pcm_prepare (handle))) - return false; - } - else if (numDone != -ESTRPIPE) - return false; - } + 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; } @@ -282,16 +344,11 @@ public: snd_pcm_sframes_t num = snd_pcm_readi (handle, scratch.getData(), numSamples); - if (failed (num)) - { - if (num == -EPIPE) - { - if (failed (snd_pcm_prepare (handle))) - return false; - } - else if (num != -ESTRPIPE) - return false; - } + 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); @@ -300,9 +357,12 @@ public: { snd_pcm_sframes_t num = snd_pcm_readn (handle, (void**) data, numSamples); - if (failed (num) && num != -EPIPE && num != -ESTRPIPE) + 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); } @@ -315,8 +375,9 @@ public: String error; int bitDepth, numChannelsRunning, latency; - //============================================================================== private: + //============================================================================== + String deviceID; const bool isInput; bool isInterleaved; MemoryBlock scratch; @@ -334,8 +395,8 @@ private: if (isLittleEndian) return new AudioData::ConverterInstance , DestType> (numInterleavedChannels, 1); - else - return new AudioData::ConverterInstance , DestType> (numInterleavedChannels, 1); + + return new AudioData::ConverterInstance , DestType> (numInterleavedChannels, 1); } else { @@ -343,8 +404,8 @@ private: if (isLittleEndian) return new AudioData::ConverterInstance > (1, numInterleavedChannels); - else - return new AudioData::ConverterInstance > (1, numInterleavedChannels); + + return new AudioData::ConverterInstance > (1, numInterleavedChannels); } } }; @@ -370,7 +431,7 @@ private: return false; error = snd_strerror (errorNum); - DBG ("ALSA error: " + error + "\n"); + JUCE_ALSA_LOG ("ALSA error: " << error); return true; } @@ -392,6 +453,7 @@ public: inputId (inputId_), outputId (outputId_), numCallbacks (0), + audioIoInProgress (false), inputChannelBuffer (1, 1), outputChannelBuffer (1, 1) { @@ -505,14 +567,12 @@ public: } if (outputDevice != nullptr && inputDevice != nullptr) - { snd_pcm_link (outputDevice->handle, inputDevice->handle); - } - if (inputDevice != nullptr && failed (snd_pcm_prepare (inputDevice->handle))) + if (inputDevice != nullptr && JUCE_ALSA_FAILED (snd_pcm_prepare (inputDevice->handle))) return; - if (outputDevice != nullptr && failed (snd_pcm_prepare (outputDevice->handle))) + if (outputDevice != nullptr && JUCE_ALSA_FAILED (snd_pcm_prepare (outputDevice->handle))) return; startThread (9); @@ -533,6 +593,25 @@ public: 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; @@ -554,13 +633,17 @@ public: { while (! threadShouldExit()) { - if (inputDevice != nullptr) + if (inputDevice != nullptr && inputDevice->handle) { + audioIoInProgress = true; + if (! inputDevice->readFromInputDevice (inputChannelBuffer, bufferSize)) { - DBG ("ALSA: read failure"); + JUCE_ALSA_LOG ("Read failure"); break; } + + audioIoInProgress = false; } if (threadShouldExit()) @@ -585,22 +668,30 @@ public: } } - if (outputDevice != nullptr) + if (outputDevice != nullptr && outputDevice->handle) { - failed (snd_pcm_wait (outputDevice->handle, 2000)); + JUCE_ALSA_FAILED (snd_pcm_wait (outputDevice->handle, 2000)); if (threadShouldExit()) break; - failed (snd_pcm_avail_update (outputDevice->handle)); + 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)) { - DBG ("ALSA: write failure"); + JUCE_ALSA_LOG ("write failure"); break; } + + audioIoInProgress = false; } } + audioIoInProgress = false; } int getBitDepth() const noexcept @@ -629,6 +720,7 @@ private: const String inputId, outputId; ScopedPointer outputDevice, inputDevice; int numCallbacks; + bool audioIoInProgress; CriticalSection callbackLock; @@ -644,7 +736,7 @@ private: return false; error = snd_strerror (errorNum); - DBG ("ALSA error: " + error + "\n"); + JUCE_ALSA_LOG ("ALSA error: " << error); return true; } @@ -659,8 +751,8 @@ private: maxChansIn = 0; unsigned int dummy = 0; - getDeviceProperties (inputId, dummy, dummy, minChansIn, maxChansIn, sampleRates); - getDeviceProperties (outputId, minChansOut, maxChansOut, dummy, dummy, sampleRates); + 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)); @@ -678,9 +770,10 @@ class ALSAAudioIODevice : public AudioIODevice { public: ALSAAudioIODevice (const String& deviceName, + const String& typeName, const String& inputId_, const String& outputId_) - : AudioIODevice (deviceName, "ALSA"), + : AudioIODevice (deviceName, typeName), inputId (inputId_), outputId (outputId_), isOpen_ (false), @@ -782,7 +875,7 @@ public: { AudioIODeviceCallback* const oldCallback = internal.callback; - start (0); + start (nullptr); if (oldCallback != nullptr) oldCallback->audioDeviceStopped(); @@ -800,15 +893,23 @@ private: class ALSAAudioIODeviceType : public AudioIODeviceType { public: - //============================================================================== - ALSAAudioIODeviceType() - : AudioIODeviceType ("ALSA"), - hasScanned (false) + 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 } //============================================================================== @@ -823,82 +924,176 @@ public: outputNames.clear(); outputIds.clear(); -/* void** hints = 0; - if (snd_device_name_hint (-1, "pcm", &hints) >= 0) - { - for (void** hint = hints; *hint != 0; ++hint) - { - const String name (getHint (*hint, "NAME")); + JUCE_ALSA_LOG ("scanForDevices()"); - if (name.isNotEmpty()) - { - const String ioid (getHint (*hint, "IOID")); + if (listOnlySoundcards) + enumerateAlsaSoundcards(); + else + enumerateAlsaPCMDevices(); - String desc (getHint (*hint, "DESC")); - if (desc.isEmpty()) - desc = name; + inputNames.appendNumbersToDuplicates (false, true); + outputNames.appendNumbersToDuplicates (false, true); + } - desc = desc.replaceCharacters ("\n\r", " "); + StringArray getDeviceNames (bool wantInputNames) const + { + jassert (hasScanned); // need to call scanForDevices() before doing this - DBG ("name: " << name << "\ndesc: " << desc << "\nIO: " << ioid); + return wantInputNames ? inputNames : outputNames; + } - if (ioid.isEmpty() || ioid == "Input") - { - inputNames.add (desc); - inputIds.add (name); - } + int getDefaultDeviceIndex (bool forInput) const + { + jassert (hasScanned); // need to call scanForDevices() before doing this - if (ioid.isEmpty() || ioid == "Output") - { - outputNames.add (desc); - outputIds.add (name); - } - } + 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); } - snd_device_name_free_hint (hints); + 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() <= 32) + while (outputIds.size() + inputIds.size() <= 64) { snd_card_next (&cardNum); if (cardNum < 0) break; - if (snd_ctl_open (&handle, ("hw:" + String (cardNum)).toUTF8(), SND_CTL_NONBLOCK) >= 0) + if (JUCE_CHECKED_RESULT (snd_ctl_open (&handle, ("hw:" + String (cardNum)).toUTF8(), SND_CTL_NONBLOCK)) >= 0) { - if (snd_ctl_card_info (handle, info) >= 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; - String id, name; - id << "hw:" << cardId << ',' << device; - - bool isInput, isOutput; + snd_pcm_info_set_device (pcmInfo, device); - if (testDevice (id, isInput, isOutput)) + for (int subDevice = 0, nbSubDevice = 1; subDevice < nbSubDevice; ++subDevice) { - name << snd_ctl_card_info_get_name (info); + 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 (name.isEmpty()) - name = id; + 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) { @@ -915,97 +1110,117 @@ public: } } - snd_ctl_close (handle); + JUCE_CHECKED_RESULT (snd_ctl_close (handle)); } } - - inputNames.appendNumbersToDuplicates (false, true); - outputNames.appendNumbersToDuplicates (false, true); } - StringArray getDeviceNames (bool wantInputNames) const + /* 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() { - jassert (hasScanned); // need to call scanForDevices() before doing this + void** hints = nullptr; - return wantInputNames ? inputNames : outputNames; - } + 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")); - int getDefaultDeviceIndex (bool /* forInput */) const - { - jassert (hasScanned); // need to call scanForDevices() before doing this - return 0; - } + JUCE_ALSA_LOG ("ID: " << id << "; desc: " << description << "; ioid: " << ioid); - bool hasSeparateInputsAndOutputs() const { return true; } + String ss = id.fromFirstOccurrenceOf ("=", false, false) + .upToFirstOccurrenceOf (",", false, false); - int getIndexOfDevice (AudioIODevice* device, bool asInput) const - { - jassert (hasScanned); // need to call scanForDevices() before doing this + if (id.isEmpty() + || id.startsWith ("default:") || id.startsWith ("sysdefault:") + || id.startsWith ("plughw:") || id == "null") + continue; - ALSAAudioIODevice* d = dynamic_cast (device); - if (d == nullptr) - return -1; + String name (description.replace ("\n", "; ")); - return asInput ? inputIds.indexOf (d->inputId) - : outputIds.indexOf (d->outputId); - } + if (name.isEmpty()) + name = id; - AudioIODevice* createDevice (const String& outputDeviceName, - const String& inputDeviceName) - { - jassert (hasScanned); // need to call scanForDevices() before doing this + bool isOutput = (ioid != "Input"); + bool isInput = (ioid != "Output"); - const int inputIndex = inputNames.indexOf (inputDeviceName); - const int outputIndex = outputNames.indexOf (outputDeviceName); + // 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"); - String deviceName (outputIndex >= 0 ? outputDeviceName - : inputDeviceName); + if (isInput) + { + inputNames.add (name); + inputIds.add (id); + } - if (inputIndex >= 0 || outputIndex >= 0) - return new ALSAAudioIODevice (deviceName, - inputIds [inputIndex], - outputIds [outputIndex]); + if (isOutput) + { + outputNames.add (name); + outputIds.add (id); + } + } - return nullptr; - } + snd_device_name_free_hint (hints); + } - //============================================================================== -private: - StringArray inputNames, outputNames, inputIds, outputIds; - bool hasScanned; + // 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"); - static bool testDevice (const String& id, bool& isInput, bool& isOutput) - { - unsigned int minChansOut = 0, maxChansOut = 0; - unsigned int minChansIn = 0, maxChansIn = 0; - Array rates; + // same for the pulseaudio plugin + if (! outputIds.contains ("pulse")) + testDevice ("pulse", "Pulseaudio output", "Pulseaudio input"); - getDeviceProperties (id, minChansOut, maxChansOut, minChansIn, maxChansIn, rates); + // 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); - DBG ("ALSA device: " + id - + " outs=" + String ((int) minChansOut) + "-" + String ((int) maxChansOut) - + " ins=" + String ((int) minChansIn) + "-" + String ((int) maxChansIn) - + " rates=" + String (rates.size())); + idx = inputIds.indexOf ("pulse"); + inputIds.move (idx, 0); + inputNames.move (idx, 0); - isInput = maxChansIn > 0; - isOutput = maxChansOut > 0; + idx = outputIds.indexOf ("default"); + outputIds.move (idx, 0); + outputNames.move (idx, 0); - return (isInput || isOutput) && rates.size() > 0; + idx = inputIds.indexOf ("default"); + inputIds.move (idx, 0); + inputNames.move (idx, 0); } - /*static String getHint (void* hint, const char* type) + static String hintToString (const void* hints, const char* type) { - char* const n = snd_device_name_get_hint (hint, type); - const String s ((const char*) n); - free (n); + char* const hint = snd_device_name_get_hint (hints, type); + const String s (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 new ALSAAudioIODeviceType(); + return createAudioIODeviceType_ALSA_PCMDevices(); } diff --git a/modules/juce_audio_devices/native/juce_linux_Midi.cpp b/modules/juce_audio_devices/native/juce_linux_Midi.cpp index 88586b1b9c..3348b206e5 100644 --- a/modules/juce_audio_devices/native/juce_linux_Midi.cpp +++ b/modules/juce_audio_devices/native/juce_linux_Midi.cpp @@ -34,171 +34,389 @@ #define JUCE_ALSA_MIDI_OUTPUT_NAME "Juce Midi Output" #endif -#ifndef JUCE_ALSA_MIDI_INPUT_PORT_NAME - #define JUCE_ALSA_MIDI_INPUT_PORT_NAME "Juce Midi In Port" -#endif +//============================================================================== +namespace +{ -#ifndef JUCE_ALSA_MIDI_OUTPUT_PORT_NAME - #define JUCE_ALSA_MIDI_OUTPUT_PORT_NAME "Juce Midi Out Port" -#endif +class AlsaPortAndCallback; //============================================================================== -namespace +class AlsaClient : public ReferenceCountedObject { - snd_seq_t* iterateMidiClient (snd_seq_t* seqHandle, - snd_seq_client_info_t* clientInfo, - const bool forInput, - StringArray& deviceNamesFound, - const int deviceIndexToOpen) +public: + typedef ReferenceCountedObjectPtr Ptr; + + AlsaClient (bool forInput) + : input (forInput), handle (nullptr) + { + snd_seq_open (&handle, "default", forInput ? SND_SEQ_OPEN_INPUT + : SND_SEQ_OPEN_OUTPUT, 0); + } + + ~AlsaClient() { - snd_seq_t* returnedHandle = nullptr; + if (handle != nullptr) + { + snd_seq_close (handle); + handle = nullptr; + } + + jassert (activeCallbacks.size() == 0); - snd_seq_port_info_t* portInfo; - if (snd_seq_port_info_malloc (&portInfo) == 0) + if (inputThread) { - int numPorts = snd_seq_client_info_get_num_ports (clientInfo); - const int client = snd_seq_client_info_get_client (clientInfo); + inputThread->stopThread (3000); + inputThread = nullptr; + } + } + + bool isInput() const noexcept { return input; } - snd_seq_port_info_set_client (portInfo, client); - snd_seq_port_info_set_port (portInfo, -1); + void setName (const String& name) + { + snd_seq_set_client_name (handle, name.toUTF8()); + } - while (--numPorts >= 0) + void registerCallback (AlsaPortAndCallback* cb) + { + if (cb != nullptr) + { { - if (snd_seq_query_next_port (seqHandle, portInfo) == 0 - && (snd_seq_port_info_get_capability (portInfo) - & (forInput ? SND_SEQ_PORT_CAP_READ - : SND_SEQ_PORT_CAP_WRITE)) != 0) - { - deviceNamesFound.add (snd_seq_client_info_get_name (clientInfo)); + const ScopedLock sl (callbackLock); + activeCallbacks.add (cb); + + if (inputThread == nullptr) + inputThread = new MidiInputThread (*this); + } - if (deviceNamesFound.size() == deviceIndexToOpen + 1) + inputThread->startThread(); + } + } + + void unregisterCallback (AlsaPortAndCallback* cb) + { + const ScopedLock sl (callbackLock); + + jassert (activeCallbacks.contains (cb)); + activeCallbacks.removeAllInstancesOf (cb); + + if (activeCallbacks.size() == 0 && inputThread->isThreadRunning()) + inputThread->signalThreadShouldExit(); + } + + void handleIncomingMidiMessage (const MidiMessage& message, int port); + + snd_seq_t* get() const noexcept { return handle; } + +private: + bool input; + snd_seq_t* handle; + + Array activeCallbacks; + CriticalSection callbackLock; + + //============================================================================== + class MidiInputThread : public Thread + { + public: + MidiInputThread (AlsaClient& c) + : Thread ("Juce MIDI Input"), client (c) + { + jassert (client.input && client.get() != nullptr); + } + + void run() + { + const int maxEventSize = 16 * 1024; + snd_midi_event_t* midiParser; + snd_seq_t* seqHandle = client.get(); + + if (snd_midi_event_new (maxEventSize, &midiParser) >= 0) + { + const int numPfds = snd_seq_poll_descriptors_count (seqHandle, POLLIN); + HeapBlock pfd (numPfds); + snd_seq_poll_descriptors (seqHandle, pfd, numPfds, POLLIN); + + HeapBlock buffer (maxEventSize); + + while (! threadShouldExit()) + { + if (poll (pfd, numPfds, 100) > 0) // there was a "500" here which is a bit long when we exit the program and have to wait for a timeout on this poll call { - const int sourcePort = snd_seq_port_info_get_port (portInfo); - const int sourceClient = snd_seq_client_info_get_client (clientInfo); + if (threadShouldExit()) + break; - if (sourcePort != -1) + snd_seq_nonblock (seqHandle, 1); + + do { - if (forInput) + snd_seq_event_t* inputEvent = nullptr; + + if (snd_seq_event_input (seqHandle, &inputEvent) >= 0) { - snd_seq_set_client_name (seqHandle, JUCE_ALSA_MIDI_INPUT_NAME); + // xxx what about SYSEXes that are too big for the buffer? + const int numBytes = snd_midi_event_decode (midiParser, buffer, + maxEventSize, inputEvent); - const int portId = snd_seq_create_simple_port (seqHandle, JUCE_ALSA_MIDI_INPUT_PORT_NAME, - SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, - SND_SEQ_PORT_TYPE_MIDI_GENERIC); + snd_midi_event_reset_decode (midiParser); - snd_seq_connect_from (seqHandle, portId, sourceClient, sourcePort); - } - else - { - snd_seq_set_client_name (seqHandle, JUCE_ALSA_MIDI_OUTPUT_NAME); + if (numBytes > 0) + { + const MidiMessage message ((const uint8*) buffer, numBytes, + Time::getMillisecondCounter() * 0.001); - const int portId = snd_seq_create_simple_port (seqHandle, JUCE_ALSA_MIDI_OUTPUT_PORT_NAME, - SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ, - SND_SEQ_PORT_TYPE_MIDI_GENERIC); + client.handleIncomingMidiMessage (message, inputEvent->dest.port); + } - snd_seq_connect_to (seqHandle, portId, sourceClient, sourcePort); + snd_seq_free_event (inputEvent); } - - returnedHandle = seqHandle; } + while (snd_seq_event_input_pending (seqHandle, 0) > 0); } } + + snd_midi_event_free (midiParser); } + }; + + private: + AlsaClient& client; + }; + + ScopedPointer inputThread; +}; + + +static AlsaClient::Ptr globalAlsaSequencerIn() +{ + static AlsaClient::Ptr global (new AlsaClient (true)); + return global; +} + +static AlsaClient::Ptr globalAlsaSequencerOut() +{ + static AlsaClient::Ptr global (new AlsaClient (false)); + return global; +} + +static AlsaClient::Ptr globalAlsaSequencer (bool input) +{ + return input ? globalAlsaSequencerIn() + : globalAlsaSequencerOut(); +} + +//============================================================================== +// represents an input or output port of the supplied AlsaClient +class AlsaPort +{ +public: + AlsaPort() noexcept : portId (-1) {} + AlsaPort (const AlsaClient::Ptr& c, int port) noexcept : client (c), portId (port) {} - snd_seq_port_info_free (portInfo); + void createPort (const AlsaClient::Ptr& c, const String& name, bool forInput) + { + client = c; + + if (snd_seq_t* handle = client->get()) + portId = snd_seq_create_simple_port (handle, name.toUTF8(), + forInput ? (SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE) + : (SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ), + SND_SEQ_PORT_TYPE_MIDI_GENERIC); + } + + void deletePort() + { + if (isValid()) + { + snd_seq_delete_simple_port (client->get(), portId); + portId = -1; } + } + + void connectWith (int sourceClient, int sourcePort) + { + if (client->isInput()) + snd_seq_connect_from (client->get(), portId, sourceClient, sourcePort); + else + snd_seq_connect_to (client->get(), portId, sourceClient, sourcePort); + } - return returnedHandle; + bool isValid() const noexcept + { + return client != nullptr && client->get() != nullptr && portId >= 0; } - snd_seq_t* iterateMidiDevices (const bool forInput, + AlsaClient::Ptr client; + int portId; +}; + +//============================================================================== +class AlsaPortAndCallback +{ +public: + AlsaPortAndCallback (AlsaPort p, MidiInput* in, MidiInputCallback* cb) + : port (p), midiInput (in), callback (cb), callbackEnabled (false) + { + } + + ~AlsaPortAndCallback() + { + enableCallback (false); + port.deletePort(); + } + + void enableCallback (bool enable) + { + if (callbackEnabled != enable) + { + callbackEnabled = enable; + + if (enable) + port.client->registerCallback (this); + else + port.client->unregisterCallback (this); + } + } + + void handleIncomingMidiMessage (const MidiMessage& message) const + { + callback->handleIncomingMidiMessage (midiInput, message); + } + +private: + AlsaPort port; + MidiInput* midiInput; + MidiInputCallback* callback; + bool callbackEnabled; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AlsaPortAndCallback) +}; + +void AlsaClient::handleIncomingMidiMessage (const MidiMessage& message, int port) +{ + const ScopedLock sl (callbackLock); + + if (AlsaPortAndCallback* const cb = activeCallbacks[port]) + cb->handleIncomingMidiMessage (message); +} + +//============================================================================== +static AlsaPort iterateMidiClient (const AlsaClient::Ptr& seq, + snd_seq_client_info_t* clientInfo, + const bool forInput, StringArray& deviceNamesFound, const int deviceIndexToOpen) +{ + AlsaPort port; + + snd_seq_t* seqHandle = seq->get(); + snd_seq_port_info_t* portInfo = nullptr; + + if (snd_seq_port_info_malloc (&portInfo) == 0) { - snd_seq_t* returnedHandle = nullptr; - snd_seq_t* seqHandle = nullptr; + int numPorts = snd_seq_client_info_get_num_ports (clientInfo); + const int client = snd_seq_client_info_get_client (clientInfo); - if (snd_seq_open (&seqHandle, "default", forInput ? SND_SEQ_OPEN_INPUT - : SND_SEQ_OPEN_OUTPUT, 0) == 0) - { - snd_seq_system_info_t* systemInfo = nullptr; - snd_seq_client_info_t* clientInfo = nullptr; + snd_seq_port_info_set_client (portInfo, client); + snd_seq_port_info_set_port (portInfo, -1); - if (snd_seq_system_info_malloc (&systemInfo) == 0) + while (--numPorts >= 0) + { + if (snd_seq_query_next_port (seqHandle, portInfo) == 0 + && (snd_seq_port_info_get_capability (portInfo) & (forInput ? SND_SEQ_PORT_CAP_READ + : SND_SEQ_PORT_CAP_WRITE)) != 0) { - if (snd_seq_system_info (seqHandle, systemInfo) == 0 - && snd_seq_client_info_malloc (&clientInfo) == 0) - { - int numClients = snd_seq_system_info_get_cur_clients (systemInfo); + deviceNamesFound.add (snd_seq_client_info_get_name (clientInfo)); - while (--numClients >= 0 && returnedHandle == 0) - if (snd_seq_query_next_client (seqHandle, clientInfo) == 0) - returnedHandle = iterateMidiClient (seqHandle, clientInfo, - forInput, deviceNamesFound, - deviceIndexToOpen); + if (deviceNamesFound.size() == deviceIndexToOpen + 1) + { + const int sourcePort = snd_seq_port_info_get_port (portInfo); + const int sourceClient = snd_seq_client_info_get_client (clientInfo); - snd_seq_client_info_free (clientInfo); + if (sourcePort != -1) + { + const String name (forInput ? JUCE_ALSA_MIDI_INPUT_NAME + : JUCE_ALSA_MIDI_OUTPUT_NAME); + seq->setName (name); + port.createPort (seq, name, forInput); + port.connectWith (sourceClient, sourcePort); + } } - - snd_seq_system_info_free (systemInfo); } - - if (returnedHandle == 0) - snd_seq_close (seqHandle); } - deviceNamesFound.appendNumbersToDuplicates (true, true); - - return returnedHandle; + snd_seq_port_info_free (portInfo); } - snd_seq_t* createMidiDevice (const bool forInput, const String& deviceNameToOpen) + return port; +} + +static AlsaPort iterateMidiDevices (const bool forInput, + StringArray& deviceNamesFound, + const int deviceIndexToOpen) +{ + AlsaPort port; + const AlsaClient::Ptr client (globalAlsaSequencer (forInput)); + + if (snd_seq_t* const seqHandle = client->get()) { - snd_seq_t* seqHandle = nullptr; + snd_seq_system_info_t* systemInfo = nullptr; + snd_seq_client_info_t* clientInfo = nullptr; - if (snd_seq_open (&seqHandle, "default", forInput ? SND_SEQ_OPEN_INPUT - : SND_SEQ_OPEN_OUTPUT, 0) == 0) + if (snd_seq_system_info_malloc (&systemInfo) == 0) { - snd_seq_set_client_name (seqHandle, - (deviceNameToOpen + (forInput ? " Input" : " Output")).toUTF8()); - - const int portId - = snd_seq_create_simple_port (seqHandle, - forInput ? "in" - : "out", - forInput ? (SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE) - : (SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ), - forInput ? SND_SEQ_PORT_TYPE_APPLICATION - : SND_SEQ_PORT_TYPE_MIDI_GENERIC); - - if (portId < 0) + if (snd_seq_system_info (seqHandle, systemInfo) == 0 + && snd_seq_client_info_malloc (&clientInfo) == 0) { - snd_seq_close (seqHandle); - seqHandle = nullptr; + int numClients = snd_seq_system_info_get_cur_clients (systemInfo); + + while (--numClients >= 0 && ! port.isValid()) + if (snd_seq_query_next_client (seqHandle, clientInfo) == 0) + port = iterateMidiClient (client, clientInfo, forInput, + deviceNamesFound, deviceIndexToOpen); + + snd_seq_client_info_free (clientInfo); } + + snd_seq_system_info_free (systemInfo); } - return seqHandle; } + + deviceNamesFound.appendNumbersToDuplicates (true, true); + + return port; } +AlsaPort createMidiDevice (const bool forInput, const String& deviceNameToOpen) +{ + AlsaPort port; + AlsaClient::Ptr client (new AlsaClient (forInput)); + + if (client->get()) + { + client->setName (deviceNameToOpen + (forInput ? " Input" : " Output")); + port.createPort (client, forInput ? "in" : "out", forInput); + } + + return port; +} //============================================================================== class MidiOutputDevice { public: - MidiOutputDevice (MidiOutput* output, snd_seq_t* handle) - : midiOutput (output), seqHandle (handle), + MidiOutputDevice (MidiOutput* const output, const AlsaPort& p) + : midiOutput (output), port (p), maxEventSize (16 * 1024) { - jassert (seqHandle != 0 && midiOutput != 0); + jassert (port.isValid() && midiOutput != nullptr); snd_midi_event_new (maxEventSize, &midiParser); } ~MidiOutputDevice() { snd_midi_event_free (midiParser); - snd_seq_close (seqHandle); } void sendMessageNow (const MidiMessage& message) @@ -216,6 +434,8 @@ public: long numBytes = (long) message.getRawDataSize(); const uint8* data = message.getRawData(); + snd_seq_t* seqHandle = port.client->get(); + while (numBytes > 0) { const long numSent = snd_midi_event_encode (midiParser, data, numBytes, &event); @@ -238,13 +458,15 @@ public: private: MidiOutput* const midiOutput; - snd_seq_t* const seqHandle; + AlsaPort port; snd_midi_event_t* midiParser; int maxEventSize; - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiOutputDevice) + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiOutputDevice); }; +} // namespace + StringArray MidiOutput::getDevices() { StringArray devices; @@ -260,12 +482,14 @@ int MidiOutput::getDefaultDeviceIndex() MidiOutput* MidiOutput::openDevice (int deviceIndex) { MidiOutput* newDevice = nullptr; + StringArray devices; + AlsaPort port (iterateMidiDevices (false, devices, deviceIndex)); - if (snd_seq_t* const handle = iterateMidiDevices (false, devices, deviceIndex)) + if (port.isValid()) { newDevice = new MidiOutput(); - newDevice->internal = new MidiOutputDevice (newDevice, handle); + newDevice->internal = new MidiOutputDevice (newDevice, port); } return newDevice; @@ -275,10 +499,12 @@ MidiOutput* MidiOutput::createNewDevice (const String& deviceName) { MidiOutput* newDevice = nullptr; - if (snd_seq_t* const handle = createMidiDevice (false, deviceName)) + AlsaPort port (createMidiDevice (false, deviceName)); + + if (port.isValid()) { newDevice = new MidiOutput(); - newDevice->internal = new MidiOutputDevice (newDevice, handle); + newDevice->internal = new MidiOutputDevice (newDevice, port); } return newDevice; @@ -294,79 +520,6 @@ void MidiOutput::sendMessageNow (const MidiMessage& message) static_cast (internal)->sendMessageNow (message); } - -//============================================================================== -class MidiInputThread : public Thread -{ -public: - MidiInputThread (MidiInput* input, snd_seq_t* handle, MidiInputCallback* cb) - : Thread ("Juce MIDI Input"), - midiInput (input), seqHandle (handle), callback (cb) - { - jassert (seqHandle != nullptr && callback != nullptr && midiInput != nullptr); - } - - ~MidiInputThread() - { - snd_seq_close (seqHandle); - } - - void run() - { - const int maxEventSize = 16 * 1024; - snd_midi_event_t* midiParser; - - if (snd_midi_event_new (maxEventSize, &midiParser) >= 0) - { - HeapBlock buffer (maxEventSize); - - const int numPfds = snd_seq_poll_descriptors_count (seqHandle, POLLIN); - HeapBlock pfd (numPfds); - - snd_seq_poll_descriptors (seqHandle, pfd, numPfds, POLLIN); - - while (! threadShouldExit()) - { - if (poll (pfd, numPfds, 500) > 0) - { - snd_seq_event_t* inputEvent = nullptr; - snd_seq_nonblock (seqHandle, 1); - - do - { - if (snd_seq_event_input (seqHandle, &inputEvent) >= 0) - { - // xxx what about SYSEXes that are too big for the buffer? - const int numBytes = snd_midi_event_decode (midiParser, buffer, maxEventSize, inputEvent); - - snd_midi_event_reset_decode (midiParser); - - if (numBytes > 0) - callback->handleIncomingMidiMessage (midiInput, - MidiMessage ((const uint8*) buffer, numBytes, - Time::getMillisecondCounter() * 0.001)); - - snd_seq_free_event (inputEvent); - } - } - while (snd_seq_event_input_pending (seqHandle, 0) > 0); - - snd_seq_free_event (inputEvent); - } - } - - snd_midi_event_free (midiParser); - } - }; - -private: - MidiInput* const midiInput; - snd_seq_t* const seqHandle; - MidiInputCallback* const callback; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiInputThread) -}; - //============================================================================== MidiInput::MidiInput (const String& nm) : name (nm), internal (nullptr) @@ -376,11 +529,18 @@ MidiInput::MidiInput (const String& nm) MidiInput::~MidiInput() { stop(); - delete static_cast (internal); + delete static_cast (internal); } -void MidiInput::start() { static_cast (internal)->startThread(); } -void MidiInput::stop() { static_cast (internal)->stopThread (3000); } +void MidiInput::start() +{ + static_cast (internal)->enableCallback (true); +} + +void MidiInput::stop() +{ + static_cast (internal)->enableCallback (false); +} int MidiInput::getDefaultDeviceIndex() { @@ -397,12 +557,14 @@ StringArray MidiInput::getDevices() MidiInput* MidiInput::openDevice (int deviceIndex, MidiInputCallback* callback) { MidiInput* newDevice = nullptr; + StringArray devices; + AlsaPort port (iterateMidiDevices (true, devices, deviceIndex)); - if (snd_seq_t* const handle = iterateMidiDevices (true, devices, deviceIndex)) + if (port.isValid()) { newDevice = new MidiInput (devices [deviceIndex]); - newDevice->internal = new MidiInputThread (newDevice, handle, callback); + newDevice->internal = new AlsaPortAndCallback (port, newDevice, callback); } return newDevice; @@ -412,10 +574,12 @@ MidiInput* MidiInput::createNewDevice (const String& deviceName, MidiInputCallba { MidiInput* newDevice = nullptr; - if (snd_seq_t* const handle = createMidiDevice (true, deviceName)) + AlsaPort port (createMidiDevice (true, deviceName)); + + if (port.isValid()) { newDevice = new MidiInput (deviceName); - newDevice->internal = new MidiInputThread (newDevice, handle, callback); + newDevice->internal = new AlsaPortAndCallback (port, newDevice, callback); } return newDevice; @@ -434,7 +598,7 @@ MidiOutput* MidiOutput::createNewDevice (const String&) { return nul MidiOutput::~MidiOutput() {} void MidiOutput::sendMessageNow (const MidiMessage&) {} -MidiInput::MidiInput (const String& name_) : name (name_), internal (0) {} +MidiInput::MidiInput (const String& nm) : name (nm), internal (nullptr) {} MidiInput::~MidiInput() {} void MidiInput::start() {} void MidiInput::stop() {}