/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. The code included in this file is provided under the terms of the ISC license http://www.isc.org/downloads/software-support-policy/isc-license. Permission To use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted provided that the above copyright notice and this permission notice appear in all copies. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { #if JUCE_ALSA //============================================================================== class AlsaClient { auto lowerBound (int portId) const { const auto comparator = [] (const auto& port, const auto& id) { return port->getPortId() < id; }; return std::lower_bound (ports.begin(), ports.end(), portId, comparator); } auto findPortIterator (int portId) const { const auto iter = lowerBound (portId); return (iter == ports.end() || (*iter)->getPortId() != portId) ? ports.end() : iter; } public: ~AlsaClient() { inputThread.reset(); jassert (activeCallbacks.get() == 0); if (handle != nullptr) { snd_seq_delete_simple_port (handle, announcementsIn); snd_seq_close (handle); } } static String getAlsaMidiName() { #ifdef JUCE_ALSA_MIDI_NAME return JUCE_ALSA_MIDI_NAME; #else if (auto* app = JUCEApplicationBase::getInstance()) return app->getApplicationName(); return "JUCE"; #endif } //============================================================================== // represents an input or output port of the supplied AlsaClient struct Port { explicit Port (bool forInput) noexcept : isInput (forInput) {} ~Port() { if (isValid()) { if (isInput) enableCallback (false); else snd_midi_event_free (midiParser); snd_seq_delete_simple_port (client->get(), portId); } } void connectWith (int sourceClient, int sourcePort) const noexcept { if (isInput) snd_seq_connect_from (client->get(), portId, sourceClient, sourcePort); else snd_seq_connect_to (client->get(), portId, sourceClient, sourcePort); } bool isValid() const noexcept { return client->get() != nullptr && portId >= 0; } void setupInput (MidiInput* input, MidiInputCallback* cb) { jassert (cb != nullptr && input != nullptr); callback = cb; midiInput = input; } void setupOutput() { jassert (! isInput); snd_midi_event_new ((size_t) maxEventSize, &midiParser); } void enableCallback (bool enable) { callbackEnabled = enable; } bool sendMessageNow (const MidiMessage& message) { if (message.getRawDataSize() > maxEventSize) { maxEventSize = message.getRawDataSize(); snd_midi_event_free (midiParser); snd_midi_event_new ((size_t) maxEventSize, &midiParser); } snd_seq_event_t event; snd_seq_ev_clear (&event); auto numBytes = (long) message.getRawDataSize(); auto* data = message.getRawData(); auto seqHandle = client->get(); bool success = true; while (numBytes > 0) { auto numSent = snd_midi_event_encode (midiParser, data, numBytes, &event); if (numSent <= 0) { success = numSent == 0; break; } numBytes -= numSent; data += numSent; snd_seq_ev_set_source (&event, (unsigned char) portId); snd_seq_ev_set_subs (&event); snd_seq_ev_set_direct (&event); if (snd_seq_event_output_direct (seqHandle, &event) < 0) { success = false; break; } } snd_midi_event_reset_encode (midiParser); return success; } bool operator== (const Port& lhs) const noexcept { return portId != -1 && portId == lhs.portId; } void createPort (const String& name, bool enableSubscription) { if (auto seqHandle = client->get()) { const unsigned int caps = isInput ? (SND_SEQ_PORT_CAP_WRITE | (enableSubscription ? SND_SEQ_PORT_CAP_SUBS_WRITE : 0)) : (SND_SEQ_PORT_CAP_READ | (enableSubscription ? SND_SEQ_PORT_CAP_SUBS_READ : 0)); portName = name; portId = snd_seq_create_simple_port (seqHandle, portName.toUTF8(), caps, SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION); } } void handleIncomingMidiMessage (const MidiMessage& message) const { if (callbackEnabled) callback->handleIncomingMidiMessage (midiInput, message); } void handlePartialSysexMessage (const uint8* messageData, int numBytesSoFar, double timeStamp) { if (callbackEnabled) callback->handlePartialSysexMessage (midiInput, messageData, numBytesSoFar, timeStamp); } int getPortId() const { return portId; } const String& getPortName() const { return portName; } private: const std::shared_ptr client = AlsaClient::getInstance(); MidiInputCallback* callback = nullptr; snd_midi_event_t* midiParser = nullptr; MidiInput* midiInput = nullptr; String portName; int maxEventSize = 4096, portId = -1; std::atomic callbackEnabled { false }; bool isInput = false; }; static std::shared_ptr getInstance() { static std::weak_ptr ptr; if (auto locked = ptr.lock()) return locked; std::shared_ptr result (new AlsaClient()); ptr = result; return result; } void handleIncomingMidiMessage (snd_seq_event* event, const MidiMessage& message) { const ScopedLock sl (callbackLock); if (auto* port = findPort (event->dest.port)) port->handleIncomingMidiMessage (message); } void handlePartialSysexMessage (snd_seq_event* event, const uint8* messageData, int numBytesSoFar, double timeStamp) { const ScopedLock sl (callbackLock); if (auto* port = findPort (event->dest.port)) port->handlePartialSysexMessage (messageData, numBytesSoFar, timeStamp); } snd_seq_t* get() const noexcept { return handle; } int getId() const noexcept { return clientId; } Port* createPort (const String& name, bool forInput, bool enableSubscription) { const ScopedLock sl (callbackLock); auto port = new Port (forInput); port->createPort (name, enableSubscription); const auto iter = lowerBound (port->getPortId()); jassert (iter == ports.end() || port->getPortId() < (*iter)->getPortId()); ports.insert (iter, rawToUniquePtr (port)); return port; } void deletePort (Port* port) { const ScopedLock sl (callbackLock); if (const auto iter = findPortIterator (port->getPortId()); iter != ports.end()) ports.erase (iter); } private: AlsaClient() { snd_seq_open (&handle, "default", SND_SEQ_OPEN_DUPLEX, 0); if (handle != nullptr) { snd_seq_nonblock (handle, SND_SEQ_NONBLOCK); snd_seq_set_client_name (handle, getAlsaMidiName().toRawUTF8()); clientId = snd_seq_client_id (handle); // It's good idea to pre-allocate a good number of elements ports.reserve (32); announcementsIn = snd_seq_create_simple_port (handle, TRANS ("announcements").toRawUTF8(), SND_SEQ_PORT_CAP_WRITE, SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION); snd_seq_connect_from (handle, announcementsIn, SND_SEQ_CLIENT_SYSTEM, SND_SEQ_PORT_SYSTEM_ANNOUNCE); inputThread.emplace (*this); } } Port* findPort (int portId) { if (const auto iter = findPortIterator (portId); iter != ports.end()) return iter->get(); return nullptr; } snd_seq_t* handle = nullptr; int clientId = 0; int announcementsIn = 0; std::vector> ports; Atomic activeCallbacks; CriticalSection callbackLock; //============================================================================== class SequencerThread { public: explicit SequencerThread (AlsaClient& c) : client (c) { } ~SequencerThread() noexcept { shouldStop = true; thread.join(); } private: // If we directly call MidiDeviceListConnectionBroadcaster::get() from the background thread, // there's a possibility that we'll deadlock in the following scenario: // - The main thread calls MidiDeviceListConnectionBroadcaster::get() for the first time // (e.g. to register a listener). The static MidiDeviceListConnectionBroadcaster singleton // begins construction. During the constructor, an AlsaClient is created to iterate midi // ins/outs. // - The AlsaClient starts a new SequencerThread. If connections are updated, the // SequencerThread may call MidiDeviceListConnectionBroadcaster::get().notify() // while the MidiDeviceListConnectionBroadcaster singleton is still being created. // - The SequencerThread blocks until the MidiDeviceListConnectionBroadcaster has been // created on the main thread, but the MidiDeviceListConnectionBroadcaster's constructor // can't complete until the AlsaClient's destructor has run, which in turn requires the // SequencerThread to join. class UpdateNotifier final : private AsyncUpdater { public: ~UpdateNotifier() override { cancelPendingUpdate(); } using AsyncUpdater::triggerAsyncUpdate; private: void handleAsyncUpdate() override { MidiDeviceListConnectionBroadcaster::get().notify(); } }; AlsaClient& client; MidiDataConcatenator concatenator { 2048 }; std::atomic shouldStop { false }; UpdateNotifier notifier; std::thread thread { [this] { Thread::setCurrentThreadName ("JUCE MIDI Input"); auto seqHandle = client.get(); const int maxEventSize = 16 * 1024; snd_midi_event_t* midiParser; if (snd_midi_event_new (maxEventSize, &midiParser) >= 0) { const ScopeGuard freeMidiEvent { [&] { snd_midi_event_free (midiParser); } }; const auto numPfds = snd_seq_poll_descriptors_count (seqHandle, POLLIN); std::vector pfd (static_cast (numPfds)); snd_seq_poll_descriptors (seqHandle, pfd.data(), (unsigned int) numPfds, POLLIN); std::vector buffer (maxEventSize); while (! shouldStop) { // This timeout shouldn't be too long, so that the program can exit in a timely manner if (poll (pfd.data(), (nfds_t) numPfds, 100) > 0) { if (shouldStop) break; do { snd_seq_event_t* inputEvent = nullptr; if (snd_seq_event_input (seqHandle, &inputEvent) >= 0) { const ScopeGuard freeInputEvent { [&] { snd_seq_free_event (inputEvent); } }; constexpr int systemEvents[] { SND_SEQ_EVENT_CLIENT_CHANGE, SND_SEQ_EVENT_CLIENT_START, SND_SEQ_EVENT_CLIENT_EXIT, SND_SEQ_EVENT_PORT_CHANGE, SND_SEQ_EVENT_PORT_START, SND_SEQ_EVENT_PORT_EXIT, SND_SEQ_EVENT_PORT_SUBSCRIBED, SND_SEQ_EVENT_PORT_UNSUBSCRIBED, }; const auto foundEvent = std::find (std::begin (systemEvents), std::end (systemEvents), inputEvent->type); if (foundEvent != std::end (systemEvents)) { notifier.triggerAsyncUpdate(); continue; } // xxx what about SYSEXes that are too big for the buffer? const auto numBytes = snd_midi_event_decode (midiParser, buffer.data(), maxEventSize, inputEvent); snd_midi_event_reset_decode (midiParser); concatenator.pushMidiData (buffer.data(), (int) numBytes, Time::getMillisecondCounter() * 0.001, inputEvent, client); } } while (snd_seq_event_input_pending (seqHandle, 0) > 0); } } } } }; }; std::optional inputThread; }; //============================================================================== static String getFormattedPortIdentifier (int clientId, int portId) { return String (clientId) + "-" + String (portId); } static AlsaClient::Port* iterateMidiClient (AlsaClient& client, snd_seq_client_info_t* clientInfo, bool forInput, Array& devices, const String& deviceIdentifierToOpen) { AlsaClient::Port* port = nullptr; auto seqHandle = client.get(); snd_seq_port_info_t* portInfo = nullptr; snd_seq_port_info_alloca (&portInfo); jassert (portInfo != nullptr); auto numPorts = snd_seq_client_info_get_num_ports (clientInfo); auto sourceClient = snd_seq_client_info_get_client (clientInfo); snd_seq_port_info_set_client (portInfo, sourceClient); snd_seq_port_info_set_port (portInfo, -1); while (--numPorts >= 0) { if (snd_seq_query_next_port (seqHandle, portInfo) == 0 && (snd_seq_port_info_get_capability (portInfo) & (forInput ? SND_SEQ_PORT_CAP_SUBS_READ : SND_SEQ_PORT_CAP_SUBS_WRITE)) != 0) { String portName (snd_seq_port_info_get_name (portInfo)); auto portID = snd_seq_port_info_get_port (portInfo); MidiDeviceInfo device (portName, getFormattedPortIdentifier (sourceClient, portID)); devices.add (device); if (deviceIdentifierToOpen.isNotEmpty() && deviceIdentifierToOpen == device.identifier) { if (portID != -1) { port = client.createPort (portName, forInput, false); jassert (port->isValid()); port->connectWith (sourceClient, portID); break; } } } } return port; } static AlsaClient::Port* iterateMidiDevices (bool forInput, Array& devices, const String& deviceIdentifierToOpen) { AlsaClient::Port* port = nullptr; auto client = AlsaClient::getInstance(); if (auto seqHandle = client->get()) { snd_seq_system_info_t* systemInfo = nullptr; snd_seq_client_info_t* clientInfo = nullptr; snd_seq_system_info_alloca (&systemInfo); jassert (systemInfo != nullptr); if (snd_seq_system_info (seqHandle, systemInfo) == 0) { snd_seq_client_info_alloca (&clientInfo); jassert (clientInfo != nullptr); auto numClients = snd_seq_system_info_get_cur_clients (systemInfo); while (--numClients >= 0) { if (snd_seq_query_next_client (seqHandle, clientInfo) == 0) { port = iterateMidiClient (*client, clientInfo, forInput, devices, deviceIdentifierToOpen); if (port != nullptr) break; } } } } return port; } struct AlsaPortPtr { explicit AlsaPortPtr (AlsaClient::Port* p) : ptr (p) {} virtual ~AlsaPortPtr() noexcept { AlsaClient::getInstance()->deletePort (ptr); } AlsaClient::Port* ptr = nullptr; }; //============================================================================== class MidiInput::Pimpl final : public AlsaPortPtr { public: using AlsaPortPtr::AlsaPortPtr; }; Array MidiInput::getAvailableDevices() { Array devices; iterateMidiDevices (true, devices, {}); return devices; } MidiDeviceInfo MidiInput::getDefaultDevice() { return getAvailableDevices().getFirst(); } std::unique_ptr MidiInput::openDevice (const String& deviceIdentifier, MidiInputCallback* callback) { if (deviceIdentifier.isEmpty()) return {}; Array devices; auto* port = iterateMidiDevices (true, devices, deviceIdentifier); if (port == nullptr || ! port->isValid()) return {}; jassert (port->isValid()); std::unique_ptr midiInput (new MidiInput (port->getPortName(), deviceIdentifier)); port->setupInput (midiInput.get(), callback); midiInput->internal = std::make_unique (port); return midiInput; } std::unique_ptr MidiInput::createNewDevice (const String& deviceName, MidiInputCallback* callback) { auto client = AlsaClient::getInstance(); auto* port = client->createPort (deviceName, true, true); if (port == nullptr || ! port->isValid()) return {}; std::unique_ptr midiInput (new MidiInput (deviceName, getFormattedPortIdentifier (client->getId(), port->getPortId()))); port->setupInput (midiInput.get(), callback); midiInput->internal = std::make_unique (port); return midiInput; } StringArray MidiInput::getDevices() { StringArray deviceNames; for (auto& d : getAvailableDevices()) deviceNames.add (d.name); deviceNames.appendNumbersToDuplicates (true, true); return deviceNames; } int MidiInput::getDefaultDeviceIndex() { return 0; } std::unique_ptr MidiInput::openDevice (int index, MidiInputCallback* callback) { return openDevice (getAvailableDevices()[index].identifier, callback); } MidiInput::MidiInput (const String& deviceName, const String& deviceIdentifier) : deviceInfo (deviceName, deviceIdentifier) { } MidiInput::~MidiInput() { stop(); } void MidiInput::start() { internal->ptr->enableCallback (true); } void MidiInput::stop() { internal->ptr->enableCallback (false); } //============================================================================== class MidiOutput::Pimpl final : public AlsaPortPtr { public: using AlsaPortPtr::AlsaPortPtr; }; Array MidiOutput::getAvailableDevices() { Array devices; iterateMidiDevices (false, devices, {}); return devices; } MidiDeviceInfo MidiOutput::getDefaultDevice() { return getAvailableDevices().getFirst(); } std::unique_ptr MidiOutput::openDevice (const String& deviceIdentifier) { if (deviceIdentifier.isEmpty()) return {}; Array devices; auto* port = iterateMidiDevices (false, devices, deviceIdentifier); if (port == nullptr || ! port->isValid()) return {}; std::unique_ptr midiOutput (new MidiOutput (port->getPortName(), deviceIdentifier)); port->setupOutput(); midiOutput->internal = std::make_unique (port); return midiOutput; } std::unique_ptr MidiOutput::createNewDevice (const String& deviceName) { auto client = AlsaClient::getInstance(); auto* port = client->createPort (deviceName, false, true); if (port == nullptr || ! port->isValid()) return {}; std::unique_ptr midiOutput (new MidiOutput (deviceName, getFormattedPortIdentifier (client->getId(), port->getPortId()))); port->setupOutput(); midiOutput->internal = std::make_unique (port); return midiOutput; } StringArray MidiOutput::getDevices() { StringArray deviceNames; for (auto& d : getAvailableDevices()) deviceNames.add (d.name); deviceNames.appendNumbersToDuplicates (true, true); return deviceNames; } int MidiOutput::getDefaultDeviceIndex() { return 0; } std::unique_ptr MidiOutput::openDevice (int index) { return openDevice (getAvailableDevices()[index].identifier); } MidiOutput::~MidiOutput() { stopBackgroundThread(); } void MidiOutput::sendMessageNow (const MidiMessage& message) { internal->ptr->sendMessageNow (message); } MidiDeviceListConnection MidiDeviceListConnection::make (std::function cb) { auto& broadcaster = MidiDeviceListConnectionBroadcaster::get(); // We capture the AlsaClient instance here to ensure that it remains alive for at least as long // as the MidiDeviceListConnection. This is necessary because system change messages will only // be processed when the AlsaClient's SequencerThread is running. return { &broadcaster, broadcaster.add ([fn = std::move (cb), client = AlsaClient::getInstance()] { NullCheckedInvocation::invoke (fn); }) }; } //============================================================================== #else class MidiInput::Pimpl {}; // (These are just stub functions if ALSA is unavailable...) MidiInput::MidiInput (const String& deviceName, const String& deviceID) : deviceInfo (deviceName, deviceID) { } MidiInput::~MidiInput() {} void MidiInput::start() {} void MidiInput::stop() {} Array MidiInput::getAvailableDevices() { return {}; } MidiDeviceInfo MidiInput::getDefaultDevice() { return {}; } std::unique_ptr MidiInput::openDevice (const String&, MidiInputCallback*) { return {}; } std::unique_ptr MidiInput::createNewDevice (const String&, MidiInputCallback*) { return {}; } StringArray MidiInput::getDevices() { return {}; } int MidiInput::getDefaultDeviceIndex() { return 0;} std::unique_ptr MidiInput::openDevice (int, MidiInputCallback*) { return {}; } class MidiOutput::Pimpl {}; MidiOutput::~MidiOutput() {} void MidiOutput::sendMessageNow (const MidiMessage&) {} Array MidiOutput::getAvailableDevices() { return {}; } MidiDeviceInfo MidiOutput::getDefaultDevice() { return {}; } std::unique_ptr MidiOutput::openDevice (const String&) { return {}; } std::unique_ptr MidiOutput::createNewDevice (const String&) { return {}; } StringArray MidiOutput::getDevices() { return {}; } int MidiOutput::getDefaultDeviceIndex() { return 0;} std::unique_ptr MidiOutput::openDevice (int) { return {}; } MidiDeviceListConnection MidiDeviceListConnection::make (std::function cb) { auto& broadcaster = MidiDeviceListConnectionBroadcaster::get(); return { &broadcaster, broadcaster.add (std::move (cb)) }; } #endif } // namespace juce