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 26854c4..687e05b 100644 --- a/PiPedalCommon/src/AlsaSequencer.cpp +++ b/PiPedalCommon/src/AlsaSequencer.cpp @@ -24,6 +24,7 @@ #include "AlsaSequencer.hpp" #include #include +#include #include "ss.hpp" // enumerate alsa sequencer ports @@ -35,7 +36,7 @@ namespace pipedal 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; } @@ -83,7 +84,7 @@ namespace pipedal 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 +#else port.isUmp = false; // UMP support is not available in all versions of ALSA #endif port.isSystemAnnounce = (typeBits & SND_SEQ_PORT_SYSTEM_ANNOUNCE) != 0; @@ -95,19 +96,52 @@ 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); + ports.push_back(std::move(port)); } } @@ -122,7 +156,7 @@ namespace pipedal // Open sequencer in input mode int rc; - rc = snd_seq_open(&seqHandle, "default", SND_SEQ_OPEN_DUPLEX, 0); + rc = snd_seq_open(&seqHandle, "default", SND_SEQ_OPEN_DUPLEX, 0); if (rc < 0) { // convert rc to message @@ -131,14 +165,15 @@ namespace pipedal 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))); } + snd_seq_nonblock(seqHandle, 1); // Set sequencer to non-blocking mode } AlsaSequencer::~AlsaSequencer() { @@ -190,69 +225,93 @@ namespace pipedal throw std::runtime_error("ALSA port not found"); } - bool AlsaSequencer::ReadMessage(AlsaMidiMessage &message) + void AlsaSequencer::WaitForMessage() { + auto fdCount = snd_seq_poll_descriptors_count(seqHandle, POLLIN); + if (fdCount == 0) return; + this->pollFds.resize(fdCount); + + snd_seq_poll_descriptors(seqHandle, pollFds.data(), fdCount, POLLIN); + + poll(pollFds.data(), fdCount, -1); // Wait indefinitely for input + } + bool AlsaSequencer::ReadMessage(AlsaMidiMessage &message, bool block) { // Event loop snd_seq_event_t *event = nullptr; - bool success = false; - if (snd_seq_event_input(seqHandle, &event) >= 0 && 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) + while (true) { + bool success = false; + int rc = snd_seq_event_input(seqHandle, &event); + if (rc < 0) { + if (rc == -EAGAIN) { + if (!block) { + return false; + } + WaitForMessage(); + } else { + // Handle other errors + throw std::runtime_error(SS("ALSA sequencer input error: " << snd_strerror(rc))); + } + } else if (event) { - 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; + 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.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; + } + snd_seq_free_event(event); + } + if (success) { + return true; } - snd_seq_free_event(event); } - return success; } int AlsaSequencer::CreateRealtimeInputQueue() @@ -294,8 +353,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); @@ -348,4 +405,17 @@ 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 {}; + } + +} // namespace pipedal diff --git a/PiPedalCommon/src/include/AlsaSequencer.hpp b/PiPedalCommon/src/include/AlsaSequencer.hpp index d70db85..abf296e 100644 --- a/PiPedalCommon/src/include/AlsaSequencer.hpp +++ b/PiPedalCommon/src/include/AlsaSequencer.hpp @@ -53,6 +53,8 @@ namespace pipedal bool isSpecific; bool isHardware = false; bool isSoftware = false; + bool isPort = false; + bool isVirtual = false; int cardNumber = -1; std::string rawMidiDevice; // e.g. "hw:0,0,0" for kernel devices }; @@ -94,7 +96,7 @@ namespace pipedal void ConnectPort(const std::string&name); // Read a single MIDI message from the sequencer input port - bool ReadMessage(AlsaMidiMessage &message); + bool ReadMessage(AlsaMidiMessage &message, bool block = true); // Get current real-time from the queue (useful for calculating precise timing) @@ -104,6 +106,7 @@ namespace pipedal int GetQueueId() const { return queueId; } private: + void WaitForMessage(); // Create an ALSA input queue with real-time timestamps for the given client/port int CreateRealtimeInputQueue(); @@ -114,7 +117,7 @@ namespace pipedal }; 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 @@ -123,4 +126,6 @@ namespace pipedal void ReadMidiFromPort(int clientId, int portId); }; + std::string RawMidiIdToSequencerId(const std::vector &seqDevices,const std::string &rawMidiId); + } \ No newline at end of file diff --git a/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl b/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl index 7be30fd..8a1e290 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl @@ -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..978bdef 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..886d8dd 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/ToobMix.ttl b/lv2/aarch64/ToobAmp.lv2/ToobMix.ttl index 4202334..8afd4ea 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobMix.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobMix.ttl @@ -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/ToobPlayer.ttl b/lv2/aarch64/ToobAmp.lv2/ToobPlayer.ttl new file mode 100644 index 0000000..246ad7f --- /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 62 ; + 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..4437aa8 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: . @@ -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..0790055 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobRecordStereo.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobRecordStereo.ttl @@ -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/ToobVolume.ttl b/lv2/aarch64/ToobAmp.lv2/ToobVolume.ttl index b3e9e35..fbf69d5 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 ; 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..e833410 100644 --- a/src/AlsaDriver.cpp +++ b/src/AlsaDriver.cpp @@ -2286,7 +2286,7 @@ namespace pipedal } #ifdef JUNK - static void ExpectEvent(AlsaDriverImpl::AlsaMidiDeviceImpl &m, int event, const std::vector message) +static void ExpectEvent(AlsaDriverImpl::AlsaMidiDeviceImpl &m, int event, const std::vector message) { MidiEvent e; m.GetMidiInputEvent(&e, event); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2f3b1d7..07108d8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -323,6 +323,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. ) @@ -360,6 +361,7 @@ target_link_libraries(pipedal_kconfig PRIVATE ftxui::dom PRIVATE ftxui::component # Not needed for this example. PRIVATE PiPedalCommon + asound systemd ) @@ -385,7 +387,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 @@ -426,7 +428,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 +464,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/PiPedalAlsa.cpp b/src/PiPedalAlsa.cpp index 63e200a..48b0fa5 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) @@ -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..906e4a2 100644 --- a/src/PiPedalAlsa.hpp +++ b/src/PiPedalAlsa.hpp @@ -41,11 +41,25 @@ 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_; + int card_ = -1; + int device_ = -1; + int subdevice_ = -1; + bool isVirtual_ = false; + int subDevices_ = -1; + Direction direction_ = Direction::None; + DECLARE_JSON_MAP(AlsaMidiDeviceInfo); }; @@ -62,4 +76,5 @@ namespace pipedal { }; std::vector GetAlsaMidiInputDevices(); std::vector GetAlsaMidiOutputDevices(); + } \ No newline at end of file diff --git a/src/PiPedalAlsaTest.cpp b/src/PiPedalAlsaTest.cpp index c0c4da5..1e153bd 100644 --- a/src/PiPedalAlsaTest.cpp +++ b/src/PiPedalAlsaTest.cpp @@ -92,7 +92,7 @@ void ReadFromsequencerTest() 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) @@ -104,9 +104,39 @@ void ReadFromsequencerTest() } } +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 = GetAlsaMidiInputDevices(); + 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(); }