Alsa Sequencer Tests

This commit is contained in:
Robin E. R. Davies
2025-06-27 14:24:58 -04:00
parent f0628c2cce
commit acb668853b
17 changed files with 754 additions and 98 deletions
+1
View File
@@ -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()
+140 -70
View File
@@ -24,6 +24,7 @@
#include "AlsaSequencer.hpp"
#include <vector>
#include <string>
#include <regex>
#include "ss.hpp"
// enumerate alsa sequencer ports
@@ -35,7 +36,7 @@ namespace pipedal
std::vector<AlsaSequencerPort> 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<AlsaSequencerPort> &seqDevices, const std::string &rawMidiId)
{
for (const auto &device : seqDevices)
{
if (device.rawMidiDevice == rawMidiId)
{
return device.id;
}
}
return {};
}
} // namespace pipedal
+7 -2
View File
@@ -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<Connection> connections;
std::vector<struct pollfd> 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<AlsaSequencerPort> &seqDevices,const std::string &rawMidiId);
}
+15 -4
View File
@@ -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" ;
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -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 ;
+307
View File
@@ -0,0 +1,307 @@
@prefix doap: <http://usefulinc.com/ns/doap#> .
@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix units: <http://lv2plug.in/ns/extensions/units#> .
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
@prefix midi: <http://lv2plug.in/ns/ext/midi#> .
@prefix epp: <http://lv2plug.in/ns/ext/port-props#> .
@prefix uiext: <http://lv2plug.in/ns/extensions/ui#> .
@prefix idpy: <http://harrisonconsoles.com/lv2/inlinedisplay#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix mod: <http://moddevices.com/ns/mod#> .
@prefix param: <http://lv2plug.in/ns/ext/parameters#> .
@prefix work: <http://lv2plug.in/ns/ext/worker#> .
@prefix pg: <http://lv2plug.in/ns/ext/port-groups#> .
@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
@prefix patch: <http://lv2plug.in/ns/ext/patch#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix state: <http://lv2plug.in/ns/ext/state#> .
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix toobPlayer: <http://two-play.com/plugins/toob-player#> .
toobPlayer:mixGroup
a param:ControlGroup ,
pg:InputGroup ;
lv2:name "Mix" ;
lv2:symbol "mixGroup" .
<http://two-play.com/rerdavies#me>
a foaf:Person ;
foaf:name "Robin Davies" ;
foaf:mbox <mailto:rerdavies@gmail.com> ;
foaf:homepage <https://github.com/sponsors/rerdavies> .
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;
.
<http://two-play.com/plugins/toob-player>
a lv2:Plugin ,
lv2:GeneratorPlugin ;
doap:name "TooB File Player" ,
"TooB File Player"@en-gb-gb
;
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
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 ;
]
.
+6 -7
View File
@@ -21,7 +21,6 @@
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ui: <http://lv2plug.in/ns/extensions/ui#> .
@prefix pprop: <http://lv2plug.in/ns/ext/port-props#> .
@prefix pipedal_ui: <http://github.com/rerdavies/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;
+4 -4
View File
@@ -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;
+1 -1
View File
@@ -31,7 +31,7 @@
<http://two-play.com/plugins/toob-volume>
a lv2:Plugin ,
lv2:UtilityPlugin ;
lv2:MixerPlugin ;
doap:name "TooB Volume" ,
"TooB Volume"@en-gb-gb
;
+4
View File
@@ -97,6 +97,10 @@
lv2:binary <ToobAmp.so> ;
rdfs:seeAlso <ToobMix.ttl> .
<http://two-play.com/plugins/toob-player> a lv2:Plugin ;
lv2:binary <ToobAmp.so> ;
rdfs:seeAlso <ToobPlayer.ttl> .
<http://two-play.com/plugins/toob-graphiceq> a lv2:Plugin ;
lv2:binary <ToobAmp.so> ;
rdfs:seeAlso <ToobGraphicEq.ttl> .
+1 -1
View File
@@ -2286,7 +2286,7 @@ namespace pipedal
}
#ifdef JUNK
static void ExpectEvent(AlsaDriverImpl::AlsaMidiDeviceImpl &m, int event, const std::vector<uint8_t> message)
static void ExpectEvent(AlsaDriverImpl::AlsaMidiDeviceImpl &m, int event, const std::vector<uint8_t> message)
{
MidiEvent e;
m.GetMidiInputEvent(&e, event);
+5 -3
View File
@@ -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)
+215 -3
View File
@@ -47,6 +47,7 @@ void PiPedalAlsaDevices::cacheDevice(const std::string &name, const AlsaDeviceIn
std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
{
std::lock_guard guard{alsaMutex};
std::vector<AlsaDeviceInfo> result;
@@ -118,7 +119,7 @@ std::vector<AlsaDeviceInfo> 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<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
info.sampleRates_.push_back(rate);
}
}
if (minBufferSize < 16) {
if (minBufferSize < 16)
{
minBufferSize = 16;
}
@@ -180,7 +182,207 @@ std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
return result;
}
static std::vector<AlsaMidiDeviceInfo> GetAlsaDevices(const char *devname, const char *direction)
static void AddMidiCardDevicesToList(snd_ctl_t *ctl, int card, int device, AlsaMidiDeviceInfo::Direction direction, std::vector<AlsaMidiDeviceInfo> *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<AlsaMidiDeviceInfo> *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<AlsaMidiDeviceInfo> GetAlsaDevices(const char *devname, const char *direction_)
{
std::vector<AlsaMidiDeviceInfo> 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<AlsaMidiDeviceInfo> OldGetAlsaDevices(const char *devname, const char *direction)
{
std::vector<AlsaMidiDeviceInfo> result;
{
@@ -214,6 +416,8 @@ static std::vector<AlsaMidiDeviceInfo> 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)
+15
View File
@@ -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<AlsaMidiDeviceInfo> GetAlsaMidiInputDevices();
std::vector<AlsaMidiDeviceInfo> GetAlsaMidiOutputDevices();
}
+31 -1
View File
@@ -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<int>(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();
}