/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2016 - ROLI Ltd. Permission is granted to use this software 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. THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ----------------------------------------------------------------------------- To release a closed-source product which uses other parts of JUCE not licensed under the ISC terms, commercial licenses are available: visit www.juce.com for more information. ============================================================================== */ #if JUCE_ALSA // You can define these strings in your app if you want to override the default names: #ifndef JUCE_ALSA_MIDI_NAME #define JUCE_ALSA_MIDI_NAME JUCEApplicationBase::getInstance()->getApplicationName().toUTF8() #endif //============================================================================== namespace { //============================================================================== class AlsaClient : public ReferenceCountedObject { public: typedef ReferenceCountedObjectPtr Ptr; //============================================================================== // represents an input or output port of the supplied AlsaClient class Port { public: Port (AlsaClient& c, bool forInput) noexcept : portId (-1), callbackEnabled (false), client (c), isInput (forInput), callback (nullptr), maxEventSize (4 * 1024), midiInput (nullptr) {} ~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 && input); callback = cb; midiInput = input; } void setupOutput() { jassert (! isInput); snd_midi_event_new ((size_t) maxEventSize, &midiParser); } void enableCallback (bool enable) { if (callbackEnabled != enable) { callbackEnabled = enable; if (enable) client.registerCallback(); else client.unregisterCallback(); } } 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); long numBytes = (long) message.getRawDataSize(); const uint8* data = message.getRawData(); snd_seq_t* seqHandle = client.get(); bool success = true; while (numBytes > 0) { const long 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, 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; } int portId; bool callbackEnabled; private: friend class AlsaClient; AlsaClient& client; bool isInput; MidiInputCallback* callback; snd_midi_event_t* midiParser; int maxEventSize; MidiInput* midiInput; void createPort (const String& name, bool enableSubscription) { if (snd_seq_t* 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_WRITE | (enableSubscription ? SND_SEQ_PORT_CAP_SUBS_READ : 0)); portId = snd_seq_create_simple_port (seqHandle, name.toUTF8(), caps, SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION); } } void handleIncomingMidiMessage (const MidiMessage& message) const { callback->handleIncomingMidiMessage (midiInput, message); } void handlePartialSysexMessage (const uint8* messageData, int numBytesSoFar, double timeStamp) { callback->handlePartialSysexMessage (midiInput, messageData, numBytesSoFar, timeStamp); } }; static Ptr getInstance() { if (instance == nullptr) instance = new AlsaClient(); return instance; } void registerCallback () { if (inputThread == nullptr) inputThread = new MidiInputThread (*this); if (++activeCallbacks - 1 == 0) inputThread->startThread(); } void unregisterCallback () { jassert (activeCallbacks.get() > 0); if (--activeCallbacks == 0 && inputThread->isThreadRunning()) inputThread->signalThreadShouldExit(); } void handleIncomingMidiMessage (snd_seq_event* event, const MidiMessage& message) { if (event->dest.port < ports.size() && ports[event->dest.port]->callbackEnabled) ports[event->dest.port]->handleIncomingMidiMessage (message); } void handlePartialSysexMessage (snd_seq_event* event, const uint8* messageData, int numBytesSoFar, double timeStamp) { if (event->dest.port < ports.size() && ports[event->dest.port]->callbackEnabled) ports[event->dest.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) { Port* port = new Port (*this, forInput); port->createPort (name, enableSubscription); ports.set (port->portId, port); incReferenceCount(); return port; } void deletePort (Port* port) { ports.remove (port->portId); decReferenceCount(); } private: snd_seq_t* handle; int clientId; OwnedArray ports; Atomic activeCallbacks; CriticalSection callbackLock; static AlsaClient* instance; //============================================================================== friend class ReferenceCountedObjectPtr; friend struct ContainerDeletePolicy; AlsaClient () : handle (nullptr), inputThread (nullptr) { jassert (instance == nullptr); snd_seq_open (&handle, "default", SND_SEQ_OPEN_DUPLEX, 0); snd_seq_nonblock (handle, SND_SEQ_NONBLOCK); snd_seq_set_client_name (handle, JUCE_ALSA_MIDI_NAME); clientId = snd_seq_client_id(handle); // It's good idea to pre-allocate a good number of elements ports.ensureStorageAllocated (32); } ~AlsaClient() { jassert (instance != nullptr); instance = nullptr; if (handle != nullptr) snd_seq_close (handle); jassert (activeCallbacks.get() == 0); if (inputThread) inputThread->stopThread (3000); } //============================================================================== class MidiInputThread : public Thread { public: MidiInputThread (AlsaClient& c) : Thread ("Juce MIDI Input"), client (c), concatenator (2048) { jassert (client.get() != nullptr); } void run() override { 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 ((size_t) numPfds); snd_seq_poll_descriptors (seqHandle, pfd, (unsigned int) numPfds, POLLIN); HeapBlock buffer (maxEventSize); while (! threadShouldExit()) { if (poll (pfd, (nfds_t) 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 { if (threadShouldExit()) break; do { snd_seq_event_t* inputEvent = nullptr; if (snd_seq_event_input (seqHandle, &inputEvent) >= 0) { // xxx what about SYSEXes that are too big for the buffer? const long numBytes = snd_midi_event_decode (midiParser, buffer, maxEventSize, inputEvent); snd_midi_event_reset_decode (midiParser); concatenator.pushMidiData (buffer, (int) numBytes, Time::getMillisecondCounter() * 0.001, inputEvent, client); snd_seq_free_event (inputEvent); } } while (snd_seq_event_input_pending (seqHandle, 0) > 0); } } snd_midi_event_free (midiParser); } }; private: AlsaClient& client; MidiDataConcatenator concatenator; }; ScopedPointer inputThread; }; AlsaClient* AlsaClient::instance = nullptr; //============================================================================== static AlsaClient::Port* iterateMidiClient (const AlsaClient::Ptr& client, snd_seq_client_info_t* clientInfo, const bool forInput, StringArray& deviceNamesFound, const int deviceIndexToOpen) { AlsaClient::Port* port = nullptr; snd_seq_t* seqHandle = client->get(); snd_seq_port_info_t* portInfo = nullptr; snd_seq_port_info_alloca (&portInfo); jassert (portInfo); int numPorts = snd_seq_client_info_get_num_ports (clientInfo); const int 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_WRITE : SND_SEQ_PORT_CAP_SUBS_READ)) != 0) { const String portName = snd_seq_port_info_get_name(portInfo); deviceNamesFound.add (portName); if (deviceNamesFound.size() == deviceIndexToOpen + 1) { const int sourcePort = snd_seq_port_info_get_port (portInfo); if (sourcePort != -1) { port = client->createPort (portName, forInput, false); jassert (port->isValid()); port->connectWith (sourceClient, sourcePort); break; } } } } return port; } static AlsaClient::Port* iterateMidiDevices (const bool forInput, StringArray& deviceNamesFound, const int deviceIndexToOpen) { AlsaClient::Port* port = nullptr; const AlsaClient::Ptr client (AlsaClient::getInstance()); if (snd_seq_t* const 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); if (snd_seq_system_info (seqHandle, systemInfo) == 0) { snd_seq_client_info_alloca (&clientInfo); jassert(clientInfo); int numClients = snd_seq_system_info_get_cur_clients (systemInfo); while (--numClients >= 0) { if (snd_seq_query_next_client (seqHandle, clientInfo) == 0) { const int sourceClient = snd_seq_client_info_get_client (clientInfo); if (sourceClient != client->getId() && sourceClient != SND_SEQ_CLIENT_SYSTEM) { port = iterateMidiClient (client, clientInfo, forInput, deviceNamesFound, deviceIndexToOpen); if (port) break; } } } } } deviceNamesFound.appendNumbersToDuplicates (true, true); return port; } } // namespace StringArray MidiOutput::getDevices() { StringArray devices; iterateMidiDevices (false, devices, -1); return devices; } int MidiOutput::getDefaultDeviceIndex() { return 0; } MidiOutput* MidiOutput::openDevice (int deviceIndex) { MidiOutput* newDevice = nullptr; StringArray devices; AlsaClient::Port* port = iterateMidiDevices (false, devices, deviceIndex); if (port == nullptr) return nullptr; jassert (port->isValid()); newDevice = new MidiOutput (devices [deviceIndex]); port->setupOutput(); newDevice->internal = port; return newDevice; } MidiOutput* MidiOutput::createNewDevice (const String& deviceName) { MidiOutput* newDevice = nullptr; const AlsaClient::Ptr client (AlsaClient::getInstance()); AlsaClient::Port* port = client->createPort (deviceName, false, true); jassert (port->isValid()); newDevice = new MidiOutput (deviceName); port->setupOutput(); newDevice->internal = port; return newDevice; } MidiOutput::~MidiOutput() { stopBackgroundThread(); AlsaClient::Ptr client (AlsaClient::getInstance()); client->deletePort (static_cast (internal)); } void MidiOutput::sendMessageNow (const MidiMessage& message) { static_cast (internal)->sendMessageNow (message); } //============================================================================== MidiInput::MidiInput (const String& nm) : name (nm), internal (nullptr) { } MidiInput::~MidiInput() { stop(); AlsaClient::Ptr client (AlsaClient::getInstance()); client->deletePort (static_cast (internal)); } void MidiInput::start() { static_cast (internal)->enableCallback (true); } void MidiInput::stop() { static_cast (internal)->enableCallback (false); } int MidiInput::getDefaultDeviceIndex() { return 0; } StringArray MidiInput::getDevices() { StringArray devices; iterateMidiDevices (true, devices, -1); return devices; } MidiInput* MidiInput::openDevice (int deviceIndex, MidiInputCallback* callback) { MidiInput* newDevice = nullptr; StringArray devices; AlsaClient::Port* port = iterateMidiDevices (true, devices, deviceIndex); if (port == nullptr) return nullptr; jassert (port->isValid()); newDevice = new MidiInput (devices [deviceIndex]); port->setupInput (newDevice, callback); newDevice->internal = port; return newDevice; } MidiInput* MidiInput::createNewDevice (const String& deviceName, MidiInputCallback* callback) { MidiInput* newDevice = nullptr; AlsaClient::Ptr client (AlsaClient::getInstance()); AlsaClient::Port* port = client->createPort (deviceName, true, true); jassert (port->isValid()); newDevice = new MidiInput (deviceName); port->setupInput (newDevice, callback); newDevice->internal = port; return newDevice; } //============================================================================== #else // (These are just stub functions if ALSA is unavailable...) StringArray MidiOutput::getDevices() { return StringArray(); } int MidiOutput::getDefaultDeviceIndex() { return 0; } MidiOutput* MidiOutput::openDevice (int) { return nullptr; } MidiOutput* MidiOutput::createNewDevice (const String&) { return nullptr; } MidiOutput::~MidiOutput() {} void MidiOutput::sendMessageNow (const MidiMessage&) {} MidiInput::MidiInput (const String& nm) : name (nm), internal (nullptr) {} MidiInput::~MidiInput() {} void MidiInput::start() {} void MidiInput::stop() {} int MidiInput::getDefaultDeviceIndex() { return 0; } StringArray MidiInput::getDevices() { return StringArray(); } MidiInput* MidiInput::openDevice (int, MidiInputCallback*) { return nullptr; } MidiInput* MidiInput::createNewDevice (const String&, MidiInputCallback*) { return nullptr; } #endif