diff --git a/.vscode/launch.json b/.vscode/launch.json index bb49bf9..9dce54a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -142,7 +142,7 @@ //"[pipedal_alsa_test]" // "[wifi_channels_test]" // "[locale]" - "[pipedal_alsa_seq_test]" + "[pipewire_input_stream]" ], "stopAtEntry": false, diff --git a/CMakeLists.txt b/CMakeLists.txt index eb19d8c..b9e7e49 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ set (DISPLAY_VERSION "PiPedal v1.4.76-Release") set (PACKAGE_ARCHITECTURE ${DEBIAN_ARCHITECTURE}) set (CMAKE_INSTALL_PREFIX "/usr/") +set (PIPEDAL_EXCLUDE_TESTS false) include(CTest) enable_testing() diff --git a/PiPedalCommon/src/AlsaSequencer.cpp b/PiPedalCommon/src/AlsaSequencer.cpp index 28be46f..e4c11d1 100644 --- a/PiPedalCommon/src/AlsaSequencer.cpp +++ b/PiPedalCommon/src/AlsaSequencer.cpp @@ -24,18 +24,113 @@ #include "AlsaSequencer.hpp" #include #include +#include #include "ss.hpp" +#include +#include +#include +#include +#include +#include "Finally.hpp" +#include "Lv2Log.hpp" +#include +#include // enumerate alsa sequencer ports namespace pipedal { + + namespace impl + { + + enum class ConnectAction + { + Subscribe, + Unsubscribe + }; + + class AlsaSequencerImpl : public AlsaSequencer + { + public: + AlsaSequencerImpl(); + + public: + ~AlsaSequencerImpl(); + + virtual void ConnectPort(int clientId, int portId) override; + virtual void ConnectPort(const std::string &name) override; + virtual void SetConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) override; + + // Read a single MIDI message from the sequencer input port. A timeout of -1 blocks indefinitely. + // A timeout of 0 returns immediately. + virtual bool ReadMessage(AlsaMidiMessage &message, int timeoutMs = -1) override; + + // Get current real-time from the queue (useful for calculating precise timing) + virtual bool GetQueueRealtime(uint64_t *sec, uint32_t *nsec) override; + + virtual void RemoveAllConnections() override; + + private: + void ModifyConnection(int clientId, int portId, ConnectAction action); + + // Get the current queue ID (returns -1 if no queue is active) + int GetQueueId() const { return queueId; } + + bool WaitForMessage(int timeoutMs); + // Create an ALSA input queue with real-time timestamps for the given client/port + int CreateRealtimeInputQueue(); + + struct Connection + { + int clientId; + int portId; + }; + + int myClientId = -1; + + std::mutex connectionsMutex; + std::vector connections; + std::vector pollFds; // For polling input events + snd_seq_t *seqHandle = nullptr; + int inPort = -1; + int queueId = -1; // Queue for real-time timestamps + }; + + class AlsaSequencerDeviceMonitorImpl : public AlsaSequencerDeviceMonitor + { + + public: + virtual ~AlsaSequencerDeviceMonitorImpl() override; + + virtual void StartMonitoring( + Callback &&onChangeCallback) override; + virtual void StopMonitoring() override; + + private: + int CreateInputQueue(snd_seq_t *seqHandle, int inPort); + + bool started = false; + void ServiceProc(); + std::unique_ptr serviceThread; + std::atomic terminateThread{false}; + Callback callback; + }; + + }; + using namespace impl; + + AlsaSequencer::ptr AlsaSequencer::Create() + { + return std::make_shared(); + } + std::vector AlsaSequencer::EnumeratePorts() { std::vector ports; snd_seq_t *seq; - if (snd_seq_open(&seq, "default", SND_SEQ_OPEN_INPUT, 0) < 0) + if (snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0) { return ports; } @@ -81,8 +176,11 @@ namespace pipedal port.canReadSubscribe = capability & SND_SEQ_PORT_CAP_SUBS_READ; port.canWriteSubscribe = capability & SND_SEQ_PORT_CAP_SUBS_WRITE; auto typeBits = snd_seq_port_info_get_type(port_info); - +#ifdef SND_SEQ_PORT_TYPE_MIDI_UMP port.isUmp = (typeBits & SND_SEQ_PORT_TYPE_MIDI_UMP) != 0; +#else + port.isUmp = false; // UMP support is not available in all versions of ALSA +#endif port.isSystemAnnounce = (typeBits & SND_SEQ_PORT_SYSTEM_ANNOUNCE) != 0; port.isMidiSynth = (typeBits & SND_SEQ_PORT_TYPE_MIDI_GENERIC) != 0 || (typeBits & SND_SEQ_PORT_TYPE_MIDI_GM) != 0 || @@ -92,19 +190,61 @@ namespace pipedal port.isSpecific = (typeBits & SND_SEQ_PORT_TYPE_SPECIFIC) != 0; port.isSynth = (typeBits & SND_SEQ_PORT_TYPE_SYNTH) != 0; port.isHardware = (typeBits & SND_SEQ_PORT_TYPE_HARDWARE) != 0; + port.isPort = (typeBits & SND_SEQ_PORT_TYPE_PORT) != 0; port.isSoftware = (typeBits & SND_SEQ_PORT_TYPE_SOFTWARE) != 0; - + port.isVirtual = port.name.starts_with("VirMIDI"); port.cardNumber = snd_seq_client_info_get_card(client_info); port.port = snd_seq_port_info_get_port(port_info); - if (port.isKernelDevice & port.cardNumber >= 0 && port.port >= 0) + if (port.isKernelDevice && port.isPort && port.cardNumber >= 0 && port.port >= 0) { // For kernel devices, we can construct a raw MIDI device string - port.rawMidiDevice = SS("hw:" << port.cardNumber << ",0," << port.port); + std::string rawMidiDevice; + if (port.isVirtual) + { + // "VirMidI 2-1" + std::regex virtualDeviceRegex{"^(VirMIDI) (\\d+)-(\\d+)$"}; + { + // Extract the card and device numbers from the match + std::smatch match; + std::regex_search(port.name, match, virtualDeviceRegex); + if (match.size() == 4) + { + try + { + std::string devName = match[1]; + int card = std::stoi(match[2]); + int device = std::stoi(match[3]); + rawMidiDevice = SS("hw:CARD=" << devName << ",DEV=" << device); + } + catch (const std::exception &ignored) + { + } + } + } + } + else + { + rawMidiDevice = SS("hw:CARD=" << port.clientName << ",DEV=" << 0); + if (port.port > 0) + { + rawMidiDevice += SS("," << port.port); + } + } + port.rawMidiDevice = std::move(rawMidiDevice); } port.id = SS("seq:" << port.clientName << "/" << port.name); - ports.push_back(port); + port.displaySortOrder = port.client * 256 + port.port; + if (port.isVirtual) + { + port.displaySortOrder += 1 * 256 * 256; // MIDI virtual ports after real ports. + } + else if (port.clientName == "Midi Through") + { + port.displaySortOrder += 2 * 256 * 256; // MIDI Through at the very end because it's weird. + } + ports.push_back(std::move(port)); } } @@ -112,7 +252,7 @@ namespace pipedal return ports; } - AlsaSequencer::AlsaSequencer() + AlsaSequencerImpl::AlsaSequencerImpl() { seqHandle = nullptr; queueId = -1; @@ -125,24 +265,48 @@ namespace pipedal // convert rc to message throw std::runtime_error(SS("Failed to open ALSA sequencer:" << snd_strerror(rc))); } + size_t inputBufferSize = snd_seq_get_input_buffer_size(seqHandle); + (void)inputBufferSize; + + rc = snd_seq_set_input_buffer_size(seqHandle, 128 * 1024); + if (rc < 0) + { + Lv2Log::warning("Failed resize the ALSA sequencer input buffer: %s", snd_strerror(rc)); + } snd_seq_set_client_name(seqHandle, "PiPedal"); inPort = snd_seq_create_simple_port(seqHandle, "PiPedal:in", - SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, - SND_SEQ_PORT_TYPE_MIDI_GENERIC | - SND_SEQ_PORT_TYPE_MIDI_GM | SND_SEQ_PORT_TYPE_APPLICATION); + SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, + SND_SEQ_PORT_TYPE_MIDI_GENERIC | + SND_SEQ_PORT_TYPE_MIDI_GM | SND_SEQ_PORT_TYPE_APPLICATION); if (inPort < 0) { // convert rc to message throw std::runtime_error(SS("Failed to open ALSA sequencer:" << snd_strerror(inPort))); } - } - AlsaSequencer::~AlsaSequencer() - { - for (const auto &connection : connections) + CreateRealtimeInputQueue(); + + snd_seq_nonblock(seqHandle, 1); // Set sequencer to non-blocking mode + + // Get our client and port numbers for reference + myClientId = snd_seq_client_id(seqHandle); + if (myClientId < 0) { - snd_seq_disconnect_from(seqHandle, inPort, connection.clientId, connection.portId); + throw std::runtime_error(SS("Failed to get client ID: " << snd_strerror(myClientId))); } + } + void AlsaSequencerImpl::RemoveAllConnections() + { + while (connections.size() != 0) + { + auto connection = connections.back(); + ModifyConnection(connection.clientId, connection.portId, ConnectAction::Unsubscribe); + } + } + AlsaSequencerImpl::~AlsaSequencerImpl() + { + RemoveAllConnections(); + if (queueId >= 0) { snd_seq_free_queue(seqHandle, queueId); @@ -160,25 +324,17 @@ namespace pipedal } } - void AlsaSequencer::ConnectPort(int clientId, int portId) + void AlsaSequencerImpl::ConnectPort(int clientId, int portId) { - - CreateRealtimeInputQueue(); - // Connect this input port to the target client:port - int rc = snd_seq_connect_from(seqHandle, inPort, clientId, portId); - if (rc < 0) - { - throw std::runtime_error(SS("Failed to connect to ALSA port: " << snd_strerror(rc))); - } - this->connections.push_back({clientId, portId}); + ModifyConnection(clientId, portId, ConnectAction::Subscribe); } - void AlsaSequencer::ConnectPort(const std::string &name) + void AlsaSequencerImpl::ConnectPort(const std::string &id) { auto ports = EnumeratePorts(); for (const auto &port : ports) { - if (port.name == name) + if (port.id == id) { ConnectPort(port.client, port.port); return; @@ -186,73 +342,303 @@ namespace pipedal } throw std::runtime_error("ALSA port not found"); } + void AlsaSequencerImpl::SetConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) + { + this->RemoveAllConnections(); // Currently no configuration options to set - bool AlsaSequencer::ReadMessage(AlsaMidiMessage &message) + auto ports = EnumeratePorts(); + for (const auto &connection : alsaSequencerConfiguration.connections()) + { + // Connect to each port specified in the configuration + std::string id = connection.id(); + for (const auto &port : ports) + { + if (port.id == id) + { + ConnectPort(port.client, port.port); + break; + } + } + } + } + + bool AlsaSequencerImpl::WaitForMessage(int timeoutMs) + { + while (true) + { + auto fdCount = snd_seq_poll_descriptors_count(seqHandle, POLLIN); + if (fdCount == 0) + return true; + this->pollFds.resize(fdCount); + + snd_seq_poll_descriptors(seqHandle, pollFds.data(), fdCount, POLLIN); + + int rc = poll(pollFds.data(), fdCount, timeoutMs); + if (rc == 0) + { + return false; // timed out. + } + else if (rc < 0) + { + if (errno == EINTR) + { + continue; + } + else + { + throw std::runtime_error(SS("ALSA sequencer poll error: " << strerror(errno))); + } + } + else + { + return true; + } + } + } + bool AlsaSequencerImpl::ReadMessage(AlsaMidiMessage &message, int timeoutMs) { // Event loop snd_seq_event_t *event = nullptr; - bool success = false; - if (snd_seq_event_input(seqHandle, &event) >= 0 && event) + while (true) { - success = true; - // Extract timestamp information - message.timestamp = event->time.tick; - message.realtime_sec = event->time.time.tv_sec; - message.realtime_nsec = event->time.time.tv_nsec; - - // Process MIDI event here, e.g. NOTEON, NOTEOFF, etc. - switch (event->type) + bool success = false; + int rc = snd_seq_event_input(seqHandle, &event); + if (rc < 0) { - case SND_SEQ_EVENT_NOTEON: - message.cc0 = 0x90 | event->data.note.channel; // channel - message.cc1 = event->data.note.note; // note - message.cc2 = event->data.note.velocity; // velocity - break; - case SND_SEQ_EVENT_NOTEOFF: - // handle note-off - message.cc0 = 0x80 | event->data.note.channel; // channel - message.cc1 = event->data.note.note; // note - message.cc2 = event->data.note.off_velocity; // off velocity - break; - case SND_SEQ_EVENT_KEYPRESS: - message.cc0 = 0xA0 | event->data.note.channel; // polyphonic key pressure - message.cc1 = event->data.note.note; // note - message.cc2 = event->data.note.velocity; // pressure - break; - case SND_SEQ_EVENT_CONTROLLER: - message.cc0 = 0xB0 | event->data.control.channel; // control change - message.cc1 = event->data.control.param; // controller number - message.cc2 = event->data.control.value; // controller value - break; - case SND_SEQ_EVENT_PGMCHANGE: - message.cc0 = 0xC0 | event->data.control.channel; // program change - message.cc1 = event->data.control.value; // program number - message.cc2 = 0; // unused - break; - case SND_SEQ_EVENT_CHANPRESS: - message.cc0 = 0xD0 | event->data.control.channel; // channel pressure - message.cc1 = event->data.control.value; // pressure value - message.cc2 = 0; // unused - break; - case SND_SEQ_EVENT_PITCHBEND: - message.cc0 = 0xE0 | event->data.control.channel; // pitch bend - message.cc1 = (event->data.control.value >> 7) & 0x7F; // MSB - message.cc2 = event->data.control.value & 0x7F; // LSB - break; - case SND_SEQ_EVENT_CONTROL14: - case SND_SEQ_EVENT_NONREGPARAM: - case SND_SEQ_EVENT_REGPARAM: - case SND_SEQ_EVENT_SONGPOS: - default: - success = false; - break; + if (rc == -EAGAIN) + { + if (!WaitForMessage(timeoutMs)) + { + return false; + } + } + else + { + // Handle other errors + throw std::runtime_error(SS("ALSA sequencer input error: " << snd_strerror(rc))); + } + } + else if (event) + { + success = true; + // Extract timestamp information + message.timestamp = event->time.tick; + message.realtime_sec = event->time.time.tv_sec; + message.realtime_nsec = event->time.time.tv_nsec; + + // Process MIDI event here, e.g. NOTEON, NOTEOFF, etc. + switch (event->type) + { + case SND_SEQ_EVENT_NOTEON: + message.Set( + (uint8_t)(0x90 | event->data.note.channel), + (uint8_t)(event->data.note.note), // note + (uint8_t)(event->data.note.velocity)); // velocity + break; + case SND_SEQ_EVENT_NOTEOFF: + // handle note-off + message.Set( + uint8_t(0x80 | event->data.note.channel), + uint8_t(event->data.note.note), + uint8_t(event->data.note.off_velocity)); // off velocity + break; + case SND_SEQ_EVENT_KEYPRESS: + message.Set( + (uint8_t)(0xA0 | event->data.note.channel), // polyphonic key pressure + (uint8_t)(event->data.note.note), // note + (uint8_t)(event->data.note.velocity)); // pressure + break; + case SND_SEQ_EVENT_CONTROLLER: + message.Set( + (uint8_t)(0xB0 | event->data.control.channel), // control change + (uint8_t)(event->data.control.param), // controller number + (uint8_t)(event->data.control.value)); // controller value + break; + case SND_SEQ_EVENT_PGMCHANGE: + message.Set( + (uint8_t)(0xC0 | event->data.control.channel), // program change + (uint8_t)(event->data.control.value)); + break; + + case SND_SEQ_EVENT_CHANPRESS: + message.Set( + uint8_t(0xD0 | event->data.control.channel), + uint8_t(event->data.control.value)); + break; + case SND_SEQ_EVENT_PITCHBEND: + message.Set(uint8_t(0xE0 | event->data.control.channel), + uint8_t((event->data.control.value >> 7) & 0x7F), + uint8_t(event->data.control.value & 0x7F)); + break; + case SND_SEQ_EVENT_CONTROL14: + message.size = 6; + message.data = message.fixedBuffer; + message.fixedBuffer[0] = uint8_t(0xB0 | event->data.control.channel); // Control Change 14-bit + message.fixedBuffer[1] = uint8_t(event->data.control.param); // MSB + message.fixedBuffer[2] = uint8_t(event->data.control.param >> 7) & 0x7F; // MSB + message.fixedBuffer[3] = uint8_t(0xB0 | event->data.control.channel); + message.fixedBuffer[4] = uint8_t(event->data.control.value + 0x20); + message.fixedBuffer[5] = uint8_t(event->data.control.value) & 0x7F; // MSB value + break; + + case SND_SEQ_EVENT_NONREGPARAM: + message.size = 12; + message.data = message.fixedBuffer; + message.fixedBuffer[0] = uint8_t(0xB0 | event->data.control.channel); // Non-registered parameter + message.fixedBuffer[1] = 0x63; // MSB + message.fixedBuffer[2] = uint8_t(event->data.control.param >> 7) & 0x7F; // MSB + message.fixedBuffer[3] = uint8_t(0xB0 | event->data.control.channel); + message.fixedBuffer[4] = 0x62; // LSB + message.fixedBuffer[5] = uint8_t(event->data.control.param) & 0x7F; // LSB + message.fixedBuffer[6] = uint8_t(0xB0 | event->data.control.channel); + message.fixedBuffer[7] = uint8_t(0x06); // Non-registered parameter value MSB + message.fixedBuffer[8] = uint8_t(event->data.control.value >> 7) & 0x7F; // MSB value + message.fixedBuffer[9] = uint8_t(0xB0 | event->data.control.channel); + message.fixedBuffer[10] = uint8_t(0x26); // Non-registered parameter value LSB + message.fixedBuffer[11] = uint8_t(event->data.control.value) & 0x7F; // LSb + break; + case SND_SEQ_EVENT_REGPARAM: + message.size = 12; + message.data = message.fixedBuffer; + message.fixedBuffer[0] = uint8_t(0xB0 | event->data.control.channel); // Registered parameter + message.fixedBuffer[1] = uint8_t(0x65); // MSB + message.fixedBuffer[2] = uint8_t(event->data.control.param >> 7) & 0x7F; // MSB + message.fixedBuffer[3] = uint8_t(0xB0 | event->data.control.channel); + message.fixedBuffer[4] = uint8_t(0x64); // LSB + message.fixedBuffer[5] = uint8_t(event->data.control.param) & 0x7F; // LSB + message.fixedBuffer[6] = uint8_t(0xB0 | event->data.control.channel); + message.fixedBuffer[7] = uint8_t(0x6); // Registered parameter value MSB + message.fixedBuffer[8] = uint8_t(event->data.control.value >> 7) & 0x7F; // MSB value + message.fixedBuffer[9] = uint8_t(0xB0 | event->data.control.channel); + message.fixedBuffer[10] = uint8_t(0x26); // Registered parameter value LSB + message.fixedBuffer[11] = uint8_t(event->data.control.value) & 0x7F; // LSB value + break; + case SND_SEQ_EVENT_SONGPOS: + message.Set( + 0xF2, + (uint8_t)((event->data.control.value >> 7) & 0x7F), // MSB + (uint8_t)(event->data.control.value & 0x7F) // LSB + ); + break; + case SND_SEQ_EVENT_SONGSEL: + message.Set( + 0xF3, + (uint8_t)((event->data.control.value >> 7) & 0x7F), // MSB + (uint8_t)(event->data.control.value & 0x7F) // LSB + ); + break; + case SND_SEQ_EVENT_QFRAME: + message.Set( + 0xF1, + (uint8_t)((event->data.control.value >> 7) & 0x7F), // MSB + (uint8_t)(event->data.control.value & 0x7F) // LSB + ); + break; + case SND_SEQ_EVENT_START: + message.Set(0xFA); // MIDI Real Time Start + break; + case SND_SEQ_EVENT_CONTINUE: + message.Set(0xFB); // MIDI Real Time Continue + break; + case SND_SEQ_EVENT_STOP: + message.Set(0xFC); // MIDI Real Time Stop + break; + case SND_SEQ_EVENT_TICK: + message.Set(0xF8); // MIDI Real Time Clock Tick + break; + case SND_SEQ_EVENT_SENSING: + message.Set(0xFE); // MIDI Real Time Active Sensing + break; + case SND_SEQ_EVENT_RESET: + message.Set(0xFF); // MIDI Real Time System Reset + break; + case SND_SEQ_EVENT_SYSEX: + // Handle SysEx messages + if (event->data.ext.len > 0 && event->data.ext.len + 2 <= sizeof(message.fixedBuffer)) + { + message.size = event->data.ext.len + 1; // +1 for SysEx + message.data = message.fixedBuffer; + message.fixedBuffer[0] = 0xF0; // Start of SysEx + memcpy(message.fixedBuffer + 1, event->data.ext.ptr, event->data.ext.len); + message.fixedBuffer[event->data.ext.len + 1] = 0xF7; // End of SysEx + } + else + { + message.size = event->data.ext.len; + message.data = (uint8_t *)event->data.ext.ptr; + if (message.data[0] != 0xF0) + { + throw std::logic_error("Invalid SysEx message: does not start with 0xF0"); + } + } + break; + case SND_SEQ_EVENT_SETPOS_TICK: + message.size = 3 + sizeof(event->data.queue.param.value); // Tempo events are usually 3 bytes + message.data = message.fixedBuffer; + message.fixedBuffer[0] = 0xFF; // Meta event type for tempo + message.fixedBuffer[1] = (uint8_t)MetaEventType::SetPositionTick; // Meta event subtype for tempo + message.fixedBuffer[2] = (uint8_t)(event->data.queue.queue); // MSB + memcpy(message.fixedBuffer + 3, &event->data.queue.param.value, sizeof(event->data.queue.param.value)); + break; + case SND_SEQ_EVENT_SETPOS_TIME: + message.size = 3 + sizeof(event->data.queue.param.time); // Tempo events are usually 3 bytes + message.data = message.fixedBuffer; + message.fixedBuffer[0] = 0xFF; // Meta event type for tempo + message.fixedBuffer[1] = (uint8_t)MetaEventType::SetPositionTime; // Meta event subtype for tempo + message.fixedBuffer[2] = (uint8_t)(event->data.queue.queue); + memcpy(message.fixedBuffer + 3, &event->data.queue.param.time, sizeof(event->data.queue.param.time)); + break; + case SND_SEQ_EVENT_TEMPO: + // Handle tempo events + message.size = sizeof(event->data.queue.param.value) + 3; // Tempo events are usually 3 bytes + if (message.size > sizeof(message.fixedBuffer)) + { + throw std::logic_error("Tempo event size exceeds fixed buffer size"); + } + message.data = message.fixedBuffer; + message.fixedBuffer[0] = 0xFF; // Meta event type for tempo + message.fixedBuffer[1] = (uint8_t)MetaEventType::Tempo; // Meta event subtype for tempo + message.fixedBuffer[2] = (uint8_t)(event->data.queue.queue); + memcpy(message.fixedBuffer + 3, &event->data.queue.param.value, sizeof(event->data.queue.param.value)); + break; + + case SND_SEQ_EVENT_CLOCK: + message.Set(0xF8); // MIDI Real Time Clock Tick + break; +#ifndef NDEBUG +#define MSG_DEBUG_LOG(x) \ + case x: \ + Lv2Log::debug("ALSA Sequencer Message" #x); \ + break; +#else +#define MSG_DEBUG_LOG(x) +#endif + MSG_DEBUG_LOG(SND_SEQ_EVENT_CLIENT_START) + MSG_DEBUG_LOG(SND_SEQ_EVENT_CLIENT_EXIT) + MSG_DEBUG_LOG(SND_SEQ_EVENT_CLIENT_CHANGE) + MSG_DEBUG_LOG(SND_SEQ_EVENT_PORT_START) + MSG_DEBUG_LOG(SND_SEQ_EVENT_PORT_EXIT) + MSG_DEBUG_LOG(SND_SEQ_EVENT_PORT_CHANGE) + MSG_DEBUG_LOG(SND_SEQ_EVENT_PORT_SUBSCRIBED) + MSG_DEBUG_LOG(SND_SEQ_EVENT_PORT_UNSUBSCRIBED) + + case SND_SEQ_EVENT_KEYSIGN: + case SND_SEQ_EVENT_TIMESIGN: + // and a PASSEL of others! + default: + success = false; + break; + } + snd_seq_free_event(event); + } + if (success) + { + return true; } - snd_seq_free_event(event); } - return success; } - int AlsaSequencer::CreateRealtimeInputQueue() + int AlsaSequencerImpl::CreateRealtimeInputQueue() { if (!seqHandle) { @@ -291,8 +677,6 @@ namespace pipedal throw std::runtime_error(SS("Failed to start queue: " << snd_strerror(rc))); } - - // Set the queue for input timestamping snd_seq_port_info_t *port_info; snd_seq_port_info_alloca(&port_info); @@ -321,7 +705,7 @@ namespace pipedal return queueId; } - bool AlsaSequencer::GetQueueRealtime(uint64_t *sec, uint32_t *nsec) + bool AlsaSequencerImpl::GetQueueRealtime(uint64_t *sec, uint32_t *nsec) { if (!seqHandle || queueId < 0) { @@ -345,4 +729,307 @@ namespace pipedal return true; } -} + + std::string RawMidiIdToSequencerId(const std::vector &seqDevices, const std::string &rawMidiId) + { + for (const auto &device : seqDevices) + { + if (device.rawMidiDevice == rawMidiId) + { + return device.id; + } + } + return {}; + } + + void AlsaSequencerImpl::ModifyConnection(int clientId, int portId, ConnectAction action) + { + std::lock_guard lock(connectionsMutex); + + // use our own seq handle so that we can do this free-threaded. + snd_seq_t *seq; + if (snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0) + { + throw std::runtime_error("Failed to open ALSA sequencer for connection"); + } + Finally seq_finally([seq]() + { snd_seq_close(seq); }); + + + snd_seq_addr_t sender, dest; + dest.client = myClientId; + dest.port = 0; + sender.client = clientId; + sender.port = portId; + + snd_seq_port_subscribe_t *subs; + int queue = this->queueId; + + snd_seq_port_subscribe_alloca(&subs); + snd_seq_port_subscribe_set_sender(subs, &sender); + snd_seq_port_subscribe_set_dest(subs, &dest); + snd_seq_port_subscribe_set_queue(subs, queue); + snd_seq_port_subscribe_set_exclusive(subs, 0); + + if (action == ConnectAction::Unsubscribe) + { + if (snd_seq_get_port_subscription(seq, subs) < 0) + { + Lv2Log::warning( + "Failed to disconnect ALSA sequencer port %d:%d. Subscripton not found.", + (int)clientId, + (int)portId); + } + else + { + int rc = snd_seq_unsubscribe_port(seq, subs); + if (rc < 0) + { + Lv2Log::warning( + "Failed to disconnect ALSA sequencer port %d:%d. (%s)", + (int)clientId, + (int)portId, + snd_strerror(rc)); + } + } + for (auto it = this->connections.begin(); it != this->connections.end(); ++it) + { + if (it->clientId == clientId && it->portId == portId) + { + it = this->connections.erase(it); + break; + } + } + } + else + { + if (snd_seq_get_port_subscription(seq, subs) == 0) + { + Lv2Log::warning("ALSA sequencer port %d:%d is already subscribed.", (int)clientId, (int)portId); + return; + } + int rc = snd_seq_subscribe_port(seq, subs); + if (rc < 0) + { + Lv2Log::error("Failed to connect ALSA sequencer port %d:%d. (%s)", + (int)clientId, (int)portId, + snd_strerror(rc)); + return; + } + this->connections.push_back({clientId, portId}); + } + } + + AlsaSequencerDeviceMonitor::ptr AlsaSequencerDeviceMonitor::Create() + { + return std::make_shared(); + } + + void AlsaSequencerDeviceMonitorImpl::StartMonitoring( + Callback &&onChangeCallback) + { + started = true; + this->callback = std::move(onChangeCallback); + this->serviceThread = std::make_unique( + [this]() + { + ServiceProc(); + }); + } + void AlsaSequencerDeviceMonitorImpl::ServiceProc() + { + int err; + snd_seq_t *seqHandle; + + err = snd_seq_open(&seqHandle, "default", SND_SEQ_OPEN_DUPLEX, 0); + + Finally seq_finally( + [seqHandle]() + { + snd_seq_close(seqHandle); + }); + if (err < 0) + { + Lv2Log::error("Error opening ALSA Device Monitor sequencer: %s", snd_strerror(err)); + return; + } + snd_seq_set_client_name(seqHandle, "Device Monitor"); + + int inPort = snd_seq_create_simple_port( + seqHandle, "PiPedal:portMonitor", + SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, + SND_SEQ_PORT_TYPE_APPLICATION); + if (inPort < 0) + { + Lv2Log::error("Error creating ALSA Device Monitor port: %s", snd_strerror(inPort)); + return; + } + Finally inPort_finaly { + [seqHandle, inPort]() + { + snd_seq_delete_port(seqHandle, inPort); + } + }; + + // Set client name + + int queueId = CreateInputQueue(seqHandle,inPort); + if (queueId < 0) + { + Lv2Log::error("Error creating ALSA Device Monitor queue: %s", snd_strerror(queueId)); + return; + } + Finally queue_finally( + [seqHandle, queueId]() + { + snd_seq_free_queue(seqHandle, queueId); + }); + + // Subscribe to system announcements + snd_seq_port_subscribe_t *subscription; + snd_seq_port_subscribe_alloca(&subscription); + snd_seq_addr_t sender, dest; + + sender.client = SND_SEQ_CLIENT_SYSTEM; + sender.port = SND_SEQ_PORT_SYSTEM_ANNOUNCE; + dest.client = snd_seq_client_id(seqHandle); + dest.port = inPort; + + snd_seq_port_subscribe_set_sender(subscription, &sender); + snd_seq_port_subscribe_set_dest(subscription, &dest); + err = snd_seq_subscribe_port(seqHandle, subscription); + if (err < 0) + { + Lv2Log::error("Failed to subscribe to ALSA sequencer announcements: %s", snd_strerror(err)); + return; + } + + // Create poll descriptors + std::vector pollFds; + + snd_seq_nonblock(seqHandle, 1); // Set sequencer to non-blocking mode + while (!terminateThread) + { + int nPollFds = snd_seq_poll_descriptors_count(seqHandle, POLLIN); + pollFds.resize(nPollFds); + snd_seq_poll_descriptors(seqHandle, pollFds.data(), nPollFds, POLLIN); + + // Poll for events + if (poll(pollFds.data(), nPollFds, 100) > 0) + { + snd_seq_event_t *event; + while (snd_seq_event_input(seqHandle, &event) > 0) + { + if (event->type == SND_SEQ_EVENT_CLIENT_START) + { + // Get the client name for logging/debugging + snd_seq_client_info_t *client_info; + snd_seq_client_info_alloca(&client_info); + if (snd_seq_get_any_client_info(seqHandle, event->data.addr.client, client_info) >= 0) { + std::string clientName = snd_seq_client_info_get_name(client_info); + callback(MonitorAction::DeviceAdded, event->data.addr.client, clientName); + } + } + else if (event->type == SND_SEQ_EVENT_CLIENT_EXIT) + { + callback(MonitorAction::DeviceRemoved, event->data.addr.client,""); + } + snd_seq_free_event(event); + } + } + } + + return; + } + + void AlsaSequencerDeviceMonitorImpl::StopMonitoring() + { + if (started) + { + started = false; + terminateThread = true; + serviceThread = nullptr; // (joins) + } + } + + AlsaSequencerDeviceMonitorImpl::~AlsaSequencerDeviceMonitorImpl() + { + StopMonitoring(); + } + + int AlsaSequencerDeviceMonitorImpl::CreateInputQueue(snd_seq_t *seqHandle, int inPort) + { + if (!seqHandle) + { + throw std::runtime_error("ALSA sequencer not initialized"); + } + + // Create a new queue if we don't have one yet + int queueId = -1; + { + queueId = snd_seq_alloc_named_queue(seqHandle, "PiPedal Device Monitor Queue"); + if (queueId < 0) + { + throw std::runtime_error(SS("Failed to create ALSA queue: " << snd_strerror(queueId))); + } + + // Set queue timing to real-time mode + snd_seq_queue_tempo_t *tempo; + snd_seq_queue_tempo_alloca(&tempo); + snd_seq_queue_tempo_set_tempo(tempo, 120); // 120 BPM default + snd_seq_queue_tempo_set_ppq(tempo, 96); // 96 ticks per quarter note + + int rc = snd_seq_set_queue_tempo(seqHandle, queueId, tempo); + if (rc < 0) + { + snd_seq_free_queue(seqHandle, queueId); + throw std::runtime_error(SS("Failed to set queue tempo: " << snd_strerror(rc))); + } + + // Start the queue + rc = snd_seq_start_queue(seqHandle, queueId, nullptr); + if (rc < 0) + { + snd_seq_free_queue(seqHandle, queueId); + throw std::runtime_error(SS("Failed to start queue: " << snd_strerror(rc))); + } + + // Set the queue for input timestamping + snd_seq_port_info_t *port_info; + snd_seq_port_info_alloca(&port_info); + + rc = snd_seq_get_port_info(seqHandle, inPort, port_info); + if (rc < 0) + { + snd_seq_free_queue(seqHandle, queueId); + throw std::runtime_error(SS("Failed to get port info: " << snd_strerror(rc))); + } + + // Enable timestamping on the input port + snd_seq_port_info_set_timestamping(port_info, 1); + snd_seq_port_info_set_timestamp_real(port_info, 1); + snd_seq_port_info_set_timestamp_queue(port_info, queueId); + + rc = snd_seq_set_port_info(seqHandle, inPort, port_info); + if (rc < 0) + { + snd_seq_free_queue(seqHandle, queueId); + throw std::runtime_error(SS("Failed to set port timestamping: " << snd_strerror(rc))); + } + return queueId; + } + + return queueId; + } + + JSON_MAP_BEGIN(AlsaSequencerPortSelection) + JSON_MAP_REFERENCE(AlsaSequencerPortSelection, id) + JSON_MAP_REFERENCE(AlsaSequencerPortSelection, name) + JSON_MAP_REFERENCE(AlsaSequencerPortSelection, sortOrder) + JSON_MAP_END() + + JSON_MAP_BEGIN(AlsaSequencerConfiguration) + JSON_MAP_REFERENCE(AlsaSequencerConfiguration, connections) + JSON_MAP_END() + +} // namespace pipedal diff --git a/PiPedalCommon/src/CMakeLists.txt b/PiPedalCommon/src/CMakeLists.txt index 8cf14fc..99e0795 100644 --- a/PiPedalCommon/src/CMakeLists.txt +++ b/PiPedalCommon/src/CMakeLists.txt @@ -41,6 +41,7 @@ message(STATUS "NMPIPEDAL CMAKE_CXX_FLAGS: ${CMAKE_CXX_FLAGS}") # Use the sdbus-c++ target in SDBusCpp namespace add_library(PiPedalCommon STATIC + include/Finally.hpp include/dbus/org.freedesktop.NetworkManager.Device.Statistics.hpp include/dbus/org.freedesktop.NetworkManager.Settings.Connection.hpp include/dbus/org.freedesktop.NetworkManager.IP6Config.hpp diff --git a/PiPedalCommon/src/HtmlHelper.cpp b/PiPedalCommon/src/HtmlHelper.cpp index 33c1bb4..607c7aa 100644 --- a/PiPedalCommon/src/HtmlHelper.cpp +++ b/PiPedalCommon/src/HtmlHelper.cpp @@ -44,9 +44,9 @@ std::string HtmlHelper::timeToHttpDate(std::filesystem::file_time_type time) { // Convert to time_t. - std::chrono::system_clock::time_point system_time = std::chrono::clock_cast(time); + auto sctp = std::chrono::time_point_cast(time - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now()); - auto time_t_value = std::chrono::system_clock::to_time_t(system_time); + auto time_t_value = std::chrono::system_clock::to_time_t(sctp); return timeToHttpDate(time_t_value); } diff --git a/PiPedalCommon/src/WifiConfigSettings.cpp b/PiPedalCommon/src/WifiConfigSettings.cpp index ddeb005..9b574ac 100644 --- a/PiPedalCommon/src/WifiConfigSettings.cpp +++ b/PiPedalCommon/src/WifiConfigSettings.cpp @@ -89,7 +89,6 @@ bool WifiConfigSettings::ValidateCountryCode(const std::string &text) return std::isalpha(text[0]) && std::isalpha(text[1]); } - void WifiConfigSettings::Load() { try @@ -115,9 +114,9 @@ void WifiConfigSettings::Load() } static void openWithPerms( - pipedal::ofstream_synced &f, - const std::filesystem::path &path, - std::filesystem::perms perms = + pipedal::ofstream_synced &f, + const std::filesystem::path &path, + std::filesystem::perms perms = std::filesystem::perms::owner_read | std::filesystem::perms::owner_write | std::filesystem::perms::group_read | std::filesystem::perms::group_write) { @@ -132,13 +131,16 @@ static void openWithPerms( f.open(path); f.close(); } - try { + try + { // set the perms. std::filesystem::permissions( path, perms, - std::filesystem::perm_options::replace); - } catch (const std::exception&) { + std::filesystem::perm_options::replace); + } + catch (const std::exception &) + { Lv2Log::warning(SS("Failed to set permissions on" << path << ".")); } } @@ -147,10 +149,10 @@ static void openWithPerms( f.open(path); } -void WifiConfigSettings::Save() +void WifiConfigSettings::Save() { - WifiConfigSettings newSettings {*this}; - + WifiConfigSettings newSettings{*this}; + // sync legacy settings, just in case i don't know what. newSettings.mdnsName_ = newSettings.hotspotName_; newSettings.enable_ = newSettings.IsEnabled(); @@ -163,12 +165,12 @@ void WifiConfigSettings::Save() newSettings.password_ = oldSettings.password_; } newSettings.hasSavedPassword_ = newSettings.hasPassword_; - + try { ofstream_synced f; - openWithPerms(f,CONFIG_PATH); - json_writer writer(f,false); + openWithPerms(f, CONFIG_PATH); + json_writer writer(f, false); writer.write(&newSettings); } catch (const std::exception &e) @@ -300,17 +302,14 @@ namespace pipedal::priv connection["ipv4"]["method"] = sdbus::Variant(std::string("shared")); connection["ipv6"]["method"] = sdbus::Variant(std::string("ignore")); - - connection["ipv4"]["address-data"] = sdbus::Variant(std::vector>{{ - {"address", sdbus::Variant("192.168.4.1")}, - {"prefix", sdbus::Variant(uint32_t(24))} - }}); + connection["ipv4"]["address-data"] = sdbus::Variant(std::vector>{{{"address", sdbus::Variant("192.168.4.1")}, + {"prefix", sdbus::Variant(uint32_t(24))}}}); connection["ipv4"]["dhcp-send-hostname"] = sdbus::Variant(true); connection["ipv4"]["dhcp-hostname"] = sdbus::Variant("raspberrypi"); connection["ipv4"]["dhcp-options"] = sdbus::Variant(std::vector{ "option:classless-static-route,192.168.4.0/24,0.0.0.0,0.0.0.0/0,192.168.4.1" //"option:classless-static-route,192.168.4.0/24,192.168.4.1" - }); + }); auto connection_path = AddConnection(connection); return connection_path; @@ -326,7 +325,6 @@ namespace pipedal::priv return active_connection_path; } - void RemoveConnection(const std::string &connection) { auto connection_proxy = sdbus::createProxy( @@ -429,7 +427,6 @@ namespace pipedal::priv }; } - JSON_MAP_BEGIN(WifiConfigSettings) // v0 JSON_MAP_REFERENCE(WifiConfigSettings, valid) @@ -560,52 +557,51 @@ bool WifiConfigSettings::ValidateChannel(const std::string &countryCode, const s return true; } -static const char* trueValues[] { +static const char *trueValues[]{ "true", "on", "yes" "1", - nullptr -}; -static const char* falseValues[] { + nullptr}; +static const char *falseValues[]{ "false", "off", "no", "0", - nullptr -}; + nullptr}; -static bool Matches(const std::string&value, const char**matches) +static bool Matches(const std::string &value, const char **matches) { - while(*matches) + while (*matches) { - if (value == *matches) return true; + if (value == *matches) + return true; ++matches; } return false; } -static bool TryStringToBool(const std::string &value, bool*outputValue) +static bool TryStringToBool(const std::string &value, bool *outputValue) { - if (Matches(value,trueValues)) + if (Matches(value, trueValues)) { *outputValue = true; return true; } - if (Matches(value, falseValues)) { + if (Matches(value, falseValues)) + { *outputValue = false; return true; } *outputValue = false; return false; - } -static WifiConfigSettings::ssid_t readSsid(std::istream&ss) +static WifiConfigSettings::ssid_t readSsid(std::istream &ss) { using ssid_t = WifiConfigSettings::ssid_t; char c; - + ssid_t result; while (ss.peek() == ' ') { @@ -624,28 +620,33 @@ static WifiConfigSettings::ssid_t readSsid(std::istream&ss) break; } ss >> c; - switch (c) { - case 'n': - result.push_back((uint8_t)'\n'); - break; - case 'r': - result.push_back((uint8_t)'\r'); - break; - case 't': - result.push_back((uint8_t)'\t'); - break; - case 'b': - result.push_back((uint8_t)'\b'); - break; - default: - result.push_back((uint8_t)c); - break; + switch (c) + { + case 'n': + result.push_back((uint8_t)'\n'); + break; + case 'r': + result.push_back((uint8_t)'\r'); + break; + case 't': + result.push_back((uint8_t)'\t'); + break; + case 'b': + result.push_back((uint8_t)'\b'); + break; + default: + result.push_back((uint8_t)c); + break; } - } else { + } + else + { result.push_back((uint8_t)c); } } - } else { + } + else + { while (!ss.eof()) { if (c == ':') @@ -662,7 +663,7 @@ static WifiConfigSettings::ssid_t readSsid(std::istream&ss) } return result; } -static std::vector stringToSsidArray(const std::string&value) +static std::vector stringToSsidArray(const std::string &value) { using ssid_t = WifiConfigSettings::ssid_t; std::istringstream ss(value); @@ -671,7 +672,7 @@ static std::vector stringToSsidArray(const std::stri while (true) { ssid_t ssid = readSsid(ss); - if (ssid.size() == 0) + if (ssid.size() == 0) { break; } @@ -683,16 +684,15 @@ static std::vector stringToSsidArray(const std::stri } } return result; - } void WifiConfigSettings::ParseArguments( const std::vector &argv, HotspotAutoStartMode startMode, const std::string homeNetworkSsid - ) +) { - this->valid_ = false ; + this->valid_ = false; if (argv.size() != 4) { throw invalid_argument("Invalid number of arguments."); @@ -714,7 +714,6 @@ void WifiConfigSettings::ParseArguments( this->hasPassword_ = this->password_.length() != 0; this->hasSavedPassword_ = oldSettings.hasSavedPassword_; - if (!ValidateCountryCode(this->countryCode_)) { throw invalid_argument("Invalid country code."); @@ -723,7 +722,7 @@ void WifiConfigSettings::ParseArguments( throw invalid_argument("Hotspot name is too long."); if (this->hotspotName_.length() < 1) throw invalid_argument("Hotspot name is too short."); - + if (this->password_.size() != 0 && this->password_.size() < 8) throw invalid_argument("Passphrase must be at least 8 characters long."); @@ -742,12 +741,12 @@ void WifiConfigSettings::ParseArguments( this->valid_ = true; } -bool WifiConfigSettings::ConfigurationChanged(const WifiConfigSettings&other) const +bool WifiConfigSettings::ConfigurationChanged(const WifiConfigSettings &other) const { return !( this->valid_ == other.valid_ && - //this->rebootRequired_ == other.rebootRequired_ && - //this->enable_ == other.enable_ && + // this->rebootRequired_ == other.rebootRequired_ && + // this->enable_ == other.enable_ && this->countryCode_ == other.countryCode_ && this->hotspotName_ == other.hotspotName_ && this->hasPassword_ == other.hasPassword_ && @@ -756,20 +755,18 @@ bool WifiConfigSettings::ConfigurationChanged(const WifiConfigSettings&other) co this->homeNetwork_ == other.homeNetwork_ && this->autoStartMode_ == other.autoStartMode_ && - this->hasSavedPassword_ == other.hasSavedPassword_ - ); - + this->hasSavedPassword_ == other.hasSavedPassword_); } -bool WifiConfigSettings::operator==(const WifiConfigSettings&other) const +bool WifiConfigSettings::operator==(const WifiConfigSettings &other) const { return !ConfigurationChanged(other) && this->wifiWarningGiven_ == other.wifiWarningGiven_; } -static bool CanSeeHomeNetwork(const std::string&home, const std::vector&availableNetworks) +static bool CanSeeHomeNetwork(const std::string &home, const std::vector &availableNetworks) { - for (const auto &availableNetwork: availableNetworks) + for (const auto &availableNetwork : availableNetworks) { - if (availableNetwork == home) + if (availableNetwork == home) { return true; } @@ -778,38 +775,39 @@ static bool CanSeeHomeNetwork(const std::string&home, const std::vector &availableRememberedNetworks, const std::vector &availableNetworks) { - if ((!this->valid_)) + if ((!this->valid_)) return false; - + HotspotAutoStartMode autoStartMode = (HotspotAutoStartMode)this->autoStartMode_; switch (autoStartMode) { - case HotspotAutoStartMode::Never: - default: - return false; + case HotspotAutoStartMode::Never: + default: + return false; - case HotspotAutoStartMode::NoEthernetConnection: - return !ethernetConnected; - case HotspotAutoStartMode::NotAtHome: - return !CanSeeHomeNetwork(this->homeNetwork_, availableNetworks); - case HotspotAutoStartMode::NoRememberedWifiConections: - return availableRememberedNetworks.size() == 0; - case HotspotAutoStartMode::Always: - return true; + case HotspotAutoStartMode::NoEthernetConnection: + return !ethernetConnected; + case HotspotAutoStartMode::NotAtHome: + return !CanSeeHomeNetwork(this->homeNetwork_, availableNetworks); + case HotspotAutoStartMode::NoRememberedWifiConections: + return availableRememberedNetworks.size() == 0; + case HotspotAutoStartMode::Always: + return true; } } std::string pipedal::ssidToString(const std::vector &ssid) { std::stringstream s; - for (auto v: ssid) + for (auto v : ssid) { - if (v == 0) break; // breaks for some arguably legal ssids, but I can live with that. + if (v == 0) + break; // breaks for some arguably legal ssids, but I can live with that. s << (char)v; } return s.str(); @@ -819,21 +817,43 @@ std::vector pipedal::ssidToStringVector(const std::vector result; result.reserve(ssids.size()); - for (const std::vector &ssid: ssids) + for (const std::vector &ssid : ssids) { result.push_back(ssidToString(ssid)); } return result; } bool WifiConfigSettings::WantsHotspot( - bool ethernetConnected, + bool ethernetConnected, const std::vector> &availableRememberedNetworks, // remembered networks that are currently visible - const std::vector> &availableNetworks // all visible networks. - ) + const std::vector> &availableNetworks // all visible networks. +) { std::vector sAvailableRememberedNetworks = ssidToStringVector(availableRememberedNetworks); std::vector sAvailableNetworks = ssidToStringVector(availableNetworks); - return WantsHotspot(ethernetConnected,sAvailableRememberedNetworks,sAvailableNetworks); + return WantsHotspot(ethernetConnected, sAvailableRememberedNetworks, sAvailableNetworks); +} + +bool WifiConfigSettings::NeedsWifi() const +{ + HotspotAutoStartMode autoStartMode = (HotspotAutoStartMode)this->autoStartMode_; + return autoStartMode != HotspotAutoStartMode::Never; } +bool WifiConfigSettings::NeedsScan() const +{ + HotspotAutoStartMode autoStartMode = (HotspotAutoStartMode)this->autoStartMode_; + switch (autoStartMode) + { + case HotspotAutoStartMode::Never: + case HotspotAutoStartMode::NoEthernetConnection: + case HotspotAutoStartMode::Always: + default: + return false; + + case HotspotAutoStartMode::NotAtHome: + case HotspotAutoStartMode::NoRememberedWifiConections: + return true; + } +} diff --git a/PiPedalCommon/src/include/AlsaSequencer.hpp b/PiPedalCommon/src/include/AlsaSequencer.hpp index d70db85..f9a9a78 100644 --- a/PiPedalCommon/src/include/AlsaSequencer.hpp +++ b/PiPedalCommon/src/include/AlsaSequencer.hpp @@ -25,12 +25,77 @@ #include #include -#include -#include -#include +#include +#include +#include "json.hpp" namespace pipedal { + + // event messages starting with 0xFF are meta events. + // data not in midi format, is variable length, and private and the second byte is the MetaEventType. + enum class MetaEventType + { + None, + Tempo, + TimeSignature, + KeySignature, + SetPositionTick, + SetPositionTime + }; + + class AlsaSequencerPortSelection { + private: + std::string id_; + std::string name_; + int32_t sortOrder_ = 0; + + public: + AlsaSequencerPortSelection() = default; + AlsaSequencerPortSelection(const std::string &id, const std::string &name, int32_t displaySortOrder) + : id_(id), name_(name), sortOrder_(displaySortOrder) {} + AlsaSequencerPortSelection(const AlsaSequencerPortSelection &other) = default; + AlsaSequencerPortSelection(AlsaSequencerPortSelection &&other) = default; + AlsaSequencerPortSelection &operator=(const AlsaSequencerPortSelection &other) = default; + AlsaSequencerPortSelection &operator=(AlsaSequencerPortSelection &&other) = default; + + + const std::string& id() const { return id_; } + void id(const std::string &value) { id_ = value; } + const std::string &name() const { return name_; } + void name(const std::string &value) { name_ = value; } + + DECLARE_JSON_MAP(AlsaSequencerPortSelection); + }; + class AlsaSequencerConfiguration { + private: + std::vector connections_; + public: + const std::vector& connections() const { return connections_; } + std::vector& connections() { return connections_; } + void connections(const std::vector& value) { connections_ = value; } + + + bool operator==(const AlsaSequencerConfiguration &other) const + { + if (connections_.size() != other.connections_.size()) + return false; + for (size_t i = 0; i < connections_.size(); ++i) + { + if (connections_[i].id() != other.connections_[i].id() || + connections_[i].name() != other.connections_[i].name()) + { + return false; + } + } + return true; + } + + DECLARE_JSON_MAP(AlsaSequencerConfiguration); + + }; + + struct AlsaSequencerPort { std::string id; @@ -53,28 +118,181 @@ namespace pipedal bool isSpecific; bool isHardware = false; bool isSoftware = false; + bool isPort = false; + bool isVirtual = false; int cardNumber = -1; + int32_t displaySortOrder = 0; std::string rawMidiDevice; // e.g. "hw:0,0,0" for kernel devices }; struct AlsaMidiMessage { - uint32_t timestamp; // in microseconds (tick time if using queue) + AlsaMidiMessage() + { + size = 0; + data = fixedBuffer; + } + AlsaMidiMessage(uint8_t cc0) + { + size = 1; + data = fixedBuffer; + fixedBuffer[0] = cc0; + } + AlsaMidiMessage(uint8_t cc0, uint8_t cc1) + { + size = 2; + data = fixedBuffer; + fixedBuffer[0] = cc0; + fixedBuffer[1] = cc1; + } + AlsaMidiMessage(uint8_t cc0, uint8_t cc1, uint8_t cc2) + { + size = 3; + data = fixedBuffer; + fixedBuffer[0] = cc0; + fixedBuffer[1] = cc1; + fixedBuffer[2] = cc2; + } + AlsaMidiMessage(uint8_t *data, size_t size) + { + this->size = size; + this->data = data; + } + AlsaMidiMessage(const AlsaMidiMessage &other) + { + this->timestamp = other.timestamp; + this->realtime_sec = other.realtime_sec; + this->realtime_nsec = other.realtime_nsec; + + if (other.size > 3) + { + this->size = other.size; + this->data = other.data; + } + else + { + this->size = other.size; + this->data = fixedBuffer; + for (size_t i = 0; i < other.size; ++i) + { + fixedBuffer[i] = other.fixedBuffer[i]; + } + } + } + AlsaMidiMessage &operator=(const AlsaMidiMessage &other) + { + this->timestamp = other.timestamp; + this->realtime_sec = other.realtime_sec; + this->realtime_nsec = other.realtime_nsec; + + if (other.size > 3) + { + this->size = other.size; + this->data = other.data; + } + else + { + this->size = other.size; + this->data = fixedBuffer; + for (size_t i = 0; i < other.size; ++i) + { + fixedBuffer[i] = other.fixedBuffer[i]; + } + } + return *this; + } + AlsaMidiMessage(AlsaMidiMessage &&other) + { + this->timestamp = other.timestamp; + this->realtime_sec = other.realtime_sec; + this->realtime_nsec = other.realtime_nsec; + + if (other.size > 3) + { + this->size = other.size; + this->data = other.data; + } + else + { + this->size = other.size; + this->data = fixedBuffer; + for (size_t i = 0; i < other.size; ++i) + { + fixedBuffer[i] = other.fixedBuffer[i]; + } + } + } + AlsaMidiMessage &operator=(AlsaMidiMessage &&other) + { + this->timestamp = other.timestamp; + this->realtime_sec = other.realtime_sec; + this->realtime_nsec = other.realtime_nsec; + + if (other.size > 3) + { + this->size = other.size; + this->data = other.data; + } + else + { + this->size = other.size; + this->data = fixedBuffer; + for (size_t i = 0; i < other.size; ++i) + { + fixedBuffer[i] = other.fixedBuffer[i]; + } + } + return *this; + } + + void Set(uint8_t cc0) + { + size = 1; + data = fixedBuffer; + fixedBuffer[0] = cc0; + } + void Set(uint8_t cc0, uint8_t cc1) + { + size = 2; + data = fixedBuffer; + fixedBuffer[0] = cc0; + fixedBuffer[1] = cc1; + } + void Set(uint8_t cc0, uint8_t cc1, uint8_t cc2) + { + size = 3; + data = fixedBuffer; + fixedBuffer[0] = cc0; + fixedBuffer[1] = cc1; + fixedBuffer[2] = cc2; + } + void Set(uint8_t *data, size_t size) + { + this->size = size; + this->data = data; + } + uint8_t cc0() const { return size > 0 ? data[0] : 0; } + uint8_t cc1() const { return size > 1 ? data[1] : 0; } + uint8_t cc2() const { return size > 2 ? data[2] : 0; } + + + uint32_t timestamp; // in microseconds (tick time if using queue) uint64_t realtime_sec; // real-time timestamp seconds uint32_t realtime_nsec; // real-time timestamp nanoseconds - uint8_t cc0; - uint8_t cc1; - uint8_t cc2; + size_t size = 0; + uint8_t *data = fixedBuffer; + + uint8_t fixedBuffer[12]; }; /* * AlsaSequencer - ALSA MIDI sequencer interface with real-time timestamp support - * + * * Example usage with real-time timestamps: - * + * * AlsaSequencer sequencer; * sequencer.connect(clientId, portId); - * + * * AlsaMidiMessage message; * while (sequencer.ReadMessage(&message)) { * // message.realtime_sec and message.realtime_nsec contain precise real-time timestamps @@ -82,45 +300,58 @@ namespace pipedal * printf("MIDI event at %lu.%09u seconds\n", message.realtime_sec, message.realtime_nsec); * } */ + class AlsaSequencer { + protected: + AlsaSequencer() {} + public: + virtual ~AlsaSequencer() = default; + + using self = AlsaSequencer; + using ptr = std::shared_ptr; + + static ptr Create(); + static std::vector EnumeratePorts(); - AlsaSequencer(); - ~AlsaSequencer(); + public: + virtual void ConnectPort(int clientId, int portId) = 0; + virtual void ConnectPort(const std::string &id) = 0; - void ConnectPort(int clientId, int portId); - void ConnectPort(const std::string&name); + virtual void SetConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0; - // Read a single MIDI message from the sequencer input port - bool ReadMessage(AlsaMidiMessage &message); + // Read a single MIDI message from the sequencer input port. A timeout of -1 blocks indefinitely. + // A timeout of 0 returns immediately. + virtual bool ReadMessage(AlsaMidiMessage &message, int timeoutMs = -1) = 0; + // currently non-functional + virtual bool GetQueueRealtime(uint64_t *sec, uint32_t *nsec) = 0; - // Get current real-time from the queue (useful for calculating precise timing) - bool GetQueueRealtime(uint64_t* sec, uint32_t* nsec); - - // Get the current queue ID (returns -1 if no queue is active) - int GetQueueId() const { return queueId; } - - private: - // Create an ALSA input queue with real-time timestamps for the given client/port - int CreateRealtimeInputQueue(); - - struct Connection - { - int clientId; - int portId; - }; - - std::vector connections; - - snd_seq_t *seqHandle = nullptr; - int inPort = -1; - int queueId = -1; // Queue for real-time timestamps - - // Example: open an ALSA sequencer input port and read MIDI events continuously - void ReadMidiFromPort(int clientId, int portId); + virtual void RemoveAllConnections() = 0; }; + + class AlsaSequencerDeviceMonitor { + protected: + AlsaSequencerDeviceMonitor() {} + public: + virtual ~AlsaSequencerDeviceMonitor() {} + + using self = AlsaSequencerDeviceMonitor; + using ptr = std::shared_ptr; + static ptr Create(); + enum class MonitorAction { + DeviceAdded, + DeviceRemoved + }; + using Callback = std::function; + virtual void StartMonitoring(Callback &&onChangeCallback) = 0; + virtual void StopMonitoring() = 0; + + }; + + std::string RawMidiIdToSequencerId(const std::vector &seqDevices, const std::string &rawMidiId); + } \ No newline at end of file diff --git a/PiPedalCommon/src/include/Finally.hpp b/PiPedalCommon/src/include/Finally.hpp new file mode 100644 index 0000000..cbbdc7e --- /dev/null +++ b/PiPedalCommon/src/include/Finally.hpp @@ -0,0 +1,39 @@ +// Copyright (c) 2024 Robin Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#pragma once +#include + +namespace pipedal { + + class Finally { + public: + Finally(std::function &&fn) + :fn (std::move(fn)) + { + + } + ~Finally() { + fn(); + } + private: + std::function fn; + }; + +} \ No newline at end of file diff --git a/PiPedalCommon/src/include/WifiConfigSettings.hpp b/PiPedalCommon/src/include/WifiConfigSettings.hpp index 44492aa..ace67af 100644 --- a/PiPedalCommon/src/include/WifiConfigSettings.hpp +++ b/PiPedalCommon/src/include/WifiConfigSettings.hpp @@ -95,6 +95,10 @@ namespace pipedal { bool operator==(const WifiConfigSettings&other) const; bool ConfigurationChanged(const WifiConfigSettings&other) const; + bool WantsHotspot(bool ethernetConnected) + { + return WantsHotspot(ethernetConnected, std::vector{}, std::vector{}); + } bool WantsHotspot( bool ethernetConnected, const std::vector &availableRememberedNetworks, // remembered networks that are currently visible @@ -105,7 +109,8 @@ namespace pipedal { const std::vector> &availableRememberedNetworks, // remembered networks that are currently visible const std::vector> &availableNetworks // all visible networks. ); - + bool NeedsScan() const; + bool NeedsWifi() const; public: DECLARE_JSON_MAP(WifiConfigSettings); }; diff --git a/PiPedalCommon/src/include/json.hpp b/PiPedalCommon/src/include/json.hpp index 93e9d03..aa193aa 100644 --- a/PiPedalCommon/src/include/json.hpp +++ b/PiPedalCommon/src/include/json.hpp @@ -35,6 +35,8 @@ #include #include #include +#include +#include #define DECLARE_JSON_MAP(CLASSNAME) \ static pipedal::json_map::storage_type jmap diff --git a/PiPedalCommon/src/include/json_variant.hpp b/PiPedalCommon/src/include/json_variant.hpp index d6454f2..9c4b69b 100644 --- a/PiPedalCommon/src/include/json_variant.hpp +++ b/PiPedalCommon/src/include/json_variant.hpp @@ -30,6 +30,7 @@ #include #include #include +#include #include "json.hpp" namespace pipedal diff --git a/cmake/FindPipeWire.cmake b/cmake/FindPipeWire.cmake new file mode 100644 index 0000000..c90c3e0 --- /dev/null +++ b/cmake/FindPipeWire.cmake @@ -0,0 +1,122 @@ +#.rst: +# FindPipeWire +# ------- +# +# Try to find PipeWire on a Unix system. +# +# This will define the following variables: +# +# ``PipeWire_FOUND`` +# True if (the requested version of) PipeWire is available +# ``PipeWire_VERSION`` +# The version of PipeWire +# ``PipeWire_LIBRARIES`` +# This can be passed to target_link_libraries() instead of the ``PipeWire::PipeWire`` +# target +# ``PipeWire_INCLUDE_DIRS`` +# This should be passed to target_include_directories() if the target is not +# used for linking +# ``PipeWire_DEFINITIONS`` +# This should be passed to target_compile_options() if the target is not +# used for linking +# +# If ``PipeWire_FOUND`` is TRUE, it will also define the following imported target: +# +# ``PipeWire::PipeWire`` +# The PipeWire library +# +# In general we recommend using the imported target, as it is easier to use. +# Bear in mind, however, that if the target is in the link interface of an +# exported library, it must be made available by the package config file. + +#============================================================================= +# Copyright 2014 Alex Merry +# Copyright 2014 Martin Gräßlin +# Copyright 2018-2020 Jan Grulich +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#============================================================================= + +# Use pkg-config to get the directories and then use these values +# in the FIND_PATH() and FIND_LIBRARY() calls +find_package(PkgConfig QUIET) + +pkg_search_module(PKG_PipeWire QUIET libpipewire-0.3 libpipewire-0.2) +pkg_search_module(PKG_Spa QUIET libspa-0.2 libspa-0.1) + +set(PipeWire_DEFINITIONS "${PKG_PipeWire_CFLAGS}" "${PKG_Spa_CFLAGS}") +set(PipeWire_VERSION "${PKG_PipeWire_VERSION}") + +find_path(PipeWire_INCLUDE_DIRS + NAMES + pipewire/pipewire.h + HINTS + ${PKG_PipeWire_INCLUDE_DIRS} + ${PKG_PipeWire_INCLUDE_DIRS}/pipewire-0.3 +) + +find_path(Spa_INCLUDE_DIRS + NAMES + spa/param/props.h + HINTS + ${PKG_Spa_INCLUDE_DIRS} + ${PKG_Spa_INCLUDE_DIRS}/spa-0.2 +) + +find_library(PipeWire_LIBRARIES + NAMES + pipewire-0.3 + pipewire-0.2 + HINTS + ${PKG_PipeWire_LIBRARY_DIRS} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(PipeWire + FOUND_VAR + PipeWire_FOUND + REQUIRED_VARS + PipeWire_LIBRARIES + PipeWire_INCLUDE_DIRS + Spa_INCLUDE_DIRS + VERSION_VAR + PipeWire_VERSION +) + +if(PipeWire_FOUND AND NOT TARGET PipeWire::PipeWire) + add_library(PipeWire::PipeWire UNKNOWN IMPORTED) + set_target_properties(PipeWire::PipeWire PROPERTIES + IMPORTED_LOCATION "${PipeWire_LIBRARIES}" + INTERFACE_COMPILE_OPTIONS "${PipeWire_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${PipeWire_INCLUDE_DIRS};${Spa_INCLUDE_DIRS}" + ) +endif() + +mark_as_advanced(PipeWire_LIBRARIES PipeWire_INCLUDE_DIRS) + +include(FeatureSummary) +set_package_properties(PipeWire PROPERTIES + URL "https://www.pipewire.org" + DESCRIPTION "PipeWire - multimedia processing" +) \ No newline at end of file diff --git a/docs/BuildPrerequisites.md b/docs/BuildPrerequisites.md index 40adbc5..d3c9d28 100644 --- a/docs/BuildPrerequisites.md +++ b/docs/BuildPrerequisites.md @@ -26,7 +26,8 @@ Run the following commands to install dependent libraries required by the PiPeda libsystemd-dev catch libasound2-dev uuid-dev \ authbind libavahi-client-dev libnm-dev libicu-dev \ libsdbus-c++-dev libzip-dev google-perftools \ - libgoogle-perftools-dev + libgoogle-perftools-dev \ + libpipewire-0.3-dev ### Installing Sources diff --git a/lv2/aarch64/ToobAmp.lv2/CabIR.ttl b/lv2/aarch64/ToobAmp.lv2/CabIR.ttl index 5634996..a5cdcb5 100644 --- a/lv2/aarch64/ToobAmp.lv2/CabIR.ttl +++ b/lv2/aarch64/ToobAmp.lv2/CabIR.ttl @@ -93,7 +93,7 @@ cabir:impulseFile3 doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; rdfs:comment """ TooB Cab IR is a convolution-based guitar cabinet impulse response simulator. diff --git a/lv2/aarch64/ToobAmp.lv2/CabSim.ttl b/lv2/aarch64/ToobAmp.lv2/CabSim.ttl index 04e2903..70ba918 100644 --- a/lv2/aarch64/ToobAmp.lv2/CabSim.ttl +++ b/lv2/aarch64/ToobAmp.lv2/CabSim.ttl @@ -50,7 +50,7 @@ toob:frequencyResponseVector doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; mod:brand "TooB"; mod:label "TooB CabSim"; diff --git a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl index 441992c..3165808 100644 --- a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl @@ -53,7 +53,7 @@ toobimpulse:impulseFile doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; rdfs:comment """ Convolution reverb is a notoriously compute-intensive effect. If you are having performance issues, use the Max T control to constrain the length of the impulse file to diff --git a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl index e77c07d..a9ff502 100644 --- a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl @@ -51,7 +51,7 @@ toobimpulse:impulseFile doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; rdfs:comment """ diff --git a/lv2/aarch64/ToobAmp.lv2/InputStage.ttl b/lv2/aarch64/ToobAmp.lv2/InputStage.ttl index 0872580..6ec1c19 100644 --- a/lv2/aarch64/ToobAmp.lv2/InputStage.ttl +++ b/lv2/aarch64/ToobAmp.lv2/InputStage.ttl @@ -66,7 +66,7 @@ inputStage:filterGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; mod:brand "TooB"; mod:label "TooB Input"; diff --git a/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl b/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl index da5f078..eeede7d 100644 --- a/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl +++ b/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl @@ -68,7 +68,7 @@ pstage:stage3 doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; mod:brand "TooB"; mod:label "Power Stage"; diff --git a/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl b/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl index 6385edf..70bda8c 100644 --- a/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl +++ b/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl @@ -57,7 +57,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; rdfs:comment "TooB spectrum analyzer" ; mod:brand "TooB"; diff --git a/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl b/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl index 7be30fd..f9d51b9 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl @@ -57,7 +57,7 @@ tonestack:eqGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; uiext:ui ; @@ -131,17 +131,28 @@ tone stack used in Polytone and HiWatt amps. rdfs:label "Baxandall" ; rdf:value 2.0 ]; + ],[ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 4 ; + lv2:symbol "gain" ; + lv2:name "Gain"; + lv2:default 0.0 ; + lv2:minimum -40.0 ; + lv2:maximum 30.0; + units:unit units:db; ], [ a lv2:AudioPort , lv2:InputPort ; - lv2:index 4 ; + lv2:index 5 ; lv2:symbol "in" ; lv2:name "In" ], [ a lv2:AudioPort , lv2:OutputPort ; - lv2:index 5 ; + lv2:index 6 ; lv2:symbol "out" ; lv2:name "Out" ],[ @@ -150,7 +161,7 @@ tone stack used in Polytone and HiWatt amps. atom:bufferType atom:Sequence ; # atom:supports patch:Message; lv2:designation lv2:control ; - lv2:index 6 ; + lv2:index 7 ; lv2:symbol "control" ; lv2:name "Control" ; rdfs:comment "Plugin to GUI communication" ; @@ -160,7 +171,7 @@ tone stack used in Polytone and HiWatt amps. atom:bufferType atom:Sequence ; # atom:supports patch:Message; lv2:designation lv2:control ; - lv2:index 7 ; + lv2:index 8 ; lv2:symbol "notify" ; lv2:name "Notify" ; rdfs:comment "Plugin to GUI communication" ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so index da96e67..b6357f6 100644 Binary files a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so and b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so differ diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so b/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so index ed6ce54..92c76eb 100644 Binary files a/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so and b/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so differ diff --git a/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl b/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl index 299f637..44e6b0b 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl @@ -40,7 +40,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; rdfs:comment """ Emulation of a Boss CE-2 Chorus. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl b/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl index 84ea5bc..97573a7 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; rdfs:comment """ A straightforward no-frills digital delay. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl b/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl index dfdb899..7b2b30d 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; rdfs:comment """ Emulation of a Boss BF-2 Flanger, based on circuit analysis and simulation of the original. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl b/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl index 46f8776..5a85fdd 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; rdfs:comment """ Digital emulation of a Boss BF-2 Flanger. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl b/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl index 4e4202e..94a7c65 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; rdfs:comment """ Toob Freeverb is an Lv2 implementation of the famous Freeverb reverb effect. FreeVerb delivers a well-balanced reverb with very little tonal coloration. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobGraphicEq.ttl b/lv2/aarch64/ToobAmp.lv2/ToobGraphicEq.ttl index 9279d63..fcf298c 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobGraphicEq.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobGraphicEq.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; rdfs:comment """ A 7 octave graphic equalizer, with frequencies chosen to be useful for EQ-ing guitar signals. """ ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobLooperFour.ttl b/lv2/aarch64/ToobAmp.lv2/ToobLooperFour.ttl index 6bae997..cb2c508 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobLooperFour.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobLooperFour.ttl @@ -92,7 +92,7 @@ myprefix:output_group doap:license ; doap:maintainer ; lv2:minorVersion 1 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; ui:ui ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobLooperOne.ttl b/lv2/aarch64/ToobAmp.lv2/ToobLooperOne.ttl index e1fd778..9efd0ca 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobLooperOne.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobLooperOne.ttl @@ -78,7 +78,7 @@ myprefix:output_group doap:license ; doap:maintainer ; lv2:minorVersion 1 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; ui:ui ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobML.ttl b/lv2/aarch64/ToobAmp.lv2/ToobML.ttl index be6b362..5e6e3ee 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobML.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobML.ttl @@ -67,7 +67,7 @@ toobml:sagGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; rdfs:comment """ The TooB ML Amplifier plugin provides emulation of a variety of amplifiers and overdrive pedals that are implemented using neural-network-based machine learning models of real amplifiers. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobMix.ttl b/lv2/aarch64/ToobAmp.lv2/ToobMix.ttl index 4202334..2e397d4 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobMix.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobMix.ttl @@ -40,7 +40,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; rdfs:comment """ Remix a stereo input signal. @@ -57,7 +57,7 @@ Remix a stereo input signal. lv2:index 0 ; lv2:symbol "trimL" ; - lv2:name "Trim L"; + lv2:name "Vol L"; lv2:default 0.0 ; lv2:minimum -60.0 ; @@ -89,7 +89,7 @@ Remix a stereo input signal. lv2:index 2 ; lv2:symbol "trimR" ; - lv2:name "Trim R"; + lv2:name "Vol R"; lv2:default 0.0 ; lv2:minimum -60.0 ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl b/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl index bd4bf6b..5355066 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl @@ -61,33 +61,43 @@ toobNam:eqGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; rdfs:comment """ A port of Steven Atkinson's Neural Amp Modeler to LV2. -TooB Neural Amp Modeler uses uploadable .nam model files. Download .nam files from http://tonehunt.org, and then load them into TooB Neural Amp Modeler. +TooB Neural Amp Modeler uses uploadable .nam model files. Download .nam model files from http://tone3000.com, and then load them into TooB Neural Amp Modeler. -If you are using TooB Neural Amp Modeler from PiPedal, download the model files to the system on which you are running the client. You must then -upload the file to the PiPedal server. Click on the "Model" control and then search for the Upload button in the file browser. Once uploaded to the server, -you can then select the uploaded file in the browser directly. +The TONE3000 website contains a huge collection of community-developed amp models that can be used with TooB Neural Amp Modeler. You can also use .nam models from +other sources. Using TONE3000 models with TooB Neural Amp Modeler is a two-step process. First, download model files to your local system using a +web browser. Then upload the model files to the PiPedal server using the PiPedal web interface. You can find an "Upload" button in +the file browser when you click on the "Model" control in the PiPedal web interface. -If you are not using PiPedal, just click on the Model control, and use the file browser to select the .nam file on your local system. -TooB Neural Amp Modeler supports a much wider range of amp models than ToobML, but usually uses much more CPU. +If you are not using PiPedal, just click on the Model control, and use the file browser to select the .nam file directly on your local system. + +TooB Neural Amp Modeler supports a much wider range of amp models than ToobML, but usually uses more CPU. You will need at least a Pi 4 to use Toob Neural Amp Modeler, and you may need to increase your audio buffer sizes to prevent overruns. -If you are having trouble with CPU usage, tonehunt.org does contain some smaller amp models. Search for the "feather" tag to find -models that use less CPU. +If you are having trouble with CPU usage, tone3000.com does contain some smaller amp models. Search for the "feather" tag to find +models that use less CPU. Generally, you can use up to three NAM models at once in a given preset on a Pi 4, and up to 5 MAM models +simultaneously on a Pi 5. N100 micro PCs could presumably do even better. However, exact CPU use varies depending on the complexity of the amp models you are using. +Users have reported success using NAM on Raspberry Pi 3 devices, but this is not recommended (or really supported), as CPU usage is very high on a Raspberry Pi 3. +If you are interested in profiling your own amps and effect pedals, please visit https://www.tone3000.com/capture -If you are interested in building your own amp models, please visit https://www.neuralampmodeler.com/ +TooB Neural Amp Modeler contains code optimizations that allow use of a third model on Raspberry PI 4 devices. These optimizations +were offered to the upstream NAM project, but we recommended that they not be merged into upstream sources, because they +significantly affect maintainability of the codebase. and the performance increase they provide (about 30%) is really only +relevant on Raspberry PI 4-class devices. If you are interested in these optimizations, they have been published in ToobAmp +project sources on GitHub. -TooB Neural Amp Modeler uses code from the NeuralAmp Modeler Core project. The TooB Team wishes to express gratitude to Steven Atkinson for -making this extraordinary technology available as open-source code. +TooB Neural Amp Modeler uses code from the NeuralAmp Modeler Core project (https://www.neuralampmodeler.com/). The TooB team wishes +to express gratitude to Steven Atkinson for making this revolutionary technology available as open-source code. -Code from the the NeuralAmpModelerCore project (https://github.com/sdatkinson/NeuralAmpModelerCore) is provided under the following license. +Code from the the NeuralAmpModelerCore project (https://github.com/sdatkinson/NeuralAmpModelerCore) is provided +under the following license. MIT License -Copyright (c) 2023 Steven Atkinson +Copyright (c) 2025 Steven Atkinson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/lv2/aarch64/ToobAmp.lv2/ToobNoiseGate.ttl b/lv2/aarch64/ToobAmp.lv2/ToobNoiseGate.ttl index 485851d..932ca56 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobNoiseGate.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobNoiseGate.ttl @@ -54,7 +54,7 @@ noisegate:envelope_group doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; rdfs:comment """ A noise gate is an audio processing tool that controls the volume of an audio signal by allowing it to pass through only when it exceeds a set threshold. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobPhaser.ttl b/lv2/aarch64/ToobAmp.lv2/ToobPhaser.ttl index 5598a62..6963a38 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobPhaser.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobPhaser.ttl @@ -40,7 +40,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; rdfs:comment """ A loose emulation of an MXR® Phase 90 Phaser. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobPlayer.ttl b/lv2/aarch64/ToobAmp.lv2/ToobPlayer.ttl new file mode 100644 index 0000000..000489f --- /dev/null +++ b/lv2/aarch64/ToobAmp.lv2/ToobPlayer.ttl @@ -0,0 +1,307 @@ +@prefix doap: . +@prefix lv2: . +@prefix rdf: . +@prefix rdfs: . +@prefix units: . +@prefix urid: . +@prefix atom: . +@prefix midi: . +@prefix epp: . +@prefix uiext: . +@prefix idpy: . +@prefix foaf: . +@prefix mod: . +@prefix param: . +@prefix work: . +@prefix pg: . + +@prefix atom: . +@prefix patch: . +@prefix rdfs: . +@prefix state: . +@prefix urid: . +@prefix xsd: . +@prefix toobPlayer: . + +toobPlayer:mixGroup + a param:ControlGroup , + pg:InputGroup ; + lv2:name "Mix" ; + lv2:symbol "mixGroup" . + + + + a foaf:Person ; + foaf:name "Robin Davies" ; + foaf:mbox ; + foaf:homepage . + + +toobPlayer:audioFile + a lv2:Parameter; + rdfs:label "File"; + mod:fileTypes "audiotrack,wav,flac,mp3"; + rdfs:range atom:Path; + lv2:index 4 + . +toobPlayer:seek + a lv2:Parameter; + rdfs:label "Seek"; + rdfs:range atom:Float; + . + + + + a lv2:Plugin , + lv2:GeneratorPlugin ; + doap:name "TooB File Player" , + "TooB File Player"@en-gb + ; + doap:license ; + doap:maintainer ; + lv2:minorVersion 0 ; + lv2:microVersion 63 ; + rdfs:comment """ +Play audio files. Audio files can optionally be loooped by pressing the "Set loop" button. + +To skip the start of a file, press the "Set Loop" button, set the "Start" value in the seek dialog, and leave the "Loop Enabled" +checkbox unchecked. + +Note that loop information is stored as per-file metadata, and is not stored in the preset. If you need different loop settings for the +same file, create a copy of the file. Files are copied using hard links, so the copy will not take up significant disk space. + + +TooB Player will display audio metadata for a file (track number, title, album, artist) if available. +If the file's metadata contains album art, that will be displayed as well. If not, the player will look for a folder artwork file in the same directory. The +folder artwork file can be called any of "Folder.jpg", "Cover.jpg", "AlbumArt.jpg" or a number of other common names used by media players and taggers. +You can upload album artwork files using the Upload button in the file browser. The Upload button will allow you to upload audio files, or +folder artwork files that have a .jpg file extension. + + +""" ; + + mod:brand "TooB"; + mod:label "File Player"; + + # ONLY works in PiPedal + lv2:requiredFeature "http://github.com/rerdavies/pipedal#host" ; + + lv2:optionalFeature lv2:hardRTCapable,state::freePath, state:makePath, state:mapPath, work:schedule ; + lv2:extensionData state:interface, work:interface; + + patch:readable + toobPlayer:audioFile,toobPlayer:seek; + patch:writable + toobPlayer:audioFile; + + lv2:port + [ + a lv2:AudioPort , + lv2:InputPort ; + lv2:index 0 ; + lv2:symbol "inl" ; + lv2:name "In L" + ], + [ + a lv2:AudioPort , + lv2:InputPort ; + lv2:index 1 ; + lv2:symbol "inr" ; + lv2:name "In R" + ], + [ + a lv2:AudioPort , + lv2:OutputPort ; + lv2:index 2 ; + lv2:symbol "outl" ; + lv2:name "Out L" + ], + [ + a lv2:AudioPort , + lv2:OutputPort ; + lv2:index 3 ; + lv2:symbol "outr" ; + lv2:name "OutR" + ], + ###################### + [ + a atom:AtomPort , + lv2:InputPort ; + lv2:index 4 ; + atom:bufferType atom:Sequence ; + atom:supports patch:Message; + lv2:designation lv2:control ; + lv2:symbol "controlIn" ; + lv2:name "ControlIn" + ], + [ + a atom:AtomPort , + lv2:OutputPort ; + lv2:index 5 ; + atom:bufferType atom:Sequence ; + atom:supports patch:Message; + lv2:designation lv2:control ; + + lv2:symbol "controlOut" ; + lv2:name "ControlOut" + ], + + ###################### + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 6; + lv2:symbol "stop" ; + lv2:name "Stop"; + rdf:comment "Stop playback."; + lv2:default 0.0 ; + lv2:minimum 0.0; + lv2:maximum 1.0; + lv2:portProperty lv2:toggled,epp:trigger; + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 7; + lv2:symbol "pause" ; + lv2:name "Pause"; + rdf:comment "Pause playback."; + lv2:default 0.0 ; + lv2:minimum 0.0; + lv2:maximum 1.0; + lv2:portProperty lv2:toggled,epp:trigger; + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 8; + lv2:symbol "play" ; + lv2:name "Play"; + rdf:comment "Play selected file."; + lv2:default 0.0 ; + lv2:minimum 0.0; + lv2:maximum 1.0; + lv2:portProperty lv2:toggled,epp:trigger; + ], + + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 9 ; + lv2:symbol "volIn" ; + lv2:name "Input Vol"; + rdfs:comment "Volume audio passed through from the input. "; + + + lv2:default 0.0 ; + lv2:minimum -40.0 ; + lv2:maximum 30.0 ; + units:unit units:db ; + lv2:scalePoint + [ + rdfs:label "-INF" ; + rdf:value -40.0 + ]; + pg:group toobPlayer:mixGroup ; + + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 10; + lv2:symbol "panIn" ; + lv2:name "Input Pan"; + rdfs:comment "Pan for passed-through audio."; + + lv2:default 0.0 ; + lv2:minimum -1.0; + lv2:maximum 1.0; + pg:group toobPlayer:mixGroup ; + rdfs:comment "Input Pan"; + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 11 ; + lv2:symbol "volFile" ; + lv2:name "File Vol"; + rdfs:comment "Volume of the audio file being played."; + + lv2:default 0.0 ; + lv2:minimum -40.0 ; + lv2:maximum 30.0 ; + units:unit units:db ; + lv2:scalePoint + [ + rdfs:label "-INF" ; + rdf:value -40.0 + ]; + pg:group toobPlayer:mixGroup ; + + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 12; + lv2:symbol "panFile" ; + lv2:name "File Pan"; + rdfs:comment "Pan position of the file being played."; + + lv2:default 0.0 ; + lv2:minimum -1.0; + lv2:maximum 1.0; + pg:group toobPlayer:mixGroup ; + ], + + + + ########################### + [ + a lv2:OutputPort , + lv2:ControlPort ; + + lv2:index 13; + lv2:symbol "state" ; + lv2:name "Play State"; + lv2:portProperty epp:notOnGUI; + + lv2:default 0.0 ; + lv2:minimum 0.0; + lv2:maximum 10.0; + ], + [ + a lv2:OutputPort , + lv2:ControlPort ; + + lv2:index 14; + lv2:symbol "position" ; + lv2:name "Play Position"; + lv2:default 0.0 ; + lv2:minimum 0.0; + lv2:maximum 1.0E+38; + units:unit units:s ; + ], + [ + a lv2:OutputPort , + lv2:ControlPort ; + + lv2:index 15; + lv2:symbol "duration" ; + lv2:name "Duration"; + rdfs:comment "Length of the selected audio file."; + lv2:default 0.0 ; + lv2:minimum 0.0; + lv2:maximum 1.0E+38; + units:unit units:s ; + ] + + . + + diff --git a/lv2/aarch64/ToobAmp.lv2/ToobRecordMono.ttl b/lv2/aarch64/ToobAmp.lv2/ToobRecordMono.ttl index 0d4a731..b127513 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobRecordMono.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobRecordMono.ttl @@ -21,7 +21,6 @@ @prefix urid: . @prefix xsd: . @prefix ui: . -@prefix pprop: . @prefix pipedal_ui: . @@ -52,7 +51,7 @@ recordPrefix:audioFile doap:license ; doap:maintainer ; lv2:minorVersion 1 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; ui:ui ; @@ -61,7 +60,7 @@ recordPrefix:audioFile patch:writable recordPrefix:audioFile; - lv2:extensionData state:interface ; + lv2:extensionData state:interface, work:interface ; rdfs:comment """ Record the plugin's audio input signal to a monophonic file. See also: TooB Record Input (stereo), which records a stereo file. @@ -83,7 +82,7 @@ Record the plugin's audio input signal to a monophonic file. See also: TooB Reco lv2:default 0.0 ; lv2:minimum 0.0; lv2:maximum 1.0; - lv2:portProperty lv2:toggled,pprop:trigger; + lv2:portProperty lv2:toggled,epp:trigger; ], [ a lv2:InputPort , @@ -96,7 +95,7 @@ Record the plugin's audio input signal to a monophonic file. See also: TooB Reco lv2:default 0.0 ; lv2:minimum 0.0; lv2:maximum 1.0; - lv2:portProperty lv2:toggled,pprop:trigger; + lv2:portProperty lv2:toggled,epp:trigger; ], [ a lv2:OutputPort , @@ -117,11 +116,11 @@ Record the plugin's audio input signal to a monophonic file. See also: TooB Reco lv2:index 3; lv2:symbol "play" ; lv2:name "⏵"; - rdfs:comment "Preview recorded file. Click again to stop."; + rdfs:comment "Preview recorded file."; lv2:default 0.0 ; lv2:minimum 0.0; lv2:maximum 1.0; - lv2:portProperty lv2:toggled,pprop:trigger; + lv2:portProperty lv2:toggled,epp:trigger; ], [ a lv2:OutputPort , @@ -186,7 +185,7 @@ Record the plugin's audio input signal to a monophonic file. See also: TooB Reco lv2:index 7; lv2:symbol "level" ; - lv2:name "Level"; + lv2:name "Rec Lvl"; rdfs:comment "Input trim level for recording"; lv2:default 0.0 ; lv2:minimum -60.0; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobRecordStereo.ttl b/lv2/aarch64/ToobAmp.lv2/ToobRecordStereo.ttl index 04fc7d6..b466568 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobRecordStereo.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobRecordStereo.ttl @@ -88,7 +88,7 @@ myprefix:loop3_group doap:license ; doap:maintainer ; lv2:minorVersion 1 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; ui:ui ; @@ -97,7 +97,7 @@ myprefix:loop3_group patch:writable recordPrefix:audioFile; - lv2:extensionData state:interface ; + lv2:extensionData state:interface, work:interface ; rdfs:comment """ @@ -154,7 +154,7 @@ Record the plugin's audio input signal to a stereophonic file. See also: TooB Re lv2:index 3; lv2:symbol "play" ; lv2:name "⏵"; - rdfs:comment "Preview the recorded file. Click again to stop."; + rdfs:comment "Preview the recorded file."; lv2:default 0.0 ; lv2:minimum 0.0; lv2:maximum 1.0; @@ -223,8 +223,8 @@ Record the plugin's audio input signal to a stereophonic file. See also: TooB Re lv2:index 7; lv2:symbol "level" ; - lv2:name "Level"; - lv2:name "Input trim level for recording"; + lv2:name "Rec Lvl"; + rdfs:comment "Input trim level for recording"; lv2:default 0.0 ; lv2:minimum -60.0; lv2:maximum 30.0; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl b/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl index 7560605..90b5392 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl @@ -40,7 +40,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; rdfs:comment """ TooB Tuner is a chromatic guitar tuner. """ ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobVolume.ttl b/lv2/aarch64/ToobAmp.lv2/ToobVolume.ttl index b3e9e35..d78713b 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobVolume.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobVolume.ttl @@ -31,7 +31,7 @@ a lv2:Plugin , - lv2:UtilityPlugin ; + lv2:MixerPlugin ; doap:name "TooB Volume" , "TooB Volume"@en-gb ; @@ -39,7 +39,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 62 ; + lv2:microVersion 63 ; rdfs:comment """ Volume control. diff --git a/lv2/aarch64/ToobAmp.lv2/manifest.ttl b/lv2/aarch64/ToobAmp.lv2/manifest.ttl index 46a0088..03576f0 100644 --- a/lv2/aarch64/ToobAmp.lv2/manifest.ttl +++ b/lv2/aarch64/ToobAmp.lv2/manifest.ttl @@ -97,6 +97,10 @@ lv2:binary ; rdfs:seeAlso . + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso . + a lv2:Plugin ; lv2:binary ; rdfs:seeAlso . diff --git a/src/AlsaDriver.cpp b/src/AlsaDriver.cpp index 5bfa3e1..5df7144 100644 --- a/src/AlsaDriver.cpp +++ b/src/AlsaDriver.cpp @@ -144,8 +144,6 @@ namespace pipedal return false; } - - struct AudioFormat { char name[40]; @@ -304,12 +302,9 @@ namespace pipedal AlsaDriverImpl(AudioDriverHost *driverHost) : driverHost(driverHost) { - midiEventMemory.resize(MAX_MIDI_EVENT * MAX_MIDI_EVENT_SIZE); + midiEventMemoryIndex = 0; + midiEventMemory.resize(MIDI_MEMORY_BUFFER_SIZE); midiEvents.resize(MAX_MIDI_EVENT); - for (size_t i = 0; i < midiEvents.size(); ++i) - { - midiEvents[i].buffer = midiEventMemory.data() + i * MAX_MIDI_EVENT_SIZE; - } } virtual ~AlsaDriverImpl() { @@ -415,14 +410,7 @@ namespace pipedal snd_pcm_sw_params_free(playbackSwParams); playbackSwParams = nullptr; } - for (auto &midiState : this->midiDevices) - { - if (midiState) - { - midiState->Close(); - } - } - midiDevices.resize(0); + this->alsaSequencer = nullptr; } std::string discover_alsa_using_apps() @@ -913,7 +901,7 @@ namespace pipedal std::vector &buffers = this->playbackBuffers; int channels = this->playbackChannels; - constexpr double scale = 0x00FFFFFF; + constexpr double scale = 0x00FFFFFF; for (size_t frame = 0; frame < frames; ++frame) { for (int channel = 0; channel < channels; ++channel) @@ -1084,7 +1072,6 @@ namespace pipedal open = true; try { - OpenMidi(jackServerSettings, channelSelection); OpenAudio(jackServerSettings, channelSelection); std::atomic_thread_fence(std::memory_order::release); } @@ -1531,18 +1518,43 @@ namespace pipedal } while (frames > 0); return framesRead; } - + protected: void ReadMidiData(uint32_t audioFrame) { - for (size_t i = 0; i < midiDevices.size(); ++i) + AlsaMidiMessage message; + + midiEventCount = 0; + while(alsaSequencer->ReadMessage(message,0)) { - size_t nRead = midiDevices[i]->ReadMidiEvents( - this->midiEvents, - midiEventCount, - audioFrame); - midiEventCount += nRead; + size_t messageSize = message.size; + if (messageSize == 0) + { + continue; + } + if (midiEventMemoryIndex + messageSize >= this->midiEventMemory.size()) { + continue; + } + if (midiEventCount >= this->midiEvents.size()) { + midiEvents.resize(midiEventCount*2); + } + // for now, prevent META event messages from propagating. + if (message.data[0] == 0xFF && message.size > 1) { + continue; + } + MidiEvent *pEvent = midiEvents.data() + midiEventCount++; + pEvent->time = audioFrame; + pEvent->size = messageSize; + pEvent->buffer = midiEventMemory.data() + midiEventMemoryIndex; + + memcpy( + midiEventMemory.data() + midiEventMemoryIndex, + message.data, + message.size); + midiEventMemoryIndex += messageSize; + } } + private: long WriteBuffer(snd_pcm_t *handle, uint8_t *buf, size_t frames) { @@ -1671,16 +1683,17 @@ namespace pipedal pBuffer[j] = 0; } } - try { + try + { while (!terminateAudio()) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); // zero out input buffers. this->driverHost->OnProcess(this->bufferSize); } - } catch (const std::exception &e) + } + catch (const std::exception &e) { - } } this->driverHost->OnAudioTerminated(); @@ -1766,288 +1779,22 @@ namespace pipedal Lv2Log::debug("Audio thread joined."); } - static constexpr size_t MAX_MIDI_EVENT_SIZE = 3; - static constexpr size_t MIDI_BUFFER_SIZE = 16 * 1024; + static constexpr size_t MIDI_MEMORY_BUFFER_SIZE = 32 * 1024; static constexpr size_t MAX_MIDI_EVENT = 4 * 1024; size_t midiEventCount = 0; std::vector midiEvents; + size_t midiEventMemoryIndex = 0; std::vector midiEventMemory; + AlsaSequencer::ptr alsaSequencer; public: - class AlsaMidiDeviceImpl + + + + virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override { - private: - snd_rawmidi_t *hIn = nullptr; - snd_rawmidi_params_t *hInParams = nullptr; - std::string deviceName; - - // running status state. - uint8_t runningStatus = 0; - int dataLength = 0; - int dataIndex = 0; - size_t statusBytesRemaining = 0; - size_t data0 = 0; - size_t data1 = 0; - - bool inputProcessingSysex = false; - size_t inputSysexBufferCount = 0; - std::vector inputSysexBuffer; - - uint8_t readBuffer[1024]; - - void checkError(int result, const char *message) - { - if (result < 0) - { - throw PiPedalStateException(SS("Unexpected error: " << message << " (" << this->deviceName)); - } - } - - public: - AlsaMidiDeviceImpl() - { - inputSysexBuffer.resize(1024); - } - ~AlsaMidiDeviceImpl() { - Close(); - } - void Open(const AlsaMidiDeviceInfo &device) - { - runningStatus = 0; - inputProcessingSysex = false; - inputSysexBufferCount = 0; - - dataIndex = 0; - dataLength = 0; - - this->deviceName = device.description_; - - int err = snd_rawmidi_open(&hIn, nullptr, device.name_.c_str(), SND_RAWMIDI_NONBLOCK); - if (err < 0) - { - throw PiPedalStateException(SS("Can't open midi device " << deviceName << ". (" << snd_strerror(err))); - } - - err = snd_rawmidi_params_malloc(&hInParams); - checkError(err, "snd_rawmidi_params_malloc failed."); - - err = snd_rawmidi_params_set_buffer_size(hIn, hInParams, 2048); - checkError(err, "snd_rawmidi_params_set_buffer_size failed."); - - err = snd_rawmidi_params_set_no_active_sensing(hIn, hInParams, 1); - checkError(err, "snd_rawmidi_params_set_no_active_sensing failed."); - } - void Close() - { - if (hIn) - { - snd_rawmidi_close(hIn); - hIn = nullptr; - } - if (hInParams) - { - snd_rawmidi_params_free(hInParams); - hInParams = 0; - } - } - - int GetDataLength(uint8_t cc) - { - static int sDataLength[] = {0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 1, 1, 1, -1}; - return sDataLength[cc >> 4]; - } - - void MidiPut(uint8_t cc, uint8_t d0, uint8_t d1) - { - if (cc == 0) - return; - - // check for overrun. - if (inputEventBufferIndex >= pInputEventBuffer->size()) - { - return; - } - - auto &event = (*pInputEventBuffer)[inputEventBufferIndex]; - - event.time = inputSampleFrame; - event.size = dataLength + 1; - assert(dataLength + 1 <= MAX_MIDI_EVENT_SIZE); - event.buffer[0] = cc; - event.buffer[1] = d0; - event.buffer[2] = d1; - ++inputEventBufferIndex; - } - - void FillInputBuffer() - { - while (true) - { - ssize_t nRead = snd_rawmidi_read(hIn, readBuffer, sizeof(readBuffer)); - if (nRead == -EAGAIN) - return; - if (nRead < 0) - { - checkError(nRead, SS(this->deviceName << "MIDI event read failed. (" << snd_strerror(nRead)).c_str()); - } - ProcessInputBuffer(readBuffer, nRead); // expose write to test code. - } - } - - uint32_t inputSampleFrame = -1; - size_t inputEventBufferIndex; - std::vector *pInputEventBuffer = nullptr; - - size_t ReadMidiEvents( - std::vector &outputBuffer, - size_t startIndex, - uint32_t sampleFrame) - { - inputSampleFrame = sampleFrame; - inputEventBufferIndex = startIndex; - pInputEventBuffer = &outputBuffer; - FillInputBuffer(); - pInputEventBuffer = nullptr; - return inputEventBufferIndex - startIndex; - } - - void FlushSysex() - { - if (inputProcessingSysex) - { - // just discard it. :-/ - // if (this->eventCount != MAX_MIDI_EVENT) - // { - // auto *event = &(events[eventCount++]); - // event->size = this->bufferCount - sysexStartIndex; - // event->buffer = &(this->buffer[this->sysexStartIndex]); - // event->time = 0; - // } - // sysexStartIndex = -1; - } - inputProcessingSysex = false; - } - - int GetSystemCommonLength(uint8_t cc) - { - static int sizes[] = {-1, 1, 2, 1, -1, -1, 0, 0}; - return sizes[(cc >> 4) & 0x07]; - } - void ProcessInputBuffer(uint8_t *readBuffer, size_t nRead) - { - for (ssize_t i = 0; i < nRead; ++i) - { - uint8_t v = readBuffer[i]; - - if (v >= 0x80) - { - if (v >= 0xF0) - { - if (v == 0xF0) - { - inputProcessingSysex = true; - inputSysexBufferCount = 0; - - inputSysexBuffer[inputSysexBufferCount++] = 0xF0; - - runningStatus = 0; // discard subsequent data. - dataLength = -2; // indefinitely. - dataIndex = -1; - } - else if (v >= 0xF8) - { - // don't overwrite running status. - // don't break sysexes on a running status message. - // LV2 standard is ambiguous how realtime messages are handled, so just discard them. - continue; - } - else - { - FlushSysex(); - int length = GetSystemCommonLength(v); - if (length == -1) - break; // ignore illegal messages. - runningStatus = v; - dataLength = length; - dataIndex = 0; - } - } - else - { - FlushSysex(); - int dataLength = GetDataLength(v); - runningStatus = v; - if (dataLength == -1) - { - this->dataLength = dataLength; - dataIndex = -1; - } - else - { - this->dataLength = dataLength; - dataIndex = 0; - } - } - } - else - { - if (inputProcessingSysex) - { - if (inputSysexBufferCount != inputSysexBuffer.size()) - { - inputSysexBuffer[inputSysexBufferCount++] = v; - } - } - else - { - switch (dataIndex) - { - default: - // discard. - break; - case 0: - data0 = v; - dataIndex = 1; - break; - case 1: - data1 = v; - dataIndex = 2; - break; - } - } - } - if (dataIndex == dataLength && dataLength >= 0 && runningStatus != 0) - { - MidiPut(runningStatus, data0, data1); - dataIndex = 0; - } - } - } - }; - - std::vector> midiDevices; - - void OpenMidi(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) - { - const auto &devices = channelSelection.GetInputMidiDevices(); - - midiDevices.reserve(devices.size()); - - for (size_t i = 0; i < devices.size(); ++i) - { - try - { - const auto &device = devices[i]; - auto midiDevice = std::make_unique(); - midiDevice->Open(device); - midiDevices.push_back(std::move(midiDevice)); - } - catch (const std::exception &e) - { - Lv2Log::error(e.what()); - } - } + this->alsaSequencer = alsaSequencer; } virtual size_t InputBufferCount() const { return activeCaptureBuffers.size(); } diff --git a/src/AudioDriver.hpp b/src/AudioDriver.hpp index 6660761..71b94c2 100644 --- a/src/AudioDriver.hpp +++ b/src/AudioDriver.hpp @@ -25,7 +25,7 @@ #include "JackConfiguration.hpp" #include - +#include "AlsaSequencer.hpp" @@ -74,7 +74,7 @@ namespace pipedal { virtual float*GetOutputBuffer(size_t channe) = 0; virtual void Open(const JackServerSettings & jackServerSettings,const JackChannelSelection &channelSelection) = 0; - + virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) = 0; virtual void Activate() = 0; virtual void Deactivate() = 0; virtual void Close() = 0; diff --git a/src/AudioFiles.cpp b/src/AudioFiles.cpp index 6825241..ec67e28 100644 --- a/src/AudioFiles.cpp +++ b/src/AudioFiles.cpp @@ -33,6 +33,7 @@ #include "Lv2Log.hpp" #include "ss.hpp" #include "util.hpp" +#include #undef _GLIBCXX_DEBUG // Ensure we are not in debug mode, as this file is not compatible with it. #include "SQLiteCpp/SQLiteCpp.h" @@ -112,8 +113,9 @@ namespace static int64_t fileTimeToInt64(const fs::file_time_type &fileTime) { - std::chrono::system_clock::time_point system_time = std::chrono::clock_cast(fileTime); - auto result = std::chrono::duration_cast(system_time.time_since_epoch()).count(); + auto sctp = std::chrono::time_point_cast( + fileTime - fs::file_time_type::clock::now() + std::chrono::system_clock::now()); + auto result = std::chrono::duration_cast(sctp.time_since_epoch()).count(); return result; } static int64_t GetLastWriteTime(const fs::path &file) diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index 047237b..a255b9f 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -21,6 +21,7 @@ #include "util.hpp" #include #include "SchedulerPriority.hpp" +#include "AlsaSequencer.hpp" #include "Lv2Log.hpp" @@ -388,6 +389,9 @@ bool SystemMidiBinding::IsMatch(const MidiEvent &event) class AudioHostImpl : public AudioHost, private AudioDriverHost, private IPatchWriterCallback { private: + AlsaSequencer::ptr alsaSequencer; + AlsaSequencerDeviceMonitor::ptr alsaDeviceMonitor; + void OnWritePatchPropertyBuffer( PatchPropertyWriter::Buffer *); @@ -529,7 +533,7 @@ private: std::string GetAtomObjectType(uint8_t *pData) { - LV2_Atom_Object *pAtom = (LV2_Atom_Object *)pData; + LV2_Atom_Object *pAtom = (LV2_Atom_Object *)pData; if (pAtom->atom.type != uris.atom_Object) { throw std::invalid_argument("Not an Lv2 Object"); @@ -1248,12 +1252,26 @@ public: lv2_atom_forge_init(&inputWriterForge, pHost->GetMapFeature().GetMap()); cpuTemperatureMonitor = CpuTemperatureMonitor::Get(); + this->alsaSequencer = AlsaSequencer::Create(); + this->alsaDeviceMonitor = AlsaSequencerDeviceMonitor::Create(); + + this->alsaDeviceMonitor->StartMonitoring( + [this]( + AlsaSequencerDeviceMonitor::MonitorAction action, + int client, + const std::string &clientName) + { + HandleAlsaSequencerDevicesChanged(action, client, clientName); + } + ); } virtual ~AudioHostImpl() { + alsaDeviceMonitor->StopMonitoring(); Close(); CleanRestartThreads(true); audioDriver = nullptr; + this->alsaSequencer = nullptr; } virtual JackConfiguration GetServerConfiguration() @@ -1268,7 +1286,21 @@ public: { return this->sampleRate; } - + void HandleAlsaSequencerDevicesChanged( + AlsaSequencerDeviceMonitor::MonitorAction action, int client, const std::string &clientName) + { + if (pNotifyCallbacks) + { + if (action == AlsaSequencerDeviceMonitor::MonitorAction::DeviceRemoved) + { + pNotifyCallbacks->OnAlsaSequencerDeviceRemoved(client); + } + else if (action == AlsaSequencerDeviceMonitor::MonitorAction::DeviceAdded) + { + pNotifyCallbacks->OnAlsaSequencerDeviceAdded(client,clientName); + } + } + } void HandleAudioTerminatedAbnormally() { Lv2Log::error("Audio processing terminated unexpectedly."); @@ -1666,7 +1698,7 @@ public: try { audioDriver->Open(jackServerSettings, this->channelSelection); - + audioDriver->SetAlsaSequencer(this->alsaSequencer); this->sampleRate = audioDriver->GetSampleRate(); this->overrunGracePeriodSamples = (uint64_t)(((uint64_t)this->sampleRate) * OVERRUN_GRACE_PERIOD_S); @@ -1814,6 +1846,9 @@ public: } } + virtual void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) override; + + void OnNotifyPathPatchPropertyReceived( int64_t instanceId, const std::string &pathPatchPropertyUri, @@ -2261,6 +2296,13 @@ void AudioHostImpl::OnWritePatchPropertyBuffer( this->realtimeWriter.SendPathPropertyBuffer(buffer); } +void AudioHostImpl::SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) +{ + this->alsaSequencer->SetConfiguration(alsaSequencerConfiguration); +} + + + JSON_MAP_BEGIN(JackHostStatus) JSON_MAP_REFERENCE(JackHostStatus, active) JSON_MAP_REFERENCE(JackHostStatus, errorMessage) diff --git a/src/AudioHost.hpp b/src/AudioHost.hpp index 4fbf8a7..16a9fb3 100644 --- a/src/AudioHost.hpp +++ b/src/AudioHost.hpp @@ -39,6 +39,7 @@ namespace pipedal struct RealtimeNextMidiProgramRequest; class PluginHost; class Pedalboard; + class AlsaSequencerConfiguration; using PortMonitorCallback = std::function; @@ -178,6 +179,8 @@ namespace pipedal virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex,int64_t snapshotRequestId) = 0; virtual void OnAlsaDriverTerminatedAbnormally() = 0; + virtual void OnAlsaSequencerDeviceAdded(int client, const std::string &clientName) = 0; + virtual void OnAlsaSequencerDeviceRemoved(int client) = 0; }; class JackHostStatus @@ -225,6 +228,7 @@ namespace pipedal virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) = 0; virtual void Close() = 0; + virtual void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0; virtual uint32_t GetSampleRate() = 0; virtual JackConfiguration GetServerConfiguration() = 0; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2f3b1d7..7055495 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -9,9 +9,19 @@ set (ENABLE_BACKTRACE 0) set (USE_SANITIZE OFF) # seems to be broken on Ubuntu 24.10 + set(CXX_STANDARD 20) +find_package(PkgConfig REQUIRED) +pkg_check_modules(PipeWire REQUIRED libpipewire-0.3) + + +message(STATUS "PipeWire_INCLUDE_DIRS: ${PipeWire_INCLUDE_DIRS}" + "PipeWire_LIBRARIES: ${PipeWire_LIBRARIES}" + "PipeWire_CFLAGS: ${PipeWire_CFLAGS}" + "PipeWire_DEFINES: ${PipeWire_DEFINES}" + "PipeWire_VERSION: ${PipeWire_VERSION}") include(FetchContent) @@ -196,6 +206,7 @@ else() endif() set (PIPEDAL_SOURCES + PipewireInputStream.cpp PipewireInputStream.hpp AudioFiles.cpp AudioFiles.hpp AudioFileMetadataReader.cpp AudioFileMetadataReader.hpp AudioFileMetadata.hpp AudioFileMetadata.cpp @@ -217,7 +228,6 @@ set (PIPEDAL_SOURCES Lv2PluginChangeMonitor.cpp Lv2PluginChangeMonitor.hpp WebServerConfig.cpp WebServerConfig.hpp Locale.hpp Locale.cpp - Finally.hpp ZipFile.cpp ZipFile.hpp TemporaryFile.cpp TemporaryFile.hpp FilePropertyDirectoryTree.cpp FilePropertyDirectoryTree.hpp @@ -314,6 +324,7 @@ set (PIPEDAL_INCLUDES ${JACK_INCLUDE_DIRS} ${LILV_0_INCLUDE_DIRS} ${VST3_INCLUDES} ${WEBSOCKETPP_INCLUDE_DIRS} + ${PipeWire_INCLUDE_DIRS} . ) @@ -323,6 +334,7 @@ set(PIPEDAL_LIBS libpipedald zip pthread atomic stdc++fs asound avahi-common avahi-client systemd ${VST3_LIBRARIES} ${LILV_0_LIBRARIES} + asound # ${JACK_LIBRARIES} - pending delete for JACK support. ) @@ -332,13 +344,17 @@ set(PIPEDAL_LIBS libpipedald zip add_library(libpipedald STATIC ${PIPEDAL_SOURCES}) -target_include_directories(libpipedald PRIVATE ${PIPEDAL_INCLUDES} +target_compile_definitions(libpipedald PUBLIC "_REENTRANT") + +target_include_directories(libpipedald PUBLIC ${PIPEDAL_INCLUDES} ) target_link_libraries(libpipedald PUBLIC PiPedalCommon - SQLiteCpp) + SQLiteCpp + ${PipeWire_LIBRARIES} + ) if(${USE_PCH}) target_precompile_headers(libpipedald PRIVATE pch.h) @@ -360,6 +376,7 @@ target_link_libraries(pipedal_kconfig PRIVATE ftxui::dom PRIVATE ftxui::component # Not needed for this example. PRIVATE PiPedalCommon + asound systemd ) @@ -378,6 +395,7 @@ target_link_libraries(pipedald PRIVATE PiPedalCommon ${PIPEDAL_LIBS} ) + ################################# add_executable(hotspotManagerTest hotspotManagerTestMain.cpp @@ -385,7 +403,7 @@ add_executable(hotspotManagerTest target_link_libraries(hotspotManagerTest PRIVATE ${PIPEDAL_LIBS}) -set_target_properties(hotspotManagerTest PROPERTIES EXCLUDE_FROM_ALL true) +set_target_properties(hotspotManagerTest PROPERTIES EXCLUDE_FROM_ALL ${PIPEDAL_EXCLUDE_TESTS}) add_executable(AuxInTest @@ -396,6 +414,7 @@ add_executable(AuxInTest add_executable(pipedaltest testMain.cpp + PipewireInputStreamTest.cpp AudioFilesTest.cpp LRUCacheTest.cpp @@ -426,7 +445,7 @@ target_link_libraries(pipedaltest PRIVATE ${PIPEDAL_LIBS} ${ICU_LIBRARIES}) target_include_directories(pipedaltest PRIVATE ${PIPEDAL_INCLUDES} ) -set_target_properties(pipedaltest PROPERTIES EXCLUDE_FROM_ALL true) +set_target_properties(pipedaltest PROPERTIES EXCLUDE_FROM_ALL ${PIPEDAL_EXCLUDE_TESTS}) if (NOT GITHUB_ACTIONS) # Google perftools are not available on Github action servers. @@ -462,7 +481,7 @@ target_link_libraries(jsonTest PRIVATE PiPedalCommon) target_include_directories(jsonTest PRIVATE ${PIPEDAL_INCLUDES} ) -set_target_properties(jsonTest PROPERTIES EXCLUDE_FROM_ALL true) +set_target_properties(jsonTest PROPERTIES EXCLUDE_FROM_ALL ${PIPEDAL_EXCLUDE_TESTS}) target_link_libraries(jsonTest PRIVATE pthread) diff --git a/src/DummyAudioDriver.cpp b/src/DummyAudioDriver.cpp index a4b58c5..37b25bb 100644 --- a/src/DummyAudioDriver.cpp +++ b/src/DummyAudioDriver.cpp @@ -104,6 +104,11 @@ namespace pipedal captureChannels = channels; playbackChannels = channels; + midiEventMemoryIndex = 0; + midiEventMemory.resize(MIDI_MEMORY_BUFFER_SIZE); + midiEvents.resize(MAX_MIDI_EVENT); + + } virtual ~DummyDriverImpl() { @@ -122,6 +127,16 @@ namespace pipedal } JackServerSettings jackServerSettings; + AlsaSequencer::ptr alsaSequencer; + + static constexpr size_t MIDI_MEMORY_BUFFER_SIZE = 32 * 1024; + static constexpr size_t MAX_MIDI_EVENT = 4 * 1024; + + size_t midiEventCount = 0; + std::vector midiEvents; + size_t midiEventMemoryIndex = 0; + std::vector midiEventMemory; + unsigned int periods = 0; @@ -158,11 +173,13 @@ namespace pipedal } } - virtual size_t GetMidiInputEventCount() override { - return 0; + virtual size_t GetMidiInputEventCount() override + { + return midiEventCount; } - virtual MidiEvent*GetMidiEvents() { - return nullptr; + virtual MidiEvent *GetMidiEvents() override + { + return this->midiEvents.data(); } @@ -190,7 +207,11 @@ namespace pipedal throw; } } - virtual std::string GetConfigurationDescription() + virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override + { + this->alsaSequencer = alsaSequencer; + } + virtual std::string GetConfigurationDescription() { std::string result = SS( "DUMMY, " @@ -219,6 +240,43 @@ namespace pipedal bool block = false; + void ReadMidiData(uint32_t audioFrame) + { + AlsaMidiMessage message; + + midiEventCount = 0; + while(alsaSequencer->ReadMessage(message,0)) + { + size_t messageSize = message.size; + if (messageSize == 0) + { + continue; + } + if (midiEventMemoryIndex + messageSize >= this->midiEventMemory.size()) { + continue; + } + if (midiEventCount >= this->midiEvents.size()) { + midiEvents.resize(midiEventCount*2); + } + // for now, prevent META event messages from propagating. + if (message.data[0] == 0xFF && message.size > 1) { + continue; + } + MidiEvent *pEvent = midiEvents.data() + midiEventCount++; + pEvent->time = audioFrame; + pEvent->size = messageSize; + pEvent->buffer = midiEventMemory.data() + midiEventMemoryIndex; + + memcpy( + midiEventMemory.data() + midiEventMemoryIndex, + message.data, + message.size); + midiEventMemoryIndex += messageSize; + + } + } + + void AudioThread() { SetThreadName("dummyAudioDriver"); @@ -231,11 +289,14 @@ namespace pipedal while (true) { + if (terminateAudio()) { break; } + ReadMidiData((uint32_t)0); + ssize_t framesRead = this->bufferSize; this->driverHost->OnProcess(framesRead); @@ -335,8 +396,6 @@ namespace pipedal Lv2Log::debug("Audio thread joined."); } - static constexpr size_t MIDI_BUFFER_SIZE = 16 * 1024; - static constexpr size_t MAX_MIDI_EVENT = 4 * 1024; public: diff --git a/src/HotspotManager.cpp b/src/HotspotManager.cpp index 0e055c6..5a936d2 100644 --- a/src/HotspotManager.cpp +++ b/src/HotspotManager.cpp @@ -80,7 +80,7 @@ namespace pipedal::impl bool hasWifi = false; HasWifiListener hasWifiListener; void SetHasWifi(bool hasWifi); - virtual bool GetHasWifi() override ; + virtual bool GetHasWifi() override; void onClose(); void onError(const std::string &message); @@ -347,7 +347,7 @@ void HotspotManagerImpl::onStartMonitoring() this->networkManager = NetworkManager::Create(dbusDispatcher); - if (!networkManager->WirelessEnabled()) + if (!networkManager->WirelessEnabled() && wifiConfigSettings.NeedsWifi()) { networkManager->WirelessEnabled(true); } @@ -483,6 +483,7 @@ void HotspotManagerImpl::onReload() // force a reload. StopHotspot(); SetState(State::Monitoring); + StartScanTimer(); // or stop it as the case may be. MaybeStartHotspot(); return; } @@ -758,16 +759,26 @@ void HotspotManagerImpl::MaybeStartHotspot() // devices are transitioning. Do nothing. return; } - std::vector allAccessPoints = GetAllAccessPoints(); - std::vector> allAccessPointSsids = GetAccessPointSsids(allAccessPoints); - std::vector> connectableSsids = GetKnownVisibleAccessPoints(allAccessPointSsids); // all the ssids currently visible for which we have credentials. - this->UpdateKnownNetworks(connectableSsids, allAccessPoints); + bool wantsHotspot; - bool wantsHotspot = this->wifiConfigSettings.WantsHotspot( + if (wifiConfigSettings.NeedsScan()) { - this->ethernetConnected, connectableSsids, allAccessPointSsids); + std::vector allAccessPoints = GetAllAccessPoints(); + std::vector> allAccessPointSsids = GetAccessPointSsids(allAccessPoints); + std::vector> connectableSsids = GetKnownVisibleAccessPoints(allAccessPointSsids); // all the ssids currently visible for which we have credentials. + this->UpdateKnownNetworks(connectableSsids, allAccessPoints); + + wantsHotspot = this->wifiConfigSettings.WantsHotspot( + this->ethernetConnected, connectableSsids, allAccessPointSsids); + + + } else { + wantsHotspot = this->wifiConfigSettings.WantsHotspot( + this->ethernetConnected); + } + if (this->state == State::Monitoring && wantsHotspot) { StartHotspot(); @@ -996,8 +1007,14 @@ void HotspotManagerImpl::ScanNow() try { - Lv2Log::debug("Scanning"); - wlanWirelessDevice->RequestScan(options); + if (this->wifiConfigSettings.NeedsScan()) + { + + Lv2Log::debug("Scanning"); + wlanWirelessDevice->RequestScan(options); + } else { + return; + } } catch (const std::exception &e) { @@ -1036,7 +1053,8 @@ void HotspotManagerImpl::SetHasWifiListener(HasWifiListener &&listener) this->hasWifiListener(this->hasWifi); } } -bool HotspotManagerImpl::GetHasWifi() { +bool HotspotManagerImpl::GetHasWifi() +{ std::lock_guard lock{this->networkChangingListenerMutex}; return hasWifi; } @@ -1112,17 +1130,20 @@ static bool IsNetworkManagerRunning() return gUsingNetworkManager; } bool bResult = false; - auto result = sysExecForOutput("systemctl","is-active NetworkManager"); + auto result = sysExecForOutput("systemctl", "is-active NetworkManager"); if (result.exitCode == EXIT_SUCCESS) { - std::string text = result.output.erase(result.output.find_last_not_of(" \n\r\t")+1); + std::string text = result.output.erase(result.output.find_last_not_of(" \n\r\t") + 1); if (text == "active") { bResult = true; - } else if (text == "inactive") + } + else if (text == "inactive") { bResult = false; - } else { + } + else + { throw std::runtime_error(SS("UsingNetworkManager: unexpected result (" << text << ")")); } gNetworkManagerTestExecuted = true; @@ -1134,19 +1155,17 @@ static bool IsNetworkManagerRunning() return false; } - bool HotspotManager::HasWifiDevice() { // use procfs to decide this, as NetworkManager may not be available yet. - if ( !(get_wireless_interfaces_sysfs().empty()) ) + if (!(get_wireless_interfaces_sysfs().empty())) { return false; } - if (!IsNetworkManagerRunning()) + if (!IsNetworkManagerRunning()) { return false; } return true; - } diff --git a/src/JackConfiguration.cpp b/src/JackConfiguration.cpp index 6ea3949..da7e256 100644 --- a/src/JackConfiguration.cpp +++ b/src/JackConfiguration.cpp @@ -23,6 +23,7 @@ #include "AlsaDriver.hpp" #include "Lv2Log.hpp" #include "PiPedalException.hpp" +#include "AlsaSequencer.hpp" #if JACK_HOST @@ -106,6 +107,23 @@ namespace pipedal { #endif } +static std::vector GetAlsaSequencers() { + std::vector result; + try { + auto sequencer = AlsaSequencer::Create(); + if (sequencer) + { + std::vector ports = sequencer->EnumeratePorts(); + for (const auto &port : ports) + { + result.push_back(AlsaMidiDeviceInfo(port.id.c_str(), port.name.c_str())); + } + } + } catch (const std::exception& e) { + Lv2Log::error("Failed to enumerate ALSA sequencers: %s", e.what()); + } + return result; +} void JackConfiguration::AlsaInitialize( const JackServerSettings &jackServerSettings) { @@ -116,7 +134,7 @@ void JackConfiguration::AlsaInitialize( { this->inputMidiDevices_.clear(); } else { - this->inputMidiDevices_ = GetAlsaMidiInputDevices(); + this->inputMidiDevices_ = GetAlsaSequencers(); // NB: Sequencers, not rawmidi devices, anymore. } if (jackServerSettings.IsValid()) { diff --git a/src/JackConfiguration.hpp b/src/JackConfiguration.hpp index 59a2ec8..b76abb0 100644 --- a/src/JackConfiguration.hpp +++ b/src/JackConfiguration.hpp @@ -107,7 +107,13 @@ namespace pipedal return outputAudioPorts_; } - const std::vector& GetInputMidiDevices() const + // replaced with AlsaSequencerConfiguration + const std::vector& LegacyGetInputMidiDevices() const + { + return inputMidiDevices_; + } + // replaced with AlsaSequencerConfiguration + std::vector& LegacyGetInputMidiDevices() { return inputMidiDevices_; } diff --git a/src/PiPedalAlsa.cpp b/src/PiPedalAlsa.cpp index 63e200a..af1be7e 100644 --- a/src/PiPedalAlsa.cpp +++ b/src/PiPedalAlsa.cpp @@ -47,6 +47,7 @@ void PiPedalAlsaDevices::cacheDevice(const std::string &name, const AlsaDeviceIn std::vector PiPedalAlsaDevices::GetAlsaDevices() { + std::lock_guard guard{alsaMutex}; std::vector result; @@ -118,7 +119,7 @@ std::vector PiPedalAlsaDevices::GetAlsaDevices() if (err == 0) { unsigned int minRate = 0, maxRate = 0; - snd_pcm_uframes_t minBufferSize = 0, maxBufferSize = 0; + snd_pcm_uframes_t minBufferSize = 0, maxBufferSize = 0; int dir; err = snd_pcm_hw_params_get_rate_min(params, &minRate, &dir); if (err == 0) @@ -143,7 +144,8 @@ std::vector PiPedalAlsaDevices::GetAlsaDevices() info.sampleRates_.push_back(rate); } } - if (minBufferSize < 16) { + if (minBufferSize < 16) + { minBufferSize = 16; } @@ -180,7 +182,207 @@ std::vector PiPedalAlsaDevices::GetAlsaDevices() return result; } -static std::vector GetAlsaDevices(const char *devname, const char *direction) +static void AddMidiCardDevicesToList(snd_ctl_t *ctl, int card, int device, AlsaMidiDeviceInfo::Direction direction, std::vector *result) +{ + snd_rawmidi_info_t *info = nullptr; + const char *name = nullptr; + const char *sub_name = nullptr; + int subs = 0, subs_in = 0, subs_out = 0; + int sub = 0; + int err = 0; + ; + + snd_rawmidi_info_alloca(&info); + snd_rawmidi_info_set_device(info, device); + + snd_rawmidi_info_set_stream(info, SND_RAWMIDI_STREAM_INPUT); + err = snd_ctl_rawmidi_info(ctl, info); + if (err >= 0) + subs_in = snd_rawmidi_info_get_subdevices_count(info); + else + subs_in = 0; + + snd_rawmidi_info_set_stream(info, SND_RAWMIDI_STREAM_OUTPUT); + err = snd_ctl_rawmidi_info(ctl, info); + if (err >= 0) + subs_out = snd_rawmidi_info_get_subdevices_count(info); + else + subs_out = 0; + + subs = subs_in > subs_out ? subs_in : subs_out; + if (!subs) + return; + + switch (direction) + { + case AlsaMidiDeviceInfo::In: + if (subs_out == 0) // out for the device, in for us. + { + return; // no input devices to add + } + break; + case AlsaMidiDeviceInfo::Out: // in for the device, out for us. + if (subs_in == 0) + { + return; // no output devices to add + } + break; + case AlsaMidiDeviceInfo::InOut: + if (subs_in == 0 || subs_out == 0) + { + return; // no input or output devices to add + } + break; + case AlsaMidiDeviceInfo::None: + return; // no devices to add + } + + for (sub = 0; sub < subs; ++sub) + { + snd_rawmidi_info_set_stream(info, direction == AlsaMidiDeviceInfo::Direction::Out ? SND_RAWMIDI_STREAM_INPUT : SND_RAWMIDI_STREAM_OUTPUT); + snd_rawmidi_info_set_subdevice(info, sub); + err = snd_ctl_rawmidi_info(ctl, info); + if (err < 0) + { + throw std::runtime_error( + SS("snd_ctl_rawmidi_info failed for card " << card << ", device " << device << ", subdevice " << sub << ": " << snd_strerror(err))); + } + name = snd_rawmidi_info_get_name(info); + sub_name = snd_rawmidi_info_get_subdevice_name(info); + // get card name. + + std::string cardName; + snd_ctl_card_info_t *card_info = nullptr; + snd_ctl_card_info_malloc(&card_info); + if (snd_ctl_card_info(ctl, card_info) == 0) { + cardName = snd_ctl_card_info_get_name(card_info); + } + snd_ctl_card_info_free(card_info); + if (cardName.length() == 0) + { + return; + } + + if (sub == 0 && sub_name[0] == '\0') + { + AlsaMidiDeviceInfo info; + info.name_ = SS("hw:CARD=" << cardName << ",DEV=" << device); + info.description_ = SS("Virtual MIDI " << card << "-" << device); + info.card_ = card; + info.device_ = device; + info.subdevice_ = 0; + info.isVirtual_ = true; + info.subDevices_ = subs; + if (subs_in > 0 && subs_out > 0) + { + info.direction_ = AlsaMidiDeviceInfo::InOut; + } + else if (subs_in > 0) + { + info.direction_ = AlsaMidiDeviceInfo::In; + } + else if (subs_out > 0) + { + info.direction_ = AlsaMidiDeviceInfo::Out; + } + else + { + info.direction_ = AlsaMidiDeviceInfo::None; + } + result->push_back(info); + break; + } + else + { + AlsaMidiDeviceInfo info; + info.name_ = SS("hw:CARD=" << cardName << ",DEV=" << device); + if (sub != 0) { + info.name_ = SS(info.name_ << "," << sub); + } + info.description_ = sub_name; + info.card_ = card; + info.device_ = device; + info.subdevice_ = sub; + info.isVirtual_ = false; + info.subDevices_ = 1; + result->push_back(info); + } + } +} +static void AddMidiCardToList(int card, std::vector *result, AlsaMidiDeviceInfo::Direction direction) +{ + snd_ctl_t *ctl = nullptr; + char name[32]; + int device; + int err; + + sprintf(name, "hw:%d", card); + if ((err = snd_ctl_open(&ctl, name, 0)) < 0) + { + throw std::runtime_error(SS("cannot open control for card " << card << ": " << snd_strerror(err))); + } + device = -1; + for (;;) + { + if ((err = snd_ctl_rawmidi_next_device(ctl, &device)) < 0) + { + snd_ctl_close(ctl); + throw std::runtime_error(SS("cannot get next rawmidi device for card " << card << ": " << snd_strerror(err))); + } + if (device < 0) + break; + AddMidiCardDevicesToList(ctl, card, device, direction, result); + } + snd_ctl_close(ctl); +} + +static std::vector GetAlsaDevices(const char *devname, const char *direction_) +{ + std::vector result; + + int card, err; + + AlsaMidiDeviceInfo::Direction direction; + if (strcmp(direction_, "Input") == 0) + { + direction = AlsaMidiDeviceInfo::In; + } + else if (strcmp(direction_, "Output") == 0) + { + direction = AlsaMidiDeviceInfo::Out; + } + else if (strcmp(direction_, "InOut") == 0) + { + direction = AlsaMidiDeviceInfo::InOut; + } + else + { + direction = AlsaMidiDeviceInfo::None; + return result; + } + + card = -1; + if ((err = snd_card_next(&card)) < 0) + { + throw std::runtime_error(SS("snd_card_next failed.: " << snd_strerror(err))); + } + if (card < 0) + { + // no devices! + return result; + } + do + { + AddMidiCardToList(card, &result, direction); + if ((err = snd_card_next(&card)) < 0) + { + throw std::runtime_error(SS("snd_card_next failed: " << snd_strerror(err))); + } + } while (card >= 0); + + return result; +} +static std::vector OldGetAlsaDevices(const char *devname, const char *direction) { std::vector result; { @@ -214,6 +416,8 @@ static std::vector GetAlsaDevices(const char *devname, const result.push_back(AlsaMidiDeviceInfo(name, desc)); } } + // translate rawmidi device name to device number. + if (name && strcmp("null", name) != 0) free(name); if (desc && strcmp("null", desc) != 0) @@ -229,11 +433,11 @@ static std::vector GetAlsaDevices(const char *devname, const return result; } -std::vector pipedal::GetAlsaMidiInputDevices() +std::vector pipedal::LegacyGetAlsaMidiInputDevices() { return GetAlsaDevices("rawmidi", "Input"); } -std::vector pipedal::GetAlsaMidiOutputDevices() +std::vector pipedal::LegacyGetAlsaMidiOutputDevices() { return GetAlsaDevices("rawmidi", "Output"); } @@ -260,6 +464,14 @@ AlsaMidiDeviceInfo::AlsaMidiDeviceInfo(const char *name, const char *description } } +AlsaMidiDeviceInfo::AlsaMidiDeviceInfo(const char *name, const char *description, int card, int device, int subdevice) + : AlsaMidiDeviceInfo(name, description) +{ + this->card_ = card; + this->device_ = device; + this->subdevice_ = subdevice; +} + JSON_MAP_BEGIN(AlsaDeviceInfo) JSON_MAP_REFERENCE(AlsaDeviceInfo, cardId) JSON_MAP_REFERENCE(AlsaDeviceInfo, id) diff --git a/src/PiPedalAlsa.hpp b/src/PiPedalAlsa.hpp index 3bbf8ed..cf602b2 100644 --- a/src/PiPedalAlsa.hpp +++ b/src/PiPedalAlsa.hpp @@ -41,11 +41,26 @@ namespace pipedal { }; class AlsaMidiDeviceInfo { public: + enum Direction { + None = 0, + In = 1, + Out = 2, + InOut = 3 + }; AlsaMidiDeviceInfo() { } AlsaMidiDeviceInfo(const char*name, const char*description); + AlsaMidiDeviceInfo(const char*name, const char*description, int card, int device, int subdevice); std::string name_; std::string description_; + // non-serialized. + int card_ = -1; + int device_ = -1; + int subdevice_ = -1; + bool isVirtual_ = false; + int subDevices_ = -1; + Direction direction_ = Direction::None; + DECLARE_JSON_MAP(AlsaMidiDeviceInfo); }; @@ -60,6 +75,9 @@ namespace pipedal { std::vector GetAlsaDevices(); }; - std::vector GetAlsaMidiInputDevices(); - std::vector GetAlsaMidiOutputDevices(); + // we use ALSA sequencers now instead of ALSA rawmidi devices. + // Used by test suite to verify migration behaviour. + std::vector LegacyGetAlsaMidiInputDevices(); + std::vector LegacyGetAlsaMidiOutputDevices(); + } \ No newline at end of file diff --git a/src/PiPedalAlsaTest.cpp b/src/PiPedalAlsaTest.cpp index c0c4da5..aa36d68 100644 --- a/src/PiPedalAlsaTest.cpp +++ b/src/PiPedalAlsaTest.cpp @@ -38,10 +38,8 @@ static void DiscoveryTest() auto result = devices.GetAlsaDevices(); std::cout << result.size() << " ALSA devices found." << std::endl; - auto midiInputDevices = GetAlsaMidiInputDevices(); - std::cout << midiInputDevices.size() << " ALSA MIDI input devices found." << std::endl; - auto midiOutputDevices = GetAlsaMidiOutputDevices(); - std::cout << midiOutputDevices.size() << " ALSA MIDI output devices found." << std::endl; + auto midiInputDevices = AlsaSequencer::EnumeratePorts(); + std::cout << midiInputDevices.size() << " ALSA MIDI input sequencers found." << std::endl; } void EnumerateSequencers() @@ -86,27 +84,58 @@ void EnumerateSequencers() void ReadFromsequencerTest() { cout << "--- Reading from ALSA Sequencer" << endl; - AlsaSequencer sequencer; - sequencer.ConnectPort("V25 V25 In"); + AlsaSequencer::ptr sequencer = AlsaSequencer::Create(); + + sequencer->ConnectPort("V25 V25 In"); AlsaMidiMessage message; while (true) { - if (sequencer.ReadMessage(message)) + if (sequencer->ReadMessage(message,true)) { cout << "Received MIDI message: " - << " " << hex << setfill('0') << setw(2) << static_cast(message.cc0) - << " " << hex << setfill('0') << setw(2) << static_cast(message.cc1) - << " " << hex << setfill('0') << setw(2) << static_cast(message.cc2) + << " " << hex << setfill('0') << setw(2) << static_cast(message.cc0()) + << " " << hex << setfill('0') << setw(2) << static_cast(message.cc1()) + << " " << hex << setfill('0') << setw(2) << static_cast(message.cc2()) << " Timestamp: " << setw(8) << dec << message.timestamp << std::endl; } } } +void TestConfigMigration() +{ + cout << "--- Testing ALSA Config Migration" << endl; + // older versions of PiPedal uses ALSA rawmidi. + // this code test coversion of rawmidi device IDs to ALSA Sequencer IDs. + auto rawDevices = LegacyGetAlsaMidiInputDevices(); + auto seqDevices = AlsaSequencer::EnumeratePorts(); + + for (const auto&seqDevice: seqDevices) { + cout << seqDevice.id << " -> " << seqDevice.rawMidiDevice << endl; + } + for (const auto&rawDevice: rawDevices) { + std::string sequencerId = RawMidiIdToSequencerId(seqDevices,rawDevice.name_); + REQUIRE(!sequencerId.empty()); + cout << rawDevice.name_ << "->" << sequencerId << std::endl; + } + cout << endl; + + +} + TEST_CASE("ALSA Seq Test", "[pipedal_alsa_seq_test][Build][Dev]") { + + // hw:CARD=VirMIDI,DEV=0 VirMIDI + // hw:CARD=VirMIDI,DEV=1 VirMIDI + // hw:CARD=VirMIDI,DEV=2 VirMIDI + // hw:CARD=VirMIDI,DEV=3 VirMIDI + // hw:CARD=M2,DEV=0 M2 + // hw:CARD=V25,DEV=0 V25 EnumerateSequencers(); + TestConfigMigration(); + ReadFromsequencerTest(); } diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 51f8448..91fe6b8 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -296,6 +296,8 @@ void PiPedalModel::Load() this->audioHost->SetSystemMidiBindings(this->systemMidiBindings); + audioHost->SetAlsaSequencerConfiguration(storage.GetAlsaSequencerConfiguration()); + if (configuration.GetMLock()) { #ifndef NO_MLOCK @@ -509,7 +511,6 @@ void PiPedalModel::FireBanksChanged(int64_t clientId) } } - void PiPedalModel::FirePedalboardChanged(int64_t clientId, bool loadAudioThread) { if (loadAudioThread) @@ -595,14 +596,11 @@ void PiPedalModel::UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalbo UpdateVst3Settings(pedalboard); - - Lv2PedalboardErrorList errorMessages; std::shared_ptr lv2Pedalboard{ - this->pluginHost.UpdateLv2PedalboardStructure(pedalboard, this->lv2Pedalboard.get(), errorMessages) - }; + this->pluginHost.UpdateLv2PedalboardStructure(pedalboard, this->lv2Pedalboard.get(), errorMessages)}; this->lv2Pedalboard = lv2Pedalboard; - + // apply the error messages to the lv2Pedalboard. // return true if the error messages have changed audioHost->SetPedalboard(lv2Pedalboard); @@ -614,9 +612,7 @@ void PiPedalModel::UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalbo UpdateRealtimeVuSubscriptions(); UpdateRealtimeMonitorPortSubscriptions(); - - - this->FirePedalboardChanged(clientId,false); + this->FirePedalboardChanged(clientId, false); this->SetPresetChanged(clientId, true); } } @@ -1400,6 +1396,77 @@ void PiPedalModel::RestartAudio(bool useDummyAudioDriver) } } +void PiPedalModel::OnAlsaSequencerDeviceAdded(int client, const std::string &clientName) +{ + std::lock_guard lock(mutex); + auto alsaSequencerConfiguration = this->storage.GetAlsaSequencerConfiguration(); + std::string key = "seq:" + clientName; + bool interested = false; + for (const auto &port : alsaSequencerConfiguration.connections()) + { + if (port.id().starts_with(key)) + { + interested = true; + break; + } + } + if (interested) + { + Post( + [this] + { + // reconfigure connections. + std::lock_guard lock(this->mutex); + if (this->audioHost) { + this->audioHost->SetAlsaSequencerConfiguration(this->storage.GetAlsaSequencerConfiguration()); + } + }); + } +} +void PiPedalModel::OnAlsaSequencerDeviceRemoved(int client) +{ + // no action required. +} + +void PiPedalModel::SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) +{ + std::lock_guard lock(mutex); + + // reset midi connections even if the configuration hasn't changed. + this->audioHost->SetAlsaSequencerConfiguration(alsaSequencerConfiguration); + + auto current = storage.GetAlsaSequencerConfiguration(); + if (alsaSequencerConfiguration != current) + { + this->storage.SetAlsaSequencerConfiguration(alsaSequencerConfiguration); + // notify subscribers. + std::vector t{subscribers.begin(), subscribers.end()}; + for (auto &subscriber : t) + { + subscriber->OnAlsaSequencerConfigurationChanged(alsaSequencerConfiguration); + } + } +} +AlsaSequencerConfiguration PiPedalModel::GetAlsaSequencerConfiguration() +{ + std::lock_guard lock(mutex); + return this->storage.GetAlsaSequencerConfiguration(); +} + +std::vector PiPedalModel::GetAlsaSequencerPorts() +{ + auto ports = AlsaSequencer::EnumeratePorts(); + std::vector result; + for (auto &port : ports) + { + result.push_back(AlsaSequencerPortSelection{ + port.id, + port.name, + port.displaySortOrder}); + } + return result; +} + void PiPedalModel::SetJackChannelSelection(int64_t clientId, const JackChannelSelection &channelSelection) { { @@ -1618,7 +1685,7 @@ void PiPedalModel::SendSetPatchProperty( std::lock_guard lock(mutex); if (!audioHost) { - onError("Audio not running."); + onError("Audio not running."); return; } @@ -1629,8 +1696,9 @@ void PiPedalModel::SendSetPatchProperty( std::shared_ptr pluginInfo = GetPluginInfo(pedalboardItem->uri_); auto pipedalUi = pluginInfo->piPedalUI(); auto fileProperty = pipedalUi->GetFileProperty(propertyUri); - if (fileProperty && value.is_string()) { - + if (fileProperty && value.is_string()) + { + json_variant abstractPath = pluginHost.AbstractPath(value); std::ostringstream ss; json_writer writer(ss); @@ -1680,12 +1748,11 @@ void PiPedalModel::SendSetPatchProperty( }}; LV2_URID urid = this->pluginHost.GetLv2Urid(propertyUri.c_str()); - size_t sampleTimeout = 0.5*audioHost->GetSampleRate(); + size_t sampleTimeout = 0.5 * audioHost->GetSampleRate(); RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest( onRequestComplete, clientId, instanceId, urid, atomValue, nullptr, onError, - sampleTimeout - ); + sampleTimeout); outstandingParameterRequests.push_back(request); if (this->audioHost) @@ -1753,10 +1820,10 @@ void PiPedalModel::SendGetPatchProperty( { onError("Audio stopped."); } - size_t sampleTimeout = 0.3*audioHost->GetSampleRate(); + size_t sampleTimeout = 0.3 * audioHost->GetSampleRate(); RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest( onRequestComplete, - clientId, instanceId, urid, onSuccess, onError,sampleTimeout); + clientId, instanceId, urid, onSuccess, onError, sampleTimeout); outstandingParameterRequests.push_back(request); this->audioHost->sendRealtimeParameterRequest(request); @@ -1920,7 +1987,9 @@ void PiPedalModel::UpdateDefaults(PedalboardItem *pedalboardItem, std::unordered { pedalboardItem->midiChannelBinding(MidiChannelBinding::DefaultForMissingValue()); } - } else { + } + else + { if (pedalboardItem->midiChannelBinding()) { pedalboardItem->midiChannelBinding(std::optional()); // clear it. @@ -2196,22 +2265,23 @@ void PiPedalModel::OnNotifyMidiListen(uint8_t cc0, uint8_t cc1, uint8_t cc2) { std::lock_guard lock(mutex); bool isNote = (cc0 & 0xF0) == 0x90; // Note On - if (isNote && cc2 == 0) { + if (isNote && cc2 == 0) + { return; // Note off. Oopsie. } - bool isControl = (cc0 & 0xF0) == 0xB0; // Control Change - if (!isNote && !isControl) { + bool isControl = (cc0 & 0xF0) == 0xB0; // Control Change + if (!isNote && !isControl) + { return; // Not a note on or control change. } - - for (int i = 0; i < midiEventListeners.size(); ++i) + for (int i = 0; i < midiEventListeners.size(); ++i) { auto &listener = midiEventListeners[i]; auto subscriber = this->GetNotificationSubscriber(listener.clientId); if (subscriber) { - subscriber->OnNotifyMidiListener(listener.clientHandle, cc0,cc1,cc2); + subscriber->OnNotifyMidiListener(listener.clientHandle, cc0, cc1, cc2); } else { @@ -2296,6 +2366,7 @@ void PiPedalModel::CancelMonitorPatchProperty(int64_t clientId, int64_t clientHa audioHost->SetListenForMidiEvent(false); } } + std::vector PiPedalModel::GetAlsaDevices() { std::vector result = this->alsaDevices.GetAlsaDevices(); @@ -2353,9 +2424,9 @@ void PiPedalModel::SetSystemMidiBindings(std::vector &bindings) } } -PedalboardItem*PiPedalModel::GetPedalboardItemForFileProperty(const UiFileProperty& fileProperty) +PedalboardItem *PiPedalModel::GetPedalboardItemForFileProperty(const UiFileProperty &fileProperty) { - for (PedalboardItem*pedalboardItem: this->pedalboard.GetAllPlugins()) + for (PedalboardItem *pedalboardItem : this->pedalboard.GetAllPlugins()) { if (pedalboardItem->pathProperties_.contains(fileProperty.patchProperty())) { @@ -2369,25 +2440,24 @@ FileRequestResult PiPedalModel::GetFileList2(const std::string &relativePath_, c std::string relativePath = relativePath_; try { - if (!storage.IsInUploadsDirectory(relativePath)) + if (!storage.IsInUploadsDirectory(relativePath)) { - // if relativePath is in a resource directory of the plugin, then we have loaded a factory preset or are using a default property. - // map the resource path to the corresponding file in the uploads directory. + // map the resource path to the corresponding file in the uploads directory. // :-( PedalboardItem *pedalboardItem = GetPedalboardItemForFileProperty(fileProperty); if (pedalboardItem) { auto pluginInfo = GetPluginInfo(pedalboardItem->uri()); - if (pluginInfo) { - std::filesystem::path resourcePath = fs::path(pluginInfo->bundle_path()) - / fileProperty.resourceDirectory(); - if (IsSubdirectory(relativePath,resourcePath)) + if (pluginInfo) + { + std::filesystem::path resourcePath = fs::path(pluginInfo->bundle_path()) / fileProperty.resourceDirectory(); + if (IsSubdirectory(relativePath, resourcePath)) { - fs::path t = MakeRelativePath(relativePath,resourcePath); - t = fileProperty.directory() / t; - if (fs::exists(t)) + fs::path t = MakeRelativePath(relativePath, resourcePath); + t = fileProperty.directory() / t; + if (fs::exists(t)) { relativePath = t; } @@ -2420,10 +2490,9 @@ std::string PiPedalModel::CopyFilePropertyFile( bool overwrite) { std::lock_guard lock(mutex); - return storage.CopyFilePropertyFile(oldRelativePath, newRelativePath, uiFileProperty,overwrite); + return storage.CopyFilePropertyFile(oldRelativePath, newRelativePath, uiFileProperty, overwrite); } - void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName) { std::lock_guard lock(mutex); @@ -2435,28 +2504,29 @@ std::string PiPedalModel::CreateNewSampleDirectory(const std::string &relativePa std::lock_guard lock(mutex); return storage.CreateNewSampleDirectory(relativePath, uiFileProperty); } -FilePropertyDirectoryTree::ptr PiPedalModel::GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty,const std::string&selectedPath) +FilePropertyDirectoryTree::ptr PiPedalModel::GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty, const std::string &selectedPath) { std::lock_guard lock(mutex); - return storage.GetFilePropertydirectoryTree(uiFileProperty,selectedPath); + return storage.GetFilePropertydirectoryTree(uiFileProperty, selectedPath); } -UiFileProperty::ptr PiPedalModel::FindLoadedPatchProperty(int64_t instanceId,const std::string&patchPropertyUri) +UiFileProperty::ptr PiPedalModel::FindLoadedPatchProperty(int64_t instanceId, const std::string &patchPropertyUri) { auto pedalboardItems = pedalboard.GetAllPlugins(); - for (const auto&pedalboardItem: pedalboardItems) { + for (const auto &pedalboardItem : pedalboardItems) + { if (pedalboardItem->instanceId() == instanceId) { Lv2PluginInfo::ptr pluginInfo = GetPluginInfo(pedalboardItem->uri()); if (pluginInfo && pluginInfo->piPedalUI()) { - for (const auto&fileProperty: pluginInfo->piPedalUI()->fileProperties()) - if (fileProperty->patchProperty() == patchPropertyUri) - { - return fileProperty; - } + for (const auto &fileProperty : pluginInfo->piPedalUI()->fileProperties()) + if (fileProperty->patchProperty() == patchPropertyUri) + { + return fileProperty; + } } } } @@ -2464,9 +2534,9 @@ UiFileProperty::ptr PiPedalModel::FindLoadedPatchProperty(int64_t instanceId,con throw std::runtime_error("Permission denied. Plugin not currently loaded."); } -std::string PiPedalModel::UploadUserFile(const std::string &directory, int64_t instanceId,const std::string &patchProperty, const std::string &filename, std::istream &stream, size_t contentLength) +std::string PiPedalModel::UploadUserFile(const std::string &directory, int64_t instanceId, const std::string &patchProperty, const std::string &filename, std::istream &stream, size_t contentLength) { - UiFileProperty::ptr fileProperty = FindLoadedPatchProperty(instanceId,patchProperty); + UiFileProperty::ptr fileProperty = FindLoadedPatchProperty(instanceId, patchProperty); if (!fileProperty) { Lv2Log::error(SS("Upload fle: Permission denied. No currently-loaded plugin provides that patch property: " << patchProperty)); @@ -2856,35 +2926,40 @@ bool PiPedalModel::GetHasWifi() return hasWifi; } -std::map PiPedalModel::GetWifiRegulatoryDomains() +std::map PiPedalModel::GetWifiRegulatoryDomains() { - std::map result; - try { - auto& regDb = RegDb::GetInstance(); + std::map result; + try + { + auto ®Db = RegDb::GetInstance(); result = regDb.getRegulatoryDomains(storage.GetConfigRoot() / "iso_codes.json"); - } catch (const std::exception&e) + } + catch (const std::exception &e) { Lv2Log::warning(SS("Unable to query Wifi Regulatory domains. " << e.what())); } return result; } -void PiPedalModel::CancelAudioRetry() { +void PiPedalModel::CancelAudioRetry() +{ std::lock_guard lock(mutex); - if (audioRetryPostHandle) + if (audioRetryPostHandle) { // don't think this can ever happen, but if it did, it would be bad. this->CancelPost(audioRetryPostHandle); audioRetryPostHandle = 0; } - } -void PiPedalModel::OnAlsaDriverTerminatedAbnormally() { - // notification from the realtime thread, via the audiohost that the + +void PiPedalModel::OnAlsaDriverTerminatedAbnormally() +{ + // notification from the realtime thread, via the audiohost that the // ALSA stream has broken. We want to restart. // get off the service thread as promptly as possible - this->Post([&]() { + this->Post([&]() + { std::lock_guard lock(mutex); if (closed) return; @@ -2932,8 +3007,7 @@ void PiPedalModel::OnAlsaDriverTerminatedAbnormally() { } else { Lv2Log::error(SS("Unable to reastart audio.")); - } - }); + } }); } bool PiPedalModel::IsInUploadsDirectory(const std::string &path) @@ -2943,26 +3017,43 @@ bool PiPedalModel::IsInUploadsDirectory(const std::string &path) void PiPedalModel::MoveAudioFile( const std::string &directory, - int32_t fromPosition, - int32_t toPosition -) { + int32_t fromPosition, + int32_t toPosition) +{ if (directory.empty()) { throw std::runtime_error("Directory is empty."); } AudioDirectoryInfo::Ptr dir = AudioDirectoryInfo::Create(directory); - dir->MoveAudioFile(directory, fromPosition, toPosition); + dir->MoveAudioFile(directory, fromPosition, toPosition); } void PiPedalModel::SetPedalboardItemTitle(int64_t instanceId, const std::string &title) { std::lock_guard lock(mutex); - if (!this->pedalboard.SetItemTitle(instanceId,title)) + if (!this->pedalboard.SetItemTitle(instanceId, title)) { return; } // no need to reload the pedalboard, but we do need to notify subscribers. - this->SetPresetChanged(-1, true); - this->FirePedalboardChanged(-1,false); - + this->SetPresetChanged(-1, true); + this->FirePedalboardChanged(-1, false); +} + +void PiPedalModel::SetTone3000Auth(const std::string &apiKey) +{ + std::lock_guard lock(mutex); + storage.SetTone3000Auth(apiKey); + + std::vector t{subscribers.begin(), subscribers.end()}; + bool hasAuth = apiKey != ""; + for (auto &subscriber : t) + { + subscriber->OnTone3000AuthChanged(hasAuth); + } + +} +bool PiPedalModel::HasTone3000Auth() const +{ + return storage.GetTone3000Auth() != ""; } diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 5fd9997..d665545 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -94,7 +94,9 @@ namespace pipedal virtual void OnNetworkChanging(bool hotspotConnected) = 0; virtual void OnHasWifiChanged(bool hasWifi) = 0; + virtual void OnAlsaSequencerConfigurationChanged(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0; virtual void Close() = 0; + virtual void OnTone3000AuthChanged(bool value) = 0; }; @@ -168,6 +170,7 @@ namespace pipedal std::vector atomOutputListeners; JackServerSettings jackServerSettings; + PluginHost pluginHost; AtomConverter atomConverter; // must be AFTER pluginHost! @@ -238,6 +241,8 @@ namespace pipedal virtual void OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) override; virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex,int64_t snapshotRequestId) override; virtual void OnAlsaDriverTerminatedAbnormally() override; + virtual void OnAlsaSequencerDeviceAdded(int client, const std::string &clientName) override; + virtual void OnAlsaSequencerDeviceRemoved(int client) override; void OnNotifyPathPatchPropertyReceived( int64_t instanceId, @@ -262,6 +267,7 @@ namespace pipedal void CheckForResourceInitialization(Pedalboard &pedalboard); UiFileProperty::ptr FindLoadedPatchProperty(int64_t instanceId,const std::string&patchPropertyUri); + public: PiPedalModel(); virtual ~PiPedalModel(); @@ -387,6 +393,11 @@ namespace pipedal void SetJackChannelSelection(int64_t clientId, const JackChannelSelection &channelSelection); JackChannelSelection GetJackChannelSelection(); + void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration); + AlsaSequencerConfiguration GetAlsaSequencerConfiguration(); + + std::vector GetAlsaSequencerPorts(); + void SetShowStatusMonitor(bool show); bool GetShowStatusMonitor(); @@ -470,6 +481,10 @@ namespace pipedal int32_t to); void SetPedalboardItemTitle(int64_t instanceId, const std::string &title); + + void SetTone3000Auth(const std::string &apiKey); + bool HasTone3000Auth() const; + }; } // namespace pipedal. \ No newline at end of file diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index c24ab81..4740747 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -1416,6 +1416,10 @@ public: pReader->read(&instanceId); uint64_t result = model.DeleteBank(this->clientId, instanceId); this->Reply(replyTo, "deleteBankItem", result); + } else if (message == "getHasTone3000Auth") + { + bool result = model.HasTone3000Auth(); + this->Reply(replyTo, "getHasTone3000Auth", result); } else if (message == "renameBank") { @@ -1730,6 +1734,22 @@ public: { auto regulatoryDomains = this->model.GetWifiRegulatoryDomains(); this->Reply(replyTo, "getWifiRegulatoryDomains", regulatoryDomains); + } else if (message == "setAlsaSequencerConfiguration") + { + AlsaSequencerConfiguration config; + pReader->read(&config); + this->model.SetAlsaSequencerConfiguration(config); + this->Reply(replyTo, "setAlsaSequencerConfiguration"); + } + else if (message == "getAlsaSequencerConfiguration") + { + AlsaSequencerConfiguration config = this->model.GetAlsaSequencerConfiguration(); + this->Reply(replyTo, "getAlsaSequencerConfiguration", config); + } + else if (message == "getAlsaSequencerPorts") + { + std::vector result = model.GetAlsaSequencerPorts(); + this->Reply(replyTo,"getAlsaSequencerPorts", result); } else { @@ -1831,6 +1851,11 @@ private: Flush(); } + virtual void OnAlsaSequencerConfigurationChanged(const AlsaSequencerConfiguration &alsaSequencerConfiguration) override + { + Send("onAlsaSequencerConfigurationChanged", alsaSequencerConfiguration); + } + virtual void OnNetworkChanging(bool hotspotConnected) override { try @@ -1842,6 +1867,10 @@ private: { } } + virtual void OnTone3000AuthChanged(bool value) + { + Send("onTone3000AuthChanged", value); + } virtual void OnErrorMessage(const std::string &message) { diff --git a/src/PipewireInputStream.cpp b/src/PipewireInputStream.cpp new file mode 100644 index 0000000..b6ff96a --- /dev/null +++ b/src/PipewireInputStream.cpp @@ -0,0 +1,244 @@ +/* + * MIT License + * + * Copyright (c) 2025 Robin E. R. Davies + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "PipewireInputStream.hpp" +#include +#include +extern "C" +{ +#include +#include +#include +} +#include +#include + +using namespace pipedal; + +namespace pipedal::impl +{ + + class PipeWireInputStreamImpl : public PipeWireInputStream + { + private: + pw_main_loop *loop = nullptr; + pw_stream *stream = nullptr; + Callback callback; + + std::atomic is_running_{false}; + + static void on_process(void *userData) + { + PipeWireInputStreamImpl *this_ = (PipeWireInputStreamImpl *)userData; + this_->on_process(); + } + + void on_process() + { + pw_buffer *buf; + + if ((buf = pw_stream_dequeue_buffer(stream)) == NULL) + return; + + // Process buffer data + spa_buffer *sbuf = buf->buffer; + for (uint32_t i = 0; i < sbuf->n_datas; i++) + { + // Access audio data through sbuf->datas[i].data + // Size is sbuf->datas[i].chunk->size + } + + pw_stream_queue_buffer(stream, buf); + } + + static void on_stream_state_changed(void *userData, enum pw_stream_state old, + enum pw_stream_state state, const char *error) + { + PipeWireInputStreamImpl *this_ = (PipeWireInputStreamImpl *)userData; + this_->on_stream_state_changed( + old, state, error); + } + + void on_stream_state_changed(enum pw_stream_state old, + enum pw_stream_state state, const char *error) + { + if (state == PW_STREAM_STATE_ERROR) + { + pw_main_loop_quit(loop); + is_running_ = false; + } + } + + static const struct pw_stream_events stream_events_; + + public: + PipeWireInputStreamImpl(const std::string &stream_name, + uint32_t channels, + uint32_t rate ) + : is_running_(false) + { + + // Initialize PipeWire + pw_init(nullptr, nullptr); + + // Create main loop + loop = pw_main_loop_new(nullptr); + if (!loop) + { + throw std::runtime_error("Failed to create main loop"); + } + + // Create stream + stream = pw_stream_new_simple( + pw_main_loop_get_loop(loop), + stream_name.c_str(), + pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_CLASS, "Audio/Sink", + PW_KEY_NODE_NAME, stream_name.c_str(), + PW_KEY_NODE_DESCRIPTION, stream_name.c_str(), + PW_KEY_APP_NAME, "PiPedal Test", + PW_KEY_APP_PROCESS_BINARY, "pipedaltest", + PW_KEY_MEDIA_ROLE, "DSP", + nullptr), + &stream_events_, + this); + + if (!stream) + { + pw_main_loop_destroy(loop); + loop = nullptr; + throw std::runtime_error("Failed to create stream"); + } + + // Setup audio format + uint8_t buffer[1024]; + spa_pod_builder b = {0}; + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + const spa_pod *params[1]; + spa_audio_info_raw format = { + .format = SPA_AUDIO_FORMAT_S16, + .flags = SPA_AUDIO_FLAG_NONE, + .rate = rate, + .channels = channels}; + if (channels == 1) + { + format.position[0] = SPA_AUDIO_CHANNEL_MONO; // Mono channel + } + else if (channels == 2) + { + format.position[0] = SPA_AUDIO_CHANNEL_FL; // Front Left + format.position[1] = SPA_AUDIO_CHANNEL_FR; // Front Right + } + else if (channels == 4) + { + // 3.1 + format.position[0] = SPA_AUDIO_CHANNEL_FL; + format.position[1] = SPA_AUDIO_CHANNEL_FR; + format.position[2] = SPA_AUDIO_CHANNEL_FC; + format.position[3] = SPA_AUDIO_CHANNEL_LFE; + } + else + { + throw std::runtime_error("Unsupported number of channels: " + std::to_string(channels)); + } + params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &format); + + // Connect stream + int res = pw_stream_connect( + stream, + PW_DIRECTION_INPUT, + PW_ID_ANY, + pw_stream_flags(PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_RT_PROCESS), + params, 1); + + if (res < 0) + { + pw_stream_destroy(stream); + stream = nullptr; + pw_main_loop_destroy(loop); + loop = nullptr; + throw std::runtime_error("Failed to connect stream: " + std::string(strerror(-res))); + } + + // xxx: delete me + pw_main_loop_run(loop); + } + + ~PipeWireInputStreamImpl() + { + if (stream) + { + pw_stream_destroy(stream); + } + if (loop) + { + pw_main_loop_destroy(loop); + } + pw_deinit(); + } + + std::unique_ptr serviceThread; + + + virtual void Activate(Callback &&callback) override + { + serviceThread = std::make_unique([this]() { + if (!is_running_) + { + is_running_ = true; + pw_main_loop_run(loop); + is_running_ = false; + } + }); + } + + virtual void Deactivate() override + { + if (is_running_) + { + pw_main_loop_quit(loop); + } + serviceThread = nullptr; // and join. + } + + bool IsActive() const { return is_running_; } + }; + + const struct pw_stream_events PipeWireInputStreamImpl::stream_events_ = { + .version = PW_VERSION_STREAM_EVENTS, + .state_changed = on_stream_state_changed, + .process = on_process, + }; +} + +using namespace pipedal::impl; + +PipeWireInputStream::ptr PipeWireInputStream::Create(const std::string &streamName, int sampleRate, int channels) +{ + return std::make_shared(streamName, channels, sampleRate); +} \ No newline at end of file diff --git a/src/PipewireInputStream.hpp b/src/PipewireInputStream.hpp new file mode 100644 index 0000000..fe67370 --- /dev/null +++ b/src/PipewireInputStream.hpp @@ -0,0 +1,50 @@ +/* + * MIT License + * + * Copyright (c) 2025 Robin E. R. Davies + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#pragma once + +#include +#include +#include + +namespace pipedal +{ + class PipeWireInputStream + { + protected: + PipeWireInputStream() {} + + public: + virtual ~PipeWireInputStream() {} + + using self = PipeWireInputStream; + using ptr = std::shared_ptr; + + using Callback = std::function; + static ptr Create(const std::string &streamName, int sampleRate, int channels); + + virtual void Activate(Callback &&callback) = 0; + virtual void Deactivate() = 0; + virtual bool IsActive() const = 0; + }; +} \ No newline at end of file diff --git a/src/PipewireInputStreamTest.cpp b/src/PipewireInputStreamTest.cpp new file mode 100644 index 0000000..09cb03c --- /dev/null +++ b/src/PipewireInputStreamTest.cpp @@ -0,0 +1,61 @@ +// Copyright (c) 2022 Robin Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#include "pch.h" +#include "catch.hpp" +#include +#include +#include +#include +#include "PipewireInputStream.hpp" + +#include "PiPedalAlsa.hpp" +#include +#include + +using namespace pipedal; +using namespace std; + + +TEST_CASE("PipeWire Input Stream Test", "[pipewire_input_stream]") +{ + + auto stream = PipeWireInputStream::Create("PiPedalTest Stream", 48000, 2); + REQUIRE(stream != nullptr); + REQUIRE(!stream->IsActive()); + std::atomic frameCount = 0; + std::atomic *pFrameCount = &frameCount; + stream->Activate([pFrameCount] ( + const float*buffer, + size_t size) + { + *pFrameCount += size; + }); + + size_t lastCount = frameCount; + while (true) { + size_t t = frameCount; + if (lastCount != t) { + lastCount = t; + cout << "Frame count: " << lastCount << endl; + } + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } +} + diff --git a/src/Storage.cpp b/src/Storage.cpp index 5d058ca..29872c5 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -22,6 +22,7 @@ #include "Storage.hpp" #include "AudioConfig.hpp" #include "PiPedalException.hpp" +#include "AlsaSequencer.hpp" #include #include #include "json.hpp" @@ -253,6 +254,7 @@ void Storage::Initialize() LoadPluginPresetIndex(); LoadBankIndex(); LoadCurrentBank(); + LoadTone3000Auth(); try { LoadChannelSelection(); @@ -260,6 +262,8 @@ void Storage::Initialize() catch (const std::exception &) { } + LoadAlsaSequencerConfiguration(); + LoadWifiConfigSettings(); LoadWifiDirectConfigSettings(); LoadUserSettings(); @@ -308,10 +312,19 @@ std::filesystem::path Storage::GetCurrentPresetPath() const return this->dataRoot / "currentPreset.json"; } +std::filesystem::path Storage::GetTone3000AuthPath() const +{ + return this->dataRoot / "tone3000.json"; +} + std::filesystem::path Storage::GetChannelSelectionFileName() { return this->dataRoot / "JackChannelSelection.json"; } +std::filesystem::path Storage::GetAlsaSequencerConfigurationFileName() +{ + return this->dataRoot / "MidiDevices.json"; +} std::filesystem::path Storage::GetIndexFileName() const { return this->GetPresetsDirectory() / BANKS_FILENAME; @@ -734,6 +747,103 @@ JackChannelSelection Storage::GetJackChannelSelection(const JackConfiguration &j return jackChannelSelection.RemoveInvalidChannels(jackConfiguration); } +static AlsaSequencerConfiguration MigrateRawMidiToAlsaSequencer(std::vector &selectedDevices) +{ + AlsaSequencerConfiguration result; + try + { + auto sequencerPorts = AlsaSequencer::EnumeratePorts(); + + // Prepare Migrate raw MIDI devices to ALSA sequencer ports. + + for (auto i = selectedDevices.begin(); i != selectedDevices.end(); ++i) + { + if (i->name_.starts_with("hw:")) + { + for (const auto &port : sequencerPorts) + { + if (i->name_ == port.rawMidiDevice) + { + result.connections().push_back( + AlsaSequencerPortSelection(port.id, port.name, port.displaySortOrder)); + break; + } + } + } + } + } + catch (const std::exception &e) + { + Lv2Log::error(SS("Failed to migrate MIDI settings. " << e.what())); + // ick. + } + return result; +} +void Storage::LoadAlsaSequencerConfiguration() +{ + auto fileName = this->GetAlsaSequencerConfigurationFileName(); + + if (std::filesystem::exists(fileName)) + { + try + { + std::ifstream s(fileName); + json_reader reader(s); + AlsaSequencerConfiguration result; + reader.read(&result); + this->alsaSequencerConfiguration = result; + } + catch (const std::exception &e) + { + Lv2Log::error("I/O error reading %s: %s", fileName.c_str(), e.what()); + } + } + else + { + + // migrate legacy settings from JackConfiguration? + if (this->isJackChannelSelectionValid) + { + this->alsaSequencerConfiguration = + MigrateRawMidiToAlsaSequencer( + this->jackChannelSelection.LegacyGetInputMidiDevices()); + this->jackChannelSelection.LegacyGetInputMidiDevices().clear(); // let them be clear, the next it gets updated. + } + else + { + // no legacy settings, so just create a default configuration. + this->alsaSequencerConfiguration = AlsaSequencerConfiguration(); + } + SaveAlsaSequencerConfiguration(); + } +} +void Storage::SaveAlsaSequencerConfiguration() +{ + auto fileName = this->GetAlsaSequencerConfigurationFileName(); + + try + { + pipedal::ofstream_synced s(fileName); + json_writer writer(s); + writer.write(this->alsaSequencerConfiguration); + } + catch (const std::exception &e) + { + Lv2Log::error("I/O error writing %s: %s", fileName.c_str(), e.what()); + } +} + +void Storage::SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) +{ + this->alsaSequencerConfiguration = alsaSequencerConfiguration; + SaveAlsaSequencerConfiguration(); +} + +AlsaSequencerConfiguration Storage::GetAlsaSequencerConfiguration() const +{ + return this->alsaSequencerConfiguration; +} + void Storage::LoadChannelSelection() { auto fileName = this->GetChannelSelectionFileName(); @@ -749,7 +859,6 @@ void Storage::LoadChannelSelection() catch (const std::exception &e) { Lv2Log::error("I/O error reading %s: %s", fileName.c_str(), e.what()); - throw PiPedalStateException("Unexpected error reading Jack settings file."); } } } @@ -758,6 +867,8 @@ void Storage::SaveChannelSelection() auto fileName = this->GetChannelSelectionFileName(); try { + // replaced with AlsaSequencerConfiguration. Delete legacy data. + this->jackChannelSelection.LegacyGetInputMidiDevices().clear(); pipedal::ofstream_synced s(fileName); json_writer writer(s, false); writer.write(this->jackChannelSelection); @@ -1745,12 +1856,14 @@ static void AddTracksToResult( } const auto &path = dir_entry.path(); auto name = path.filename().string(); - try { + try + { if (dir_entry.is_directory()) { resultFiles.push_back(FileEntry{path, name, true, dir_entry.is_symlink()}); } - } catch (const std::exception &e) + } + catch (const std::exception &e) { Lv2Log::warning(SS("Failed to add directory entry: " << path.string() << " - " << e.what())); } @@ -1799,7 +1912,6 @@ static void AddTracksToResult( throw std::logic_error( SS("AddTracksToResult failed to enumerate audio files. " << rootPath.string() << " - " << error.what())); } - } catch (const std::exception &error) { @@ -2498,6 +2610,47 @@ const PluginPresetIndex &Storage::GetPluginPresetIndex() return pluginPresetIndex; } +void Storage::LoadTone3000Auth() +{ + fs::path path = GetTone3000AuthPath(); + try + { + if (!fs::exists(path)) + { + this->tone3000Auth = ""; + return; + } + std::ifstream s(path); + json_reader reader(s); + reader.read(&(this->tone3000Auth)); + } + catch (const std::exception &e) + { + Lv2Log::error("Failed to load tone3000Auth: %s", e.what()); + } +} + +void Storage::SetTone3000Auth(const std::string &apiKey) +{ + if (tone3000Auth != apiKey) + { + tone3000Auth = apiKey; + + pipedal::ofstream_synced os(this->GetTone3000AuthPath()); + if (!os.is_open()) + { + Lv2Log::error("Failed to open Tone3000 auth file for writing."); + return; + } + json_writer writer(os); + writer.write(apiKey); + } +} +std::string Storage::GetTone3000Auth() const +{ + return tone3000Auth; +} + JSON_MAP_BEGIN(UserSettings) JSON_MAP_REFERENCE(UserSettings, governor) JSON_MAP_REFERENCE(UserSettings, showStatusMonitor) diff --git a/src/Storage.hpp b/src/Storage.hpp index 440ca17..6d45d63 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -31,6 +31,7 @@ #include "FileEntry.hpp" #include #include "FilePropertyDirectoryTree.hpp" +#include "AlsaSequencer.hpp" namespace pipedal { @@ -72,6 +73,7 @@ private: BankIndex bankIndex; BankFile currentBank; PluginPresetIndex pluginPresetIndex; + std::string tone3000Auth; private: void FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std::filesystem::path&directory) const; @@ -84,7 +86,9 @@ private: std::filesystem::path GetIndexFileName() const; std::filesystem::path GetBankFileName(const std::string & name) const; std::filesystem::path GetChannelSelectionFileName(); + std::filesystem::path GetAlsaSequencerConfigurationFileName(); std::filesystem::path GetCurrentPresetPath() const; + std::filesystem::path GetTone3000AuthPath() const; void LoadBankIndex(); void SaveBankIndex(); @@ -94,11 +98,19 @@ private: void LoadChannelSelection(); void SaveChannelSelection(); + + void LoadAlsaSequencerConfiguration(); + void SaveAlsaSequencerConfiguration(); + + void SaveBankFile(const std::string& name,const BankFile&bankFile); void LoadBankFile(const std::string &name,BankFile *pBank); std::string GetPresetCopyName(const std::string &name); bool isJackChannelSelectionValid = false; JackChannelSelection jackChannelSelection; + + AlsaSequencerConfiguration alsaSequencerConfiguration;; + WifiConfigSettings wifiConfigSettings; WifiDirectConfigSettings wifiDirectConfigSettings; @@ -126,6 +138,7 @@ public: void LoadWifiDirectConfigSettings(); void LoadUserSettings(); void SaveUserSettings(); + void LoadBank(int64_t instanceId); int64_t GetBankByMidiBankNumber(uint8_t bankNumber); const Pedalboard& GetCurrentPreset(); @@ -181,6 +194,10 @@ public: void SaveCurrentPreset(const CurrentPreset ¤tPreset); bool RestoreCurrentPreset(CurrentPreset*pResult); + void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration); + AlsaSequencerConfiguration GetAlsaSequencerConfiguration() const; + + //std::string MapPropertyFileName(Lv2PluginInfo*pluginInfo, const std::string&path); private: @@ -239,6 +256,10 @@ public: const UiFileProperty&uiFileProperty, bool overwrite = false); FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty,const std::filesystem::path&selectedPath); + + void LoadTone3000Auth(); + void SetTone3000Auth(const std::string&apiKey); + std::string GetTone3000Auth() const; }; diff --git a/src/WebServerConfig.cpp b/src/WebServerConfig.cpp index 657529a..ae23dc9 100644 --- a/src/WebServerConfig.cpp +++ b/src/WebServerConfig.cpp @@ -188,6 +188,10 @@ public: { return true; } + else if (segment == "Tone3000Auth") + { + return true; + } else if (segment == "PluginPresets") { return true; @@ -734,7 +738,17 @@ public: { std::string segment = request_uri.segment(1); - if (segment == "uploadPluginPresets") + if (segment == "Tone3000Auth") { + // https://www.tone3000.com/api/v1/auth?redirect_url=http://10.0.0.151:8080/var/Tone3000Auth&otp_only=true + std::string apiKey = request_uri.query("api_key"); + + model->SetTone3000Auth(apiKey); + + res.set(HttpField::content_type, "application/json"); + res.set(HttpField::cache_control, "no-cache"); + + res.setBody("\"OK\""); + } else if (segment == "uploadPluginPresets") { PluginPresets presets; fs::path filePath = req.get_body_temporary_file(); diff --git a/src/main.cpp b/src/main.cpp index 68e02c3..12ead14 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -48,8 +48,9 @@ #include "AudioFiles.hpp" #include +#include "AlsaSequencer.hpp" -using namespace pipedal; +using namespace pipedal; #ifdef __ARM_ARCH_ISA_A64 #define AARCH64 @@ -81,9 +82,9 @@ static bool isJackServiceRunning() return std::filesystem::exists(path); } - #if ENABLE_BACKTRACE -void segvHandler(int sig) { +void segvHandler(int sig) +{ void *array[10]; // Get void*'s for all entries on the stack @@ -92,26 +93,40 @@ void segvHandler(int sig) { // Print out all the frames to stderr const char *message = "Error: SEGV signal received.\n"; - auto _ = write(STDERR_FILENO,message,strlen(message)); + auto _ = write(STDERR_FILENO, message, strlen(message)); - backtrace_symbols_fd(array+2, size-2, STDERR_FILENO); + backtrace_symbols_fd(array + 2, size - 2, STDERR_FILENO); _exit(EXIT_FAILURE); } - static void EnableBacktrace() { - signal(SIGSEGV, segvHandler); - + signal(SIGSEGV, segvHandler); } #endif -static bool TryGetLogLevel(const std::string&strLogLevel,LogLevel*result) +static bool TryGetLogLevel(const std::string &strLogLevel, LogLevel *result) { - if (strLogLevel == "debug") {*result= LogLevel::Debug; return true;} - if (strLogLevel == "info") {*result= LogLevel::Info; return true;} - if (strLogLevel == "warning") {*result= LogLevel::Warning; return true;} - if (strLogLevel == "error") { *result= LogLevel::Error; return true;} + if (strLogLevel == "debug") + { + *result = LogLevel::Debug; + return true; + } + if (strLogLevel == "info") + { + *result = LogLevel::Info; + return true; + } + if (strLogLevel == "warning") + { + *result = LogLevel::Warning; + return true; + } + if (strLogLevel == "error") + { + *result = LogLevel::Error; + return true; + } *result = LogLevel::Info; return false; } @@ -140,7 +155,7 @@ int main(int argc, char *argv[]) parser.AddOption("-h", &help); parser.AddOption("--help", &help); parser.AddOption("-systemd", &systemd); - parser.AddOption("-log-level",&logLevel); + parser.AddOption("-log-level", &logLevel); parser.AddOption("-port", &portOption); parser.AddOption("-test-extra-device", &testExtraDevice); // advertise two different devices (for testing multi-device connect) @@ -220,11 +235,12 @@ int main(int argc, char *argv[]) if (logLevel.length() != 0) { LogLevel lv2LogLevel; - if (TryGetLogLevel(logLevel,&lv2LogLevel)) + if (TryGetLogLevel(logLevel, &lv2LogLevel)) { Lv2Log::log_level(lv2LogLevel); - - } else { + } + else + { Lv2Log::error(SS("Invalid log level: " << logLevel)); return EXIT_SUCCESS; // indicate to systemd that we don't want a restart. } @@ -237,9 +253,10 @@ int main(int argc, char *argv[]) // clean up orphaned temporary files. const std::filesystem::path webTempDirectory = "/var/pipedal/web_temp"; - if (!webTempDirectory.empty()) { + if (!webTempDirectory.empty()) + { std::filesystem::remove_all(webTempDirectory); //// user must belong to the pipedald grop when debugging. - std::filesystem::create_directories(webTempDirectory); + std::filesystem::create_directories(webTempDirectory); } // configure AudiDirectoryInfo to use the correct directories. @@ -273,6 +290,7 @@ int main(int argc, char *argv[]) return EXIT_SUCCESS; // indiate to systemd that we don't want a restart. } + try { { diff --git a/todo.txt b/todo.txt index fd1dddc..940fc5d 100644 --- a/todo.txt +++ b/todo.txt @@ -1,3 +1,11 @@ +Exctly one underrun per seek in Toob Player + +Test autohotspot detection. + + + +check filetime conversion in gcc 12.2! (missing c++ 20 time conversion functions) + AudioFiles.cpp fileTimeToInt64 diff --git a/vite/src/pipedal/AlsaSequencer.tsx b/vite/src/pipedal/AlsaSequencer.tsx new file mode 100644 index 0000000..a49aff0 --- /dev/null +++ b/vite/src/pipedal/AlsaSequencer.tsx @@ -0,0 +1,53 @@ +// Copyright (c) 2025 Robin E. R. Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +export class AlsaSequencerPortSelection { + deserialize(json: any) { + this.id = json.id; + this.name = json.name; + this.sortOrder = json.sortOrder; + return this; + }; + static deserialize_array(input: any): AlsaSequencerPortSelection[] { + let result: AlsaSequencerPortSelection[] = []; + for (let i = 0; i < input.length; ++i) { + result[i] = new AlsaSequencerPortSelection().deserialize(input[i]); + } + return result; + } + id: string = ""; + name: string = ""; + sortOrder: number = 0; +}; + +export class AlsaSequencerConfiguration { + deserialize(input: any) { + this.connections = AlsaSequencerPortSelection.deserialize_array(input.connections); + return this; + } + deserialize_array(input: any): AlsaSequencerConfiguration[] { + let result: AlsaSequencerConfiguration[] = []; + for (let i = 0; i < input.length; ++i) { + result[i] = new AlsaSequencerConfiguration().deserialize(input[i]); + } + return result; + } + + connections: AlsaSequencerPortSelection[] = []; +}; diff --git a/vite/src/pipedal/App.tsx b/vite/src/pipedal/App.tsx index d1ab4dd..e2dca51 100644 --- a/vite/src/pipedal/App.tsx +++ b/vite/src/pipedal/App.tsx @@ -19,11 +19,13 @@ import React from 'react'; import { ThemeProvider, createTheme, StyledEngineProvider } from '@mui/material/styles'; -import { CssBaseline } from '@mui/material'; +import CssBaseline from '@mui/material/CssBaseline'; import VirtualKeyboardHandler from './VirtualKeyboardHandler'; import AppThemed from "./AppThemed"; import { isDarkMode } from './DarkMode'; +import Tone3000AuthComplete from './Tone3000AuthComplete'; + declare module '@mui/material/styles' { interface Theme { @@ -221,6 +223,12 @@ type AppThemeProps = { }; +function isTone3000Auth() { + let url = new URL(window.location.href); + let param = url.searchParams.get("api_key"); + return (param !== null && param !== "") +} + const App = (class extends React.Component { // Before the component mounts, we initialise our state @@ -240,8 +248,11 @@ const App = (class extends React.Component { - - + {isTone3000Auth() ? ( + + ) : ( + + )} ); diff --git a/vite/src/pipedal/FilePropertyDialog.tsx b/vite/src/pipedal/FilePropertyDialog.tsx index b85106d..b927a33 100644 --- a/vite/src/pipedal/FilePropertyDialog.tsx +++ b/vite/src/pipedal/FilePropertyDialog.tsx @@ -21,6 +21,10 @@ import React from 'react'; import { createStyles } from './WithStyles'; +// import Tone3000Dialog from './Tone3000Dialog'; +import Tone3000HelpDialog from './Tone3000HelpDialog'; +import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; +import Link from '@mui/material/Link'; import DraggableButtonBase from './DraggableButtonBase'; import CloseIcon from '@mui/icons-material/Close'; import { Theme } from '@mui/material/styles'; @@ -64,6 +68,8 @@ import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDial import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata'; +const ToobNamModelFileUrl = "http://two-play.com/plugins/toob-nam#modelFile"; + const AUTOSCROLL_TICK_DELAY = 30; const AUTOSCROLL_THRESHOLD = 48; const AUTOSCROLL_SCROLL_PER_SECOND = 500; @@ -161,6 +167,8 @@ export interface FilePropertyDialogState { initialSelection: string; multiSelect: boolean, selectedFiles: string[], + //openTone3000Dialog: boolean, + openTone3000Help: boolean }; @@ -231,7 +239,9 @@ export default withStyles( copyDialogOpen: false, initialSelection: this.props.selectedFile, multiSelect: false, - selectedFiles: [] + selectedFiles: [], + //openTone3000Dialog: false, + openTone3000Help: false }; this.requestScroll = true; } @@ -362,7 +372,7 @@ export default withStyles( dragElementDy: 0, from: index, to: index, - lastMousePoint: {...this.longPressStartPoint} + lastMousePoint: { ...this.longPressStartPoint } }; this.setState({ dragState: dragState }); } @@ -882,7 +892,6 @@ export default withStyles( style={{ marginLeft: 16, marginBottom: 8, marginRight: 8, textOverflow: "", display: "flex", flexFlow: "row wrap", alignItems: "center", justifyContent: "start" }}> {breadcrumbs} - ); } @@ -998,8 +1007,7 @@ export default withStyles( let dragElementDy = dragState.lastMousePoint.y - this.longPressStartPoint.y; let originalY = dragState.from * dragState.height; let newY = originalY + dragElementDy; - if (newY < 0) - { + if (newY < 0) { newY = 0; dragElementDy = -originalY; } @@ -1040,9 +1048,26 @@ export default withStyles( () => { this.handleAutoScrollTick() }, AUTOSCROLL_TICK_DELAY); } + // handleTone3000Dialog(e: React.MouseEvent) { + // e.stopPropagation(); + // e.preventDefault(); + + // this.setState({ openTone3000Dialog: true }); + + // } + + + handleTone3000Help(e: React.MouseEvent) { + e.stopPropagation(); + e.preventDefault(); + + this.setState({ openTone3000Help: true }); + + } render() { const isTracksDirectory = this.isTracksDirectory(); + const isToobNamModelFile = this.props.fileProperty.patchProperty === ToobNamModelFileUrl; const classes = withStyles.getClasses(this.props); let columnWidth = this.state.columnWidth; @@ -1102,7 +1127,7 @@ export default withStyles( background: this.state.reordering || this.state.multiSelect ? (isDarkMode() ? "#523" : "#EDF") : undefined, - marginBottom: 16, paddingTop: 0 + marginBottom: 8, paddingTop: 0 }} > {this.state.reordering && ( @@ -1257,7 +1282,7 @@ export default withStyles( - { this.listContainerElementRef = element; this.onMeasureRef(element); }} style={{ flex: "1 1 100%", display: "flex", flexFlow: "row wrap", - position: "relative", justifyContent: "flex-start", alignContent: "flex-start", paddingLeft: 16, paddingBottom: 16 + position: "relative", justifyContent: "flex-start", alignContent: "flex-start", + paddingLeft: 16, paddingBottom: 16, paddingTop: 16, }}> { (this.state.columns !== 0) && // don't render until we have number of columns derived from layout. @@ -1461,45 +1487,64 @@ export default withStyles( {(!this.state.reordering && !this.state.multiSelect) && ( <> - {this.state.windowWidth > 500 ? ( -
- this.handleDelete()} > - - +
+ {isToobNamModelFile && ( +
+ + Download model files from TONE3000 + + { this.handleTone3000Help(e); }} aria-label="help" edge="end" color="inherit" style={{ opacity: 0.6, marginLeft: 8 }} + > + + +
- } - onClick={() => { this.setState({ openUploadFileDialog: true }) }} disabled={protectedDirectory} - > -
Upload
-
+ )} - } - onClick={() => { this.handleDownloadFile(); }} disabled={protectedDirectory || !this.state.hasFileSelection} - > -
Download
-
+
+ this.handleDelete()} > + + -
 
+ } + onClick={() => { this.setState({ openUploadFileDialog: true }) }} disabled={protectedDirectory} + > +
Upload
+
- - +
 
+ + + +
) : (
@@ -1607,7 +1652,7 @@ export default withStyles( { this.state.renameDialogOpen && ( { this.setState({ renameDialogOpen: false }); this.onExecuteRename(newName) }} onClose={() => { this.setState({ renameDialogOpen: false }); }} acceptActionName="OK" @@ -1645,6 +1690,18 @@ export default withStyles( ) ) } + {/* {this.state.openTone3000Dialog && ( + this.setState({ openTone3000Dialog: false })} + /> + )} */} + {this.state.openTone3000Help && ( + this.setState({ openTone3000Help: false })} + /> + )} ); } diff --git a/vite/src/pipedal/LoopDialog.tsx b/vite/src/pipedal/LoopDialog.tsx index 698a404..c32e5ba 100644 --- a/vite/src/pipedal/LoopDialog.tsx +++ b/vite/src/pipedal/LoopDialog.tsx @@ -36,7 +36,7 @@ import Slider, { SliderProps } from "@mui/material/Slider"; import Checkbox from "@mui/material/Checkbox"; import TimebaseSelectorDialog from "./TimebaseselectorDialog"; import Timebase, { TimebaseUnits } from "./Timebase"; -import { useWindowHeight } from "@react-hook/window-size"; +import useWindowSize from "./UseWindowSize"; import Button from "@mui/material/Button"; import PlayArrow from "@mui/icons-material/PlayArrow"; import StopIcon from '@mui/icons-material/Stop'; @@ -405,8 +405,8 @@ export default function LoopDialog(props: LoopDialogProps) { const [loopEnd, setLoopEnd] = React.useState(props.value.loopEnd); - const height = useWindowHeight(); - const fullScreen = height < 500; + const [windowSize] = useWindowSize(); + const fullScreen = windowSize.height < 500; function cancelPlaying() { props.onCancelPlaying(); @@ -444,7 +444,7 @@ export default function LoopDialog(props: LoopDialogProps) { open={props.isOpen} onEnterKey={() => { }} - fullScreen={height < 500} + fullScreen={fullScreen} sx={{ "& .MuiDialog-container": { "& .MuiPaper-root": { diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index c00728b..c9c2e5a 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -43,6 +43,7 @@ import { ColorTheme, getColorScheme, setColorScheme } from './DarkMode'; import FilePropertyDirectoryTree from './FilePropertyDirectoryTree'; import AudioFileMetadata from './AudioFileMetadata'; import { pathFileName } from './FileUtils'; +import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSequencer'; export enum State { @@ -419,6 +420,8 @@ export class PiPedalModel //implements PiPedalModel webSocket?: PiPedalSocket; + hasTone3000Auth: ObservableProperty = new ObservableProperty(false); + hasWifiDevice: ObservableProperty = new ObservableProperty(false); onSnapshotModified: ObservableEvent = new ObservableEvent(); @@ -452,6 +455,7 @@ export class PiPedalModel //implements PiPedalModel favorites: ObservableProperty = new ObservableProperty({}); + alsaSequencerConfiguration : ObservableProperty = new ObservableProperty(new AlsaSequencerConfiguration()); presets: ObservableProperty = new ObservableProperty ( @@ -668,6 +672,9 @@ export class PiPedalModel //implements PiPedalModel this.handlePluginPresetsChanged(pluginUri); } else if (message === "onJackConfigurationChanged") { this.jackConfiguration.set(new JackConfiguration().deserialize(body)); + } else if (message === "onAlsaSequencerConfigurationChanged") { + this.alsaSequencerConfiguration.set(new AlsaSequencerConfiguration().deserialize(body)); + } else if (message === "onLoadPluginPreset") { let instanceId = body.instanceId as number; let controlValues = ControlValue.deserializeArray(body.controlValues); @@ -755,6 +762,8 @@ export class PiPedalModel //implements PiPedalModel } else if (message === "onErrorMessage") { this.showAlert(body as string); + } else if (message == "onTone3000AuthChanged") { + this.hasTone3000Auth.set(body as boolean); } else if (message === "onLv2PluginsChanging") { this.onLv2PluginsChanging(); @@ -1134,6 +1143,12 @@ export class PiPedalModel //implements PiPedalModel this.jackSettings.set(new JackChannelSelection().deserialize( await this.getWebSocket().request("getJackSettings") )); + this.alsaSequencerConfiguration.set(new AlsaSequencerConfiguration().deserialize( + await this.getWebSocket().request("getAlsaSequencerConfiguration") + )); + this.hasTone3000Auth.set( + await this.getWebSocket().request("getHasTone3000Auth") + ); this.banks.set(new BankIndex().deserialize(await this.getWebSocket().request("getBankIndex"))); this.favorites.set(await this.getWebSocket().request("getFavorites")); @@ -3193,6 +3208,25 @@ export class PiPedalModel //implements PiPedalModel // notify the server. this.webSocket?.send("setPedalboardItemTitle", { instanceId: instanceId, title: title }); } + setAlsaSequencerConfiguration(alsaSequencerConfiguration: AlsaSequencerConfiguration): void { + this.webSocket?.send("setAlsaSequencerConfiguration", alsaSequencerConfiguration); + } + async getAlsaSequencerConfiguration(): Promise { + if (this.webSocket) + { + let result = await this.webSocket.request("getAlsaSequencerConfiguration"); + return new AlsaSequencerConfiguration().deserialize(result); + } + throw new Error("No connection."); + } + async getAlsaSequencerPorts(): Promise { + if (this.webSocket) { + let result = await this.webSocket.request("getAlsaSequencerPorts"); + return AlsaSequencerPortSelection.deserialize_array(result); + } + throw new Error("No connection."); + } + }; let instance: PiPedalModel | undefined = undefined; diff --git a/vite/src/pipedal/SelectMidiChannelsDialog.tsx b/vite/src/pipedal/SelectMidiChannelsDialog.tsx index db6647d..4ca546e 100644 --- a/vite/src/pipedal/SelectMidiChannelsDialog.tsx +++ b/vite/src/pipedal/SelectMidiChannelsDialog.tsx @@ -17,105 +17,196 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +import React from 'react'; import { useState } from 'react'; import Button from '@mui/material/Button'; import List from '@mui/material/List'; import ListItemButton from '@mui/material/ListItemButton'; import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; import DialogActions from '@mui/material/DialogActions'; import FormControlLabel from '@mui/material/FormControlLabel'; import Typography from '@mui/material/Typography'; import Checkbox from '@mui/material/Checkbox'; -import {AlsaMidiDeviceInfo} from './AlsaMidiDeviceInfo'; +import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSequencer'; import DialogEx from './DialogEx'; - +import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; export interface SelectMidiChannelsDialogProps { open: boolean; - selectedChannels: AlsaMidiDeviceInfo[]; - availableChannels: AlsaMidiDeviceInfo[]; - onClose: (selectedChannels: AlsaMidiDeviceInfo[] | null) => void; + onClose: () => void; } -function isChecked(selectedChannels: AlsaMidiDeviceInfo[], channel: AlsaMidiDeviceInfo): boolean { - for (let i = 0; i < selectedChannels.length; ++i) { - if (selectedChannels[i].equals(channel)) return true; - } - return false; -} -function addPort(availableChannels: AlsaMidiDeviceInfo[], selectedChannels: AlsaMidiDeviceInfo[], newChannel: AlsaMidiDeviceInfo) -:AlsaMidiDeviceInfo[] - { - let result: AlsaMidiDeviceInfo[] = []; - for (let i = 0; i < availableChannels.length; ++i) { - let channel = availableChannels[i]; - if (isChecked(selectedChannels, channel) || channel.equals(newChannel)) { - result.push(channel); - } - } - return result; -} - -function removePort(selectedChannels: AlsaMidiDeviceInfo[], channel: AlsaMidiDeviceInfo) -:AlsaMidiDeviceInfo[] -{ - let result: AlsaMidiDeviceInfo[] = []; - for (let i = 0; i < selectedChannels.length; ++i) { - if (!selectedChannels[i].equals(channel)) { - result.push(selectedChannels[i]); - } - } - return result; -} +interface DialogItem { + id: string; + name: string; + sortOrder: number + offline: boolean; +}; function SelectMidiChannelsDialog(props: SelectMidiChannelsDialogProps) { //const classes = useStyles(); - const { onClose, selectedChannels, availableChannels, open } = props; - const [currentSelection, setCurrentSelection] = useState(selectedChannels); + const { open, onClose } = props; + const [availablePorts, setAvailablePorts] = useState(null); + const [configuration, setConfiguration] = useState(null); + const [allPorts, setAllPorts] = useState(null); + const [model] = useState(PiPedalModelFactory.getInstance()); + const [changed, setChanged] = useState(false); + const [ readyToDisplay, setReadyToDisplay ] = useState(false); - - - let toggleSelect = (value: AlsaMidiDeviceInfo) => { - if (!isChecked(currentSelection, value)) { - setCurrentSelection(addPort(availableChannels, currentSelection, value)); + React.useEffect(() => { + if (open) { + setReadyToDisplay(false); + model.getAlsaSequencerPorts().then((ports) => { + setAvailablePorts(ports); + }).catch((error) => { + model.showAlert(error); + setReadyToDisplay(true); + setAvailablePorts(null); + }); + model.getAlsaSequencerConfiguration().then((config) => { + setConfiguration(config); + }).catch((error) => { + model.showAlert(error); + setReadyToDisplay(true); + setConfiguration(null); + }); + return () => { + } } else { - setCurrentSelection(removePort(currentSelection, value)); + return () => { }; } + }, [open]); + React.useEffect(() => { + if (availablePorts !== null && configuration !== null) { + let result: DialogItem[] = []; + setReadyToDisplay(true); + for (let port of availablePorts) { + result.push({ + id: port.id, + name: port.name, + sortOrder: port.sortOrder, + offline: false + }); + } + + // include ports that have been previously selected but are not in the current list of available ports + for (let port of configuration.connections) { + if (!availablePorts.some((p) => p.id === port.id)) { + result.push( + { + id: port.id, + name: port.name, + sortOrder: port.sortOrder, + offline: true + } + ); + } + } + result.sort((a, b) => { + return a.sortOrder - b.sortOrder; + }); + setAllPorts(result); + } else { + setAllPorts(null); + } + + }, [availablePorts, configuration]); + + const isChecked = (value: DialogItem) => { + if (availablePorts === null || configuration === null) { + return false; + } + return configuration.connections.some((port) => port.id === value.id); + }; + const setChecked = (value_: DialogItem, checked: boolean) => { + if (availablePorts === null || configuration === null) { + return; + } + let value = new AlsaSequencerPortSelection(); + value.id = value_.id; + value.name = value_.name; + value.sortOrder = value_.sortOrder; + let newConnections = configuration.connections.slice(); + if (checked) { + newConnections.push(value); + } else { + newConnections = newConnections.filter((port) => port.id !== value.id); + } + let newConfiguration = new AlsaSequencerConfiguration(); + newConfiguration.connections = newConnections; + setConfiguration(newConfiguration); + }; + let toggleSelect = (value: DialogItem) => { + if (availablePorts === null || configuration === null) { + return; + } + if (!isChecked(value)) { + setChecked(value, true); + } else { + setChecked(value, false); + } + setChanged(true); }; const handleClose = (): void => { - onClose(null); + onClose(); }; const handleOk = (): void => { - onClose(currentSelection); + if (changed && configuration !== null) { + model.setAlsaSequencerConfiguration(configuration); + } + onClose(); }; return ( - - Select MIDI Device - - {availableChannels.map((channel) => ( - - toggleSelect(channel)} key={channel.name} /> - } - label={channel.description} - /> - - ) + Select MIDI Inputs + + + {allPorts !== null && allPorts.length === 0 && ( + + No MIDI devices found. + )} + {allPorts != null && allPorts.map((port) => ( + + toggleSelect(port)} /> + } + label={ + ( +
+ + {port.name} + + {port.offline && ( + + (offline) + + )} +
+ )} + /> +
+ ) - )} - {availableChannels.length === 0 && ( - - No MIDI devices found. - )} -
+ )} +
+ + + { + openAuthDialog && ( + + + + + { onClose(); }} + > + + + + + TONE3000 Authorization + + + + + +
+
+
+ + The TONE3000 website provides an + online library of models for use with TooB Neural Amp Modeller. You can download + models from the TONE3000 website onto your local machine and then upload them to + PiPedal; or, more conveniently, you can browse the TONE3000 model database directly, and download models directly + to the PiPedal server from the TONE3000 model database in a single step. + + + In order to access the TONE3000 database, you must first obtain an access token from the + TONE3000 website. Clicking on the button will take you to an external website in + order to complete the authorization process. + + + + + Privacy statement: PiPedal will only have access to your authorization token + which does not contain personally identifying information, and which is stored locally on + your PiPedal server. Please refer to + the TONE3000 privacy policy for + information on how your data is used by TONE3000. + +
+
+
+ + + +
+ )} +
+ + ); +} + +export default Tone3000Dialog; \ No newline at end of file diff --git a/vite/src/pipedal/Tone3000HelpDialog.tsx b/vite/src/pipedal/Tone3000HelpDialog.tsx new file mode 100644 index 0000000..6d6c477 --- /dev/null +++ b/vite/src/pipedal/Tone3000HelpDialog.tsx @@ -0,0 +1,92 @@ +import DialogEx from "./DialogEx"; +import DialogTitle from "@mui/material/DialogTitle"; +import DialogContent from "@mui/material/DialogContent"; +import Toolbar from "@mui/material/Toolbar"; +import IconButtonEx from "./IconButtonEx"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import Typography from "@mui/material/Typography"; +import Link from "@mui/material/Link"; + + + +function Tone3000HelpDialog(props: { + open: boolean; + onClose: () => void; +}) { + const { open, onClose } = props; + return ( + { onClose(); }} + onClose={() => { onClose(); }} + open={open}> + + + + { onClose(); }} + > + + + + + TONE3000 Help + + + + + +
+
+
+ + The TONE3000 website provides a + massive collection of neural amp models that can be used by TooB Neural Amp Modeler. + + + Using TONE3000 model files in PiPedal is a two step process. First, download model files from + the website using your browser; and then upload + the files in your browser's Downloads directory to the PiPedal server using + the Upload button. PiPedal + automatically extracts bundles of model files from zip archives, so manual extraction is unnecessary. + + + TONE3000 is a community-driven platform where community members share Neural Amp Modeler profiles. + The site showcases the collaborative spirit of the guitar modeling community. And all of this is made + possible by Steven Atkins' Neural Amp Modeler library, which forms the foundation and core of TooB Neural Amp Modeler and other NAM-based plugins. + + + If you would like to profile your own amplifiers, and effects, the TONE3000 website allows you to + generate your own NAM profiles for free. Refer to + the TONE3000 website for more details. + + + When you click on the TONE3000 link, you will be taken to an external website. The TONE3000 + website is not part of PiPedal, and is not affiliated in any way with PiPedal. + + + + Privacy statement: PiPedal has no access to personal data used by the TONE3000 website. Please refer to + the TONE3000 privacy policy for + information on how your data is used by TONE3000. + +
+
+
+ + + + + ); +} +export default Tone3000HelpDialog; \ No newline at end of file diff --git a/vite/src/pipedal/ToobPlayerControl.tsx b/vite/src/pipedal/ToobPlayerControl.tsx index f97af4c..ac80320 100644 --- a/vite/src/pipedal/ToobPlayerControl.tsx +++ b/vite/src/pipedal/ToobPlayerControl.tsx @@ -38,7 +38,7 @@ import ButtonBase from '@mui/material/ButtonBase'; import FilePropertyDialog from './FilePropertyDialog'; import JsonAtom from './JsonAtom'; import { UiFileProperty } from './Lv2Plugin'; -import { Divider } from '@mui/material'; +import Divider from '@mui/material/Divider'; import useWindowSize from './UseWindowSize'; import { getAlbumArtUri } from './AudioFileMetadata'; import RepeatIcon from '@mui/icons-material/Repeat'; diff --git a/vite/src/pipedal/ToolTipEx.tsx b/vite/src/pipedal/ToolTipEx.tsx index a7a3dd7..95590d1 100644 --- a/vite/src/pipedal/ToolTipEx.tsx +++ b/vite/src/pipedal/ToolTipEx.tsx @@ -81,7 +81,7 @@ function ToolTipEx(props: ToolTipExProps) { if (valueTooltip === undefined) // no timeout if there's a value tooltip { if (t > 0) { - console.log("ToolTipEx: starting timeout for ", t); + // console.log("ToolTipEx: starting timeout for ", t); handle = window.setTimeout(() => { setOpen(true); },t); @@ -89,7 +89,7 @@ function ToolTipEx(props: ToolTipExProps) { } return () => { if (handle !== null) { - console.log("ToolTipEx: clearing timeout for ", t); + // console.log("ToolTipEx: clearing timeout for ", t); window.clearTimeout(handle); } }; @@ -161,6 +161,7 @@ function ToolTipEx(props: ToolTipExProps) { } } } + let effectiveTitle: React.ReactNode | null = null; let placement: "top-start" | "right" = "top-start"; if (valueTooltip !== undefined && !isLongPress) { @@ -181,44 +182,40 @@ function ToolTipEx(props: ToolTipExProps) { return (
{ - console.log("ToolTipEx: onClickCapture"); + // console.log("ToolTipEx: onClickCapture"); handleClickCapture(e); - setTimeout(0); - setOpen(false); - setIsLongPress(false); - setLongPressLeaving(true); }} onMouseEnter={(e)=> { - console.log("ToolTipEx: onMouseEnter"); + // console.log("ToolTipEx: onMouseEnter"); if (!longPressLeaving) {// Don't handle mouse enter if we're in a long press leaving state handleMouseEnter(e); } }} onMouseLeave={(e)=> { - console.log("ToolTipEx: onMouseLeave"); + // console.log("ToolTipEx: onMouseLeave"); handleMouseLeave(e); }} onPointerCancelCapture={(e) => { - console.log("ToolTipEx: onPointerCancelCapture"); + // console.log("ToolTipEx: onPointerCancelCapture"); handlePointerCancel(e); }} onContextMenuCapture={(e) => { - console.log("ToolTipEx: onContextMenuCapture"); + // console.log("ToolTipEx: onContextMenuCapture"); handleConextMenu(e); return false; }} onPointerDownCapture={(e) => { - console.log("ToolTipEx: onPointerDownCapture"); + // console.log("ToolTipEx: onPointerDownCapture"); handlePointerDownCapture(e); }} onPointerMoveCapture={(e) => { - console.log("ToolTipEx: onPointerMoveCapture"); + // console.log("ToolTipEx: onPointerMoveCapture"); handlePointerMoveCapture(e); }} onPointerUpCapture={(e) => { - console.log("ToolTipEx: onPointerUpCapture"); + // console.log("ToolTipEx: onPointerUpCapture"); handlePointerUpCapture(e); }} > diff --git a/vite/src/pipedal/UseWindowSize.tsx b/vite/src/pipedal/UseWindowSize.tsx index 93e92f7..e14c206 100644 --- a/vite/src/pipedal/UseWindowSize.tsx +++ b/vite/src/pipedal/UseWindowSize.tsx @@ -21,38 +21,43 @@ * SOFTWARE. */ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect } from 'react'; const getSize = () => { - return { - width: window.innerWidth, - height: window.innerHeight, - }; + return { + width: window.innerWidth, + height: window.innerHeight, + }; }; - + export interface WindowSize { width: number; height: number; } export default function useWindowSize() { - - const [size, setSize] = useState(getSize()); - - const handleResize = useCallback(() => { - let ticking = false; - if (!ticking) { - window.requestAnimationFrame(() => { - setSize(getSize()); - ticking = false; - }); - ticking = true; - } - }, []); - useEffect(() => { - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); - - return [size]; + const [size, setSize] = useState(getSize()); + + useEffect(() => { + let mounted = true; + let handleResizeT = () => { + let pendingCallback = false; + if (!pendingCallback) { + window.requestAnimationFrame(() => { + if (mounted) { + setSize(getSize()); + pendingCallback = false; + } + }); + pendingCallback = true; + } + } + window.addEventListener('resize', handleResizeT); + return () => { + mounted = false; + window.removeEventListener('resize', handleResizeT); + }; + }, []); + + return [size]; } \ No newline at end of file