This commit is contained in:
Robin E. R. Davies
2025-07-01 02:07:34 -04:00
79 changed files with 3815 additions and 901 deletions
+1 -1
View File
@@ -142,7 +142,7 @@
//"[pipedal_alsa_test]" //"[pipedal_alsa_test]"
// "[wifi_channels_test]" // "[wifi_channels_test]"
// "[locale]" // "[locale]"
"[pipedal_alsa_seq_test]" "[pipewire_input_stream]"
], ],
"stopAtEntry": false, "stopAtEntry": false,
+1
View File
@@ -11,6 +11,7 @@ set (DISPLAY_VERSION "PiPedal v1.4.76-Release")
set (PACKAGE_ARCHITECTURE ${DEBIAN_ARCHITECTURE}) set (PACKAGE_ARCHITECTURE ${DEBIAN_ARCHITECTURE})
set (CMAKE_INSTALL_PREFIX "/usr/") set (CMAKE_INSTALL_PREFIX "/usr/")
set (PIPEDAL_EXCLUDE_TESTS false)
include(CTest) include(CTest)
enable_testing() enable_testing()
+775 -88
View File
@@ -24,18 +24,113 @@
#include "AlsaSequencer.hpp" #include "AlsaSequencer.hpp"
#include <vector> #include <vector>
#include <string> #include <string>
#include <regex>
#include "ss.hpp" #include "ss.hpp"
#include <alsa/asoundlib.h>
#include <alsa/seq.h>
#include <stdexcept>
#include <poll.h>
#include <cstring>
#include "Finally.hpp"
#include "Lv2Log.hpp"
#include <mutex>
#include <thread>
// enumerate alsa sequencer ports // enumerate alsa sequencer ports
namespace pipedal 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<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
};
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<std::jthread> serviceThread;
std::atomic<bool> terminateThread{false};
Callback callback;
};
};
using namespace impl;
AlsaSequencer::ptr AlsaSequencer::Create()
{
return std::make_shared<AlsaSequencerImpl>();
}
std::vector<AlsaSequencerPort> AlsaSequencer::EnumeratePorts() std::vector<AlsaSequencerPort> AlsaSequencer::EnumeratePorts()
{ {
std::vector<AlsaSequencerPort> ports; std::vector<AlsaSequencerPort> ports;
snd_seq_t *seq; 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; return ports;
} }
@@ -81,8 +176,11 @@ namespace pipedal
port.canReadSubscribe = capability & SND_SEQ_PORT_CAP_SUBS_READ; port.canReadSubscribe = capability & SND_SEQ_PORT_CAP_SUBS_READ;
port.canWriteSubscribe = capability & SND_SEQ_PORT_CAP_SUBS_WRITE; port.canWriteSubscribe = capability & SND_SEQ_PORT_CAP_SUBS_WRITE;
auto typeBits = snd_seq_port_info_get_type(port_info); 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; 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.isSystemAnnounce = (typeBits & SND_SEQ_PORT_SYSTEM_ANNOUNCE) != 0;
port.isMidiSynth = (typeBits & SND_SEQ_PORT_TYPE_MIDI_GENERIC) != 0 || port.isMidiSynth = (typeBits & SND_SEQ_PORT_TYPE_MIDI_GENERIC) != 0 ||
(typeBits & SND_SEQ_PORT_TYPE_MIDI_GM) != 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.isSpecific = (typeBits & SND_SEQ_PORT_TYPE_SPECIFIC) != 0;
port.isSynth = (typeBits & SND_SEQ_PORT_TYPE_SYNTH) != 0; port.isSynth = (typeBits & SND_SEQ_PORT_TYPE_SYNTH) != 0;
port.isHardware = (typeBits & SND_SEQ_PORT_TYPE_HARDWARE) != 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.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.cardNumber = snd_seq_client_info_get_card(client_info);
port.port = snd_seq_port_info_get_port(port_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 // 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); 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; return ports;
} }
AlsaSequencer::AlsaSequencer() AlsaSequencerImpl::AlsaSequencerImpl()
{ {
seqHandle = nullptr; seqHandle = nullptr;
queueId = -1; queueId = -1;
@@ -125,24 +265,48 @@ namespace pipedal
// convert rc to message // convert rc to message
throw std::runtime_error(SS("Failed to open ALSA sequencer:" << snd_strerror(rc))); 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"); snd_seq_set_client_name(seqHandle, "PiPedal");
inPort = snd_seq_create_simple_port(seqHandle, "PiPedal:in", inPort = snd_seq_create_simple_port(seqHandle, "PiPedal:in",
SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE,
SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_MIDI_GENERIC |
SND_SEQ_PORT_TYPE_MIDI_GM | SND_SEQ_PORT_TYPE_APPLICATION); SND_SEQ_PORT_TYPE_MIDI_GM | SND_SEQ_PORT_TYPE_APPLICATION);
if (inPort < 0) if (inPort < 0)
{ {
// convert rc to message // convert rc to message
throw std::runtime_error(SS("Failed to open ALSA sequencer:" << snd_strerror(inPort))); throw std::runtime_error(SS("Failed to open ALSA sequencer:" << snd_strerror(inPort)));
} }
} CreateRealtimeInputQueue();
AlsaSequencer::~AlsaSequencer()
{ snd_seq_nonblock(seqHandle, 1); // Set sequencer to non-blocking mode
for (const auto &connection : connections)
// 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) if (queueId >= 0)
{ {
snd_seq_free_queue(seqHandle, queueId); 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)
{ {
ModifyConnection(clientId, portId, ConnectAction::Subscribe);
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});
} }
void AlsaSequencer::ConnectPort(const std::string &name) void AlsaSequencerImpl::ConnectPort(const std::string &id)
{ {
auto ports = EnumeratePorts(); auto ports = EnumeratePorts();
for (const auto &port : ports) for (const auto &port : ports)
{ {
if (port.name == name) if (port.id == id)
{ {
ConnectPort(port.client, port.port); ConnectPort(port.client, port.port);
return; return;
@@ -186,73 +342,303 @@ namespace pipedal
} }
throw std::runtime_error("ALSA port not found"); 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 // Event loop
snd_seq_event_t *event = nullptr; snd_seq_event_t *event = nullptr;
bool success = false; while (true)
if (snd_seq_event_input(seqHandle, &event) >= 0 && event)
{ {
success = true; bool success = false;
// Extract timestamp information int rc = snd_seq_event_input(seqHandle, &event);
message.timestamp = event->time.tick; if (rc < 0)
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: if (rc == -EAGAIN)
message.cc0 = 0x90 | event->data.note.channel; // channel {
message.cc1 = event->data.note.note; // note if (!WaitForMessage(timeoutMs))
message.cc2 = event->data.note.velocity; // velocity {
break; return false;
case SND_SEQ_EVENT_NOTEOFF: }
// handle note-off }
message.cc0 = 0x80 | event->data.note.channel; // channel else
message.cc1 = event->data.note.note; // note {
message.cc2 = event->data.note.off_velocity; // off velocity // Handle other errors
break; throw std::runtime_error(SS("ALSA sequencer input error: " << snd_strerror(rc)));
case SND_SEQ_EVENT_KEYPRESS: }
message.cc0 = 0xA0 | event->data.note.channel; // polyphonic key pressure }
message.cc1 = event->data.note.note; // note else if (event)
message.cc2 = event->data.note.velocity; // pressure {
break; success = true;
case SND_SEQ_EVENT_CONTROLLER: // Extract timestamp information
message.cc0 = 0xB0 | event->data.control.channel; // control change message.timestamp = event->time.tick;
message.cc1 = event->data.control.param; // controller number message.realtime_sec = event->time.time.tv_sec;
message.cc2 = event->data.control.value; // controller value message.realtime_nsec = event->time.time.tv_nsec;
break;
case SND_SEQ_EVENT_PGMCHANGE: // Process MIDI event here, e.g. NOTEON, NOTEOFF, etc.
message.cc0 = 0xC0 | event->data.control.channel; // program change switch (event->type)
message.cc1 = event->data.control.value; // program number {
message.cc2 = 0; // unused case SND_SEQ_EVENT_NOTEON:
break; message.Set(
case SND_SEQ_EVENT_CHANPRESS: (uint8_t)(0x90 | event->data.note.channel),
message.cc0 = 0xD0 | event->data.control.channel; // channel pressure (uint8_t)(event->data.note.note), // note
message.cc1 = event->data.control.value; // pressure value (uint8_t)(event->data.note.velocity)); // velocity
message.cc2 = 0; // unused break;
break; case SND_SEQ_EVENT_NOTEOFF:
case SND_SEQ_EVENT_PITCHBEND: // handle note-off
message.cc0 = 0xE0 | event->data.control.channel; // pitch bend message.Set(
message.cc1 = (event->data.control.value >> 7) & 0x7F; // MSB uint8_t(0x80 | event->data.note.channel),
message.cc2 = event->data.control.value & 0x7F; // LSB uint8_t(event->data.note.note),
break; uint8_t(event->data.note.off_velocity)); // off velocity
case SND_SEQ_EVENT_CONTROL14: break;
case SND_SEQ_EVENT_NONREGPARAM: case SND_SEQ_EVENT_KEYPRESS:
case SND_SEQ_EVENT_REGPARAM: message.Set(
case SND_SEQ_EVENT_SONGPOS: (uint8_t)(0xA0 | event->data.note.channel), // polyphonic key pressure
default: (uint8_t)(event->data.note.note), // note
success = false; (uint8_t)(event->data.note.velocity)); // pressure
break; 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) if (!seqHandle)
{ {
@@ -291,8 +677,6 @@ namespace pipedal
throw std::runtime_error(SS("Failed to start queue: " << snd_strerror(rc))); throw std::runtime_error(SS("Failed to start queue: " << snd_strerror(rc)));
} }
// Set the queue for input timestamping // Set the queue for input timestamping
snd_seq_port_info_t *port_info; snd_seq_port_info_t *port_info;
snd_seq_port_info_alloca(&port_info); snd_seq_port_info_alloca(&port_info);
@@ -321,7 +705,7 @@ namespace pipedal
return queueId; return queueId;
} }
bool AlsaSequencer::GetQueueRealtime(uint64_t *sec, uint32_t *nsec) bool AlsaSequencerImpl::GetQueueRealtime(uint64_t *sec, uint32_t *nsec)
{ {
if (!seqHandle || queueId < 0) if (!seqHandle || queueId < 0)
{ {
@@ -345,4 +729,307 @@ namespace pipedal
return true; 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 {};
}
void AlsaSequencerImpl::ModifyConnection(int clientId, int portId, ConnectAction action)
{
std::lock_guard<std::mutex> 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<AlsaSequencerDeviceMonitorImpl>();
}
void AlsaSequencerDeviceMonitorImpl::StartMonitoring(
Callback &&onChangeCallback)
{
started = true;
this->callback = std::move(onChangeCallback);
this->serviceThread = std::make_unique<std::jthread>(
[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<struct pollfd> 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
+1
View File
@@ -41,6 +41,7 @@ message(STATUS "NMPIPEDAL CMAKE_CXX_FLAGS: ${CMAKE_CXX_FLAGS}")
# Use the sdbus-c++ target in SDBusCpp namespace # Use the sdbus-c++ target in SDBusCpp namespace
add_library(PiPedalCommon STATIC add_library(PiPedalCommon STATIC
include/Finally.hpp
include/dbus/org.freedesktop.NetworkManager.Device.Statistics.hpp include/dbus/org.freedesktop.NetworkManager.Device.Statistics.hpp
include/dbus/org.freedesktop.NetworkManager.Settings.Connection.hpp include/dbus/org.freedesktop.NetworkManager.Settings.Connection.hpp
include/dbus/org.freedesktop.NetworkManager.IP6Config.hpp include/dbus/org.freedesktop.NetworkManager.IP6Config.hpp
+2 -2
View File
@@ -44,9 +44,9 @@ std::string HtmlHelper::timeToHttpDate(std::filesystem::file_time_type time)
{ {
// Convert to time_t. // Convert to time_t.
std::chrono::system_clock::time_point system_time = std::chrono::clock_cast<std::chrono::system_clock>(time); auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(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); return timeToHttpDate(time_t_value);
} }
+112 -92
View File
@@ -89,7 +89,6 @@ bool WifiConfigSettings::ValidateCountryCode(const std::string &text)
return std::isalpha(text[0]) && std::isalpha(text[1]); return std::isalpha(text[0]) && std::isalpha(text[1]);
} }
void WifiConfigSettings::Load() void WifiConfigSettings::Load()
{ {
try try
@@ -115,9 +114,9 @@ void WifiConfigSettings::Load()
} }
static void openWithPerms( static void openWithPerms(
pipedal::ofstream_synced &f, pipedal::ofstream_synced &f,
const std::filesystem::path &path, const std::filesystem::path &path,
std::filesystem::perms perms = std::filesystem::perms perms =
std::filesystem::perms::owner_read | std::filesystem::perms::owner_write | std::filesystem::perms::owner_read | std::filesystem::perms::owner_write |
std::filesystem::perms::group_read | std::filesystem::perms::group_write) std::filesystem::perms::group_read | std::filesystem::perms::group_write)
{ {
@@ -132,13 +131,16 @@ static void openWithPerms(
f.open(path); f.open(path);
f.close(); f.close();
} }
try { try
{
// set the perms. // set the perms.
std::filesystem::permissions( std::filesystem::permissions(
path, path,
perms, perms,
std::filesystem::perm_options::replace); std::filesystem::perm_options::replace);
} catch (const std::exception&) { }
catch (const std::exception &)
{
Lv2Log::warning(SS("Failed to set permissions on" << path << ".")); Lv2Log::warning(SS("Failed to set permissions on" << path << "."));
} }
} }
@@ -147,10 +149,10 @@ static void openWithPerms(
f.open(path); 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. // sync legacy settings, just in case i don't know what.
newSettings.mdnsName_ = newSettings.hotspotName_; newSettings.mdnsName_ = newSettings.hotspotName_;
newSettings.enable_ = newSettings.IsEnabled(); newSettings.enable_ = newSettings.IsEnabled();
@@ -163,12 +165,12 @@ void WifiConfigSettings::Save()
newSettings.password_ = oldSettings.password_; newSettings.password_ = oldSettings.password_;
} }
newSettings.hasSavedPassword_ = newSettings.hasPassword_; newSettings.hasSavedPassword_ = newSettings.hasPassword_;
try try
{ {
ofstream_synced f; ofstream_synced f;
openWithPerms(f,CONFIG_PATH); openWithPerms(f, CONFIG_PATH);
json_writer writer(f,false); json_writer writer(f, false);
writer.write(&newSettings); writer.write(&newSettings);
} }
catch (const std::exception &e) catch (const std::exception &e)
@@ -300,17 +302,14 @@ namespace pipedal::priv
connection["ipv4"]["method"] = sdbus::Variant(std::string("shared")); connection["ipv4"]["method"] = sdbus::Variant(std::string("shared"));
connection["ipv6"]["method"] = sdbus::Variant(std::string("ignore")); connection["ipv6"]["method"] = sdbus::Variant(std::string("ignore"));
connection["ipv4"]["address-data"] = sdbus::Variant(std::vector<std::map<std::string, sdbus::Variant>>{{{"address", sdbus::Variant("192.168.4.1")},
connection["ipv4"]["address-data"] = sdbus::Variant(std::vector<std::map<std::string, sdbus::Variant>>{{ {"prefix", sdbus::Variant(uint32_t(24))}}});
{"address", sdbus::Variant("192.168.4.1")},
{"prefix", sdbus::Variant(uint32_t(24))}
}});
connection["ipv4"]["dhcp-send-hostname"] = sdbus::Variant(true); connection["ipv4"]["dhcp-send-hostname"] = sdbus::Variant(true);
connection["ipv4"]["dhcp-hostname"] = sdbus::Variant("raspberrypi"); connection["ipv4"]["dhcp-hostname"] = sdbus::Variant("raspberrypi");
connection["ipv4"]["dhcp-options"] = sdbus::Variant(std::vector<std::string>{ connection["ipv4"]["dhcp-options"] = sdbus::Variant(std::vector<std::string>{
"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,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" //"option:classless-static-route,192.168.4.0/24,192.168.4.1"
}); });
auto connection_path = AddConnection(connection); auto connection_path = AddConnection(connection);
return connection_path; return connection_path;
@@ -326,7 +325,6 @@ namespace pipedal::priv
return active_connection_path; return active_connection_path;
} }
void RemoveConnection(const std::string &connection) void RemoveConnection(const std::string &connection)
{ {
auto connection_proxy = sdbus::createProxy( auto connection_proxy = sdbus::createProxy(
@@ -429,7 +427,6 @@ namespace pipedal::priv
}; };
} }
JSON_MAP_BEGIN(WifiConfigSettings) JSON_MAP_BEGIN(WifiConfigSettings)
// v0 // v0
JSON_MAP_REFERENCE(WifiConfigSettings, valid) JSON_MAP_REFERENCE(WifiConfigSettings, valid)
@@ -560,52 +557,51 @@ bool WifiConfigSettings::ValidateChannel(const std::string &countryCode, const s
return true; return true;
} }
static const char* trueValues[] { static const char *trueValues[]{
"true", "true",
"on", "on",
"yes" "yes"
"1", "1",
nullptr nullptr};
}; static const char *falseValues[]{
static const char* falseValues[] {
"false", "false",
"off", "off",
"no", "no",
"0", "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; ++matches;
} }
return false; 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; *outputValue = true;
return true; return true;
} }
if (Matches(value, falseValues)) { if (Matches(value, falseValues))
{
*outputValue = false; *outputValue = false;
return true; return true;
} }
*outputValue = false; *outputValue = false;
return 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; using ssid_t = WifiConfigSettings::ssid_t;
char c; char c;
ssid_t result; ssid_t result;
while (ss.peek() == ' ') while (ss.peek() == ' ')
{ {
@@ -624,28 +620,33 @@ static WifiConfigSettings::ssid_t readSsid(std::istream&ss)
break; break;
} }
ss >> c; ss >> c;
switch (c) { switch (c)
case 'n': {
result.push_back((uint8_t)'\n'); case 'n':
break; result.push_back((uint8_t)'\n');
case 'r': break;
result.push_back((uint8_t)'\r'); case 'r':
break; result.push_back((uint8_t)'\r');
case 't': break;
result.push_back((uint8_t)'\t'); case 't':
break; result.push_back((uint8_t)'\t');
case 'b': break;
result.push_back((uint8_t)'\b'); case 'b':
break; result.push_back((uint8_t)'\b');
default: break;
result.push_back((uint8_t)c); default:
break; result.push_back((uint8_t)c);
break;
} }
} else { }
else
{
result.push_back((uint8_t)c); result.push_back((uint8_t)c);
} }
} }
} else { }
else
{
while (!ss.eof()) while (!ss.eof())
{ {
if (c == ':') if (c == ':')
@@ -662,7 +663,7 @@ static WifiConfigSettings::ssid_t readSsid(std::istream&ss)
} }
return result; return result;
} }
static std::vector<WifiConfigSettings::ssid_t> stringToSsidArray(const std::string&value) static std::vector<WifiConfigSettings::ssid_t> stringToSsidArray(const std::string &value)
{ {
using ssid_t = WifiConfigSettings::ssid_t; using ssid_t = WifiConfigSettings::ssid_t;
std::istringstream ss(value); std::istringstream ss(value);
@@ -671,7 +672,7 @@ static std::vector<WifiConfigSettings::ssid_t> stringToSsidArray(const std::stri
while (true) while (true)
{ {
ssid_t ssid = readSsid(ss); ssid_t ssid = readSsid(ss);
if (ssid.size() == 0) if (ssid.size() == 0)
{ {
break; break;
} }
@@ -683,16 +684,15 @@ static std::vector<WifiConfigSettings::ssid_t> stringToSsidArray(const std::stri
} }
} }
return result; return result;
} }
void WifiConfigSettings::ParseArguments( void WifiConfigSettings::ParseArguments(
const std::vector<std::string> &argv, const std::vector<std::string> &argv,
HotspotAutoStartMode startMode, HotspotAutoStartMode startMode,
const std::string homeNetworkSsid const std::string homeNetworkSsid
) )
{ {
this->valid_ = false ; this->valid_ = false;
if (argv.size() != 4) if (argv.size() != 4)
{ {
throw invalid_argument("Invalid number of arguments."); throw invalid_argument("Invalid number of arguments.");
@@ -714,7 +714,6 @@ void WifiConfigSettings::ParseArguments(
this->hasPassword_ = this->password_.length() != 0; this->hasPassword_ = this->password_.length() != 0;
this->hasSavedPassword_ = oldSettings.hasSavedPassword_; this->hasSavedPassword_ = oldSettings.hasSavedPassword_;
if (!ValidateCountryCode(this->countryCode_)) if (!ValidateCountryCode(this->countryCode_))
{ {
throw invalid_argument("Invalid country code."); throw invalid_argument("Invalid country code.");
@@ -723,7 +722,7 @@ void WifiConfigSettings::ParseArguments(
throw invalid_argument("Hotspot name is too long."); throw invalid_argument("Hotspot name is too long.");
if (this->hotspotName_.length() < 1) if (this->hotspotName_.length() < 1)
throw invalid_argument("Hotspot name is too short."); throw invalid_argument("Hotspot name is too short.");
if (this->password_.size() != 0 && this->password_.size() < 8) if (this->password_.size() != 0 && this->password_.size() < 8)
throw invalid_argument("Passphrase must be at least 8 characters long."); throw invalid_argument("Passphrase must be at least 8 characters long.");
@@ -742,12 +741,12 @@ void WifiConfigSettings::ParseArguments(
this->valid_ = true; this->valid_ = true;
} }
bool WifiConfigSettings::ConfigurationChanged(const WifiConfigSettings&other) const bool WifiConfigSettings::ConfigurationChanged(const WifiConfigSettings &other) const
{ {
return !( return !(
this->valid_ == other.valid_ && this->valid_ == other.valid_ &&
//this->rebootRequired_ == other.rebootRequired_ && // this->rebootRequired_ == other.rebootRequired_ &&
//this->enable_ == other.enable_ && // this->enable_ == other.enable_ &&
this->countryCode_ == other.countryCode_ && this->countryCode_ == other.countryCode_ &&
this->hotspotName_ == other.hotspotName_ && this->hotspotName_ == other.hotspotName_ &&
this->hasPassword_ == other.hasPassword_ && this->hasPassword_ == other.hasPassword_ &&
@@ -756,20 +755,18 @@ bool WifiConfigSettings::ConfigurationChanged(const WifiConfigSettings&other) co
this->homeNetwork_ == other.homeNetwork_ && this->homeNetwork_ == other.homeNetwork_ &&
this->autoStartMode_ == other.autoStartMode_ && 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_; return !ConfigurationChanged(other) && this->wifiWarningGiven_ == other.wifiWarningGiven_;
} }
static bool CanSeeHomeNetwork(const std::string&home, const std::vector<std::string>&availableNetworks) static bool CanSeeHomeNetwork(const std::string &home, const std::vector<std::string> &availableNetworks)
{ {
for (const auto &availableNetwork: availableNetworks) for (const auto &availableNetwork : availableNetworks)
{ {
if (availableNetwork == home) if (availableNetwork == home)
{ {
return true; return true;
} }
@@ -778,38 +775,39 @@ static bool CanSeeHomeNetwork(const std::string&home, const std::vector<std::str
} }
bool WifiConfigSettings::WantsHotspot( bool WifiConfigSettings::WantsHotspot(
bool ethernetConnected, bool ethernetConnected,
const std::vector<std::string> &availableRememberedNetworks, const std::vector<std::string> &availableRememberedNetworks,
const std::vector<std::string> &availableNetworks) const std::vector<std::string> &availableNetworks)
{ {
if ((!this->valid_)) if ((!this->valid_))
return false; return false;
HotspotAutoStartMode autoStartMode = (HotspotAutoStartMode)this->autoStartMode_; HotspotAutoStartMode autoStartMode = (HotspotAutoStartMode)this->autoStartMode_;
switch (autoStartMode) switch (autoStartMode)
{ {
case HotspotAutoStartMode::Never: case HotspotAutoStartMode::Never:
default: default:
return false; return false;
case HotspotAutoStartMode::NoEthernetConnection: case HotspotAutoStartMode::NoEthernetConnection:
return !ethernetConnected; return !ethernetConnected;
case HotspotAutoStartMode::NotAtHome: case HotspotAutoStartMode::NotAtHome:
return !CanSeeHomeNetwork(this->homeNetwork_, availableNetworks); return !CanSeeHomeNetwork(this->homeNetwork_, availableNetworks);
case HotspotAutoStartMode::NoRememberedWifiConections: case HotspotAutoStartMode::NoRememberedWifiConections:
return availableRememberedNetworks.size() == 0; return availableRememberedNetworks.size() == 0;
case HotspotAutoStartMode::Always: case HotspotAutoStartMode::Always:
return true; return true;
} }
} }
std::string pipedal::ssidToString(const std::vector<uint8_t> &ssid) std::string pipedal::ssidToString(const std::vector<uint8_t> &ssid)
{ {
std::stringstream s; 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; s << (char)v;
} }
return s.str(); return s.str();
@@ -819,21 +817,43 @@ std::vector<std::string> pipedal::ssidToStringVector(const std::vector<std::vect
{ {
std::vector<std::string> result; std::vector<std::string> result;
result.reserve(ssids.size()); result.reserve(ssids.size());
for (const std::vector<uint8_t> &ssid: ssids) for (const std::vector<uint8_t> &ssid : ssids)
{ {
result.push_back(ssidToString(ssid)); result.push_back(ssidToString(ssid));
} }
return result; return result;
} }
bool WifiConfigSettings::WantsHotspot( bool WifiConfigSettings::WantsHotspot(
bool ethernetConnected, bool ethernetConnected,
const std::vector<std::vector<uint8_t>> &availableRememberedNetworks, // remembered networks that are currently visible const std::vector<std::vector<uint8_t>> &availableRememberedNetworks, // remembered networks that are currently visible
const std::vector<std::vector<uint8_t>> &availableNetworks // all visible networks. const std::vector<std::vector<uint8_t>> &availableNetworks // all visible networks.
) )
{ {
std::vector<std::string> sAvailableRememberedNetworks = ssidToStringVector(availableRememberedNetworks); std::vector<std::string> sAvailableRememberedNetworks = ssidToStringVector(availableRememberedNetworks);
std::vector<std::string> sAvailableNetworks = ssidToStringVector(availableNetworks); std::vector<std::string> 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;
}
}
+271 -40
View File
@@ -25,12 +25,77 @@
#include <vector> #include <vector>
#include <string> #include <string>
#include <alsa/asoundlib.h> #include <memory>
#include <alsa/seq.h> #include <functional>
#include <stdexcept> #include "json.hpp"
namespace pipedal 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<AlsaSequencerPortSelection> connections_;
public:
const std::vector<AlsaSequencerPortSelection>& connections() const { return connections_; }
std::vector<AlsaSequencerPortSelection>& connections() { return connections_; }
void connections(const std::vector<AlsaSequencerPortSelection>& 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 struct AlsaSequencerPort
{ {
std::string id; std::string id;
@@ -53,28 +118,181 @@ namespace pipedal
bool isSpecific; bool isSpecific;
bool isHardware = false; bool isHardware = false;
bool isSoftware = false; bool isSoftware = false;
bool isPort = false;
bool isVirtual = false;
int cardNumber = -1; int cardNumber = -1;
int32_t displaySortOrder = 0;
std::string rawMidiDevice; // e.g. "hw:0,0,0" for kernel devices std::string rawMidiDevice; // e.g. "hw:0,0,0" for kernel devices
}; };
struct AlsaMidiMessage 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 uint64_t realtime_sec; // real-time timestamp seconds
uint32_t realtime_nsec; // real-time timestamp nanoseconds uint32_t realtime_nsec; // real-time timestamp nanoseconds
uint8_t cc0; size_t size = 0;
uint8_t cc1; uint8_t *data = fixedBuffer;
uint8_t cc2;
uint8_t fixedBuffer[12];
}; };
/* /*
* AlsaSequencer - ALSA MIDI sequencer interface with real-time timestamp support * AlsaSequencer - ALSA MIDI sequencer interface with real-time timestamp support
* *
* Example usage with real-time timestamps: * Example usage with real-time timestamps:
* *
* AlsaSequencer sequencer; * AlsaSequencer sequencer;
* sequencer.connect(clientId, portId); * sequencer.connect(clientId, portId);
* *
* AlsaMidiMessage message; * AlsaMidiMessage message;
* while (sequencer.ReadMessage(&message)) { * while (sequencer.ReadMessage(&message)) {
* // message.realtime_sec and message.realtime_nsec contain precise real-time timestamps * // 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); * printf("MIDI event at %lu.%09u seconds\n", message.realtime_sec, message.realtime_nsec);
* } * }
*/ */
class AlsaSequencer class AlsaSequencer
{ {
protected:
AlsaSequencer() {}
public: public:
virtual ~AlsaSequencer() = default;
using self = AlsaSequencer;
using ptr = std::shared_ptr<self>;
static ptr Create();
static std::vector<AlsaSequencerPort> EnumeratePorts(); static std::vector<AlsaSequencerPort> EnumeratePorts();
AlsaSequencer(); public:
~AlsaSequencer(); virtual void ConnectPort(int clientId, int portId) = 0;
virtual void ConnectPort(const std::string &id) = 0;
void ConnectPort(int clientId, int portId); virtual void SetConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0;
void ConnectPort(const std::string&name);
// Read a single MIDI message from the sequencer input port // Read a single MIDI message from the sequencer input port. A timeout of -1 blocks indefinitely.
bool ReadMessage(AlsaMidiMessage &message); // 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) virtual void RemoveAllConnections() = 0;
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<Connection> 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);
}; };
class AlsaSequencerDeviceMonitor {
protected:
AlsaSequencerDeviceMonitor() {}
public:
virtual ~AlsaSequencerDeviceMonitor() {}
using self = AlsaSequencerDeviceMonitor;
using ptr = std::shared_ptr<self>;
static ptr Create();
enum class MonitorAction {
DeviceAdded,
DeviceRemoved
};
using Callback = std::function<void(MonitorAction action, int client, const std::string& clientName)>;
virtual void StartMonitoring(Callback &&onChangeCallback) = 0;
virtual void StopMonitoring() = 0;
};
std::string RawMidiIdToSequencerId(const std::vector<AlsaSequencerPort> &seqDevices, const std::string &rawMidiId);
} }
+39
View File
@@ -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 <functional>
namespace pipedal {
class Finally {
public:
Finally(std::function<void(void)> &&fn)
:fn (std::move(fn))
{
}
~Finally() {
fn();
}
private:
std::function<void(void)> fn;
};
}
@@ -95,6 +95,10 @@ namespace pipedal {
bool operator==(const WifiConfigSettings&other) const; bool operator==(const WifiConfigSettings&other) const;
bool ConfigurationChanged(const WifiConfigSettings&other) const; bool ConfigurationChanged(const WifiConfigSettings&other) const;
bool WantsHotspot(bool ethernetConnected)
{
return WantsHotspot(ethernetConnected, std::vector<std::string>{}, std::vector<std::string>{});
}
bool WantsHotspot( bool WantsHotspot(
bool ethernetConnected, bool ethernetConnected,
const std::vector<std::string> &availableRememberedNetworks, // remembered networks that are currently visible const std::vector<std::string> &availableRememberedNetworks, // remembered networks that are currently visible
@@ -105,7 +109,8 @@ namespace pipedal {
const std::vector<std::vector<uint8_t>> &availableRememberedNetworks, // remembered networks that are currently visible const std::vector<std::vector<uint8_t>> &availableRememberedNetworks, // remembered networks that are currently visible
const std::vector<std::vector<uint8_t>> &availableNetworks // all visible networks. const std::vector<std::vector<uint8_t>> &availableNetworks // all visible networks.
); );
bool NeedsScan() const;
bool NeedsWifi() const;
public: public:
DECLARE_JSON_MAP(WifiConfigSettings); DECLARE_JSON_MAP(WifiConfigSettings);
}; };
+2
View File
@@ -35,6 +35,8 @@
#include <string_view> #include <string_view>
#include <string.h> #include <string.h>
#include <stdexcept> #include <stdexcept>
#include <vector>
#include <memory>
#define DECLARE_JSON_MAP(CLASSNAME) \ #define DECLARE_JSON_MAP(CLASSNAME) \
static pipedal::json_map::storage_type<CLASSNAME> jmap static pipedal::json_map::storage_type<CLASSNAME> jmap
@@ -30,6 +30,7 @@
#include <string> #include <string>
#include <stdexcept> #include <stdexcept>
#include <utility> #include <utility>
#include <memory>
#include "json.hpp" #include "json.hpp"
namespace pipedal namespace pipedal
+122
View File
@@ -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 <alex.merry@kde.org>
# Copyright 2014 Martin Gräßlin <mgraesslin@kde.org>
# Copyright 2018-2020 Jan Grulich <jgrulich@redhat.com>
#
# 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"
)
+2 -1
View File
@@ -26,7 +26,8 @@ Run the following commands to install dependent libraries required by the PiPeda
libsystemd-dev catch libasound2-dev uuid-dev \ libsystemd-dev catch libasound2-dev uuid-dev \
authbind libavahi-client-dev libnm-dev libicu-dev \ authbind libavahi-client-dev libnm-dev libicu-dev \
libsdbus-c++-dev libzip-dev google-perftools \ libsdbus-c++-dev libzip-dev google-perftools \
libgoogle-perftools-dev libgoogle-perftools-dev \
libpipewire-0.3-dev
### Installing Sources ### Installing Sources
+1 -1
View File
@@ -93,7 +93,7 @@ cabir:impulseFile3
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
rdfs:comment """ rdfs:comment """
TooB Cab IR is a convolution-based guitar cabinet impulse response simulator. TooB Cab IR is a convolution-based guitar cabinet impulse response simulator.
+1 -1
View File
@@ -50,7 +50,7 @@ toob:frequencyResponseVector
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
mod:brand "TooB"; mod:brand "TooB";
mod:label "TooB CabSim"; mod:label "TooB CabSim";
@@ -53,7 +53,7 @@ toobimpulse:impulseFile
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
rdfs:comment """ 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 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
@@ -51,7 +51,7 @@ toobimpulse:impulseFile
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
rdfs:comment """ rdfs:comment """
+1 -1
View File
@@ -66,7 +66,7 @@ inputStage:filterGroup
doap:license <https://two-play.com/TooB/licenses/isc> ; doap:license <https://two-play.com/TooB/licenses/isc> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
mod:brand "TooB"; mod:brand "TooB";
mod:label "TooB Input"; mod:label "TooB Input";
+1 -1
View File
@@ -68,7 +68,7 @@ pstage:stage3
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
mod:brand "TooB"; mod:brand "TooB";
mod:label "Power Stage"; mod:label "Power Stage";
+1 -1
View File
@@ -57,7 +57,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
rdfs:comment "TooB spectrum analyzer" ; rdfs:comment "TooB spectrum analyzer" ;
mod:brand "TooB"; mod:brand "TooB";
+16 -5
View File
@@ -57,7 +57,7 @@ tonestack:eqGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
uiext:ui <http://two-play.com/plugins/toob-tone-stack-ui>; uiext:ui <http://two-play.com/plugins/toob-tone-stack-ui>;
@@ -131,17 +131,28 @@ tone stack used in Polytone and HiWatt amps.
rdfs:label "Baxandall" ; rdfs:label "Baxandall" ;
rdf:value 2.0 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 , a lv2:AudioPort ,
lv2:InputPort ; lv2:InputPort ;
lv2:index 4 ; lv2:index 5 ;
lv2:symbol "in" ; lv2:symbol "in" ;
lv2:name "In" lv2:name "In"
], [ ], [
a lv2:AudioPort , a lv2:AudioPort ,
lv2:OutputPort ; lv2:OutputPort ;
lv2:index 5 ; lv2:index 6 ;
lv2:symbol "out" ; lv2:symbol "out" ;
lv2:name "Out" lv2:name "Out"
],[ ],[
@@ -150,7 +161,7 @@ tone stack used in Polytone and HiWatt amps.
atom:bufferType atom:Sequence ; atom:bufferType atom:Sequence ;
# atom:supports patch:Message; # atom:supports patch:Message;
lv2:designation lv2:control ; lv2:designation lv2:control ;
lv2:index 6 ; lv2:index 7 ;
lv2:symbol "control" ; lv2:symbol "control" ;
lv2:name "Control" ; lv2:name "Control" ;
rdfs:comment "Plugin to GUI communication" ; rdfs:comment "Plugin to GUI communication" ;
@@ -160,7 +171,7 @@ tone stack used in Polytone and HiWatt amps.
atom:bufferType atom:Sequence ; atom:bufferType atom:Sequence ;
# atom:supports patch:Message; # atom:supports patch:Message;
lv2:designation lv2:control ; lv2:designation lv2:control ;
lv2:index 7 ; lv2:index 8 ;
lv2:symbol "notify" ; lv2:symbol "notify" ;
lv2:name "Notify" ; lv2:name "Notify" ;
rdfs:comment "Plugin to GUI communication" ; rdfs:comment "Plugin to GUI communication" ;
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
rdfs:comment """ rdfs:comment """
Emulation of a Boss CE-2 Chorus. Emulation of a Boss CE-2 Chorus.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
rdfs:comment """ rdfs:comment """
A straightforward no-frills digital delay. A straightforward no-frills digital delay.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
rdfs:comment """ rdfs:comment """
Emulation of a Boss BF-2 Flanger, based on circuit analysis and simulation of the original. Emulation of a Boss BF-2 Flanger, based on circuit analysis and simulation of the original.
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
rdfs:comment """ rdfs:comment """
Digital emulation of a Boss BF-2 Flanger. Digital emulation of a Boss BF-2 Flanger.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
rdfs:comment """ 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. Toob Freeverb is an Lv2 implementation of the famous Freeverb reverb effect. FreeVerb delivers a well-balanced reverb with very little tonal coloration.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
rdfs:comment """ rdfs:comment """
A 7 octave graphic equalizer, with frequencies chosen to be useful for EQ-ing guitar signals. A 7 octave graphic equalizer, with frequencies chosen to be useful for EQ-ing guitar signals.
""" ; """ ;
+1 -1
View File
@@ -92,7 +92,7 @@ myprefix:output_group
doap:license <https://opensource.org/license/mit/> ; doap:license <https://opensource.org/license/mit/> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 1 ; lv2:minorVersion 1 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
ui:ui <http://two-play.com/plugins/toob-looper-four-ui>; ui:ui <http://two-play.com/plugins/toob-looper-four-ui>;
+1 -1
View File
@@ -78,7 +78,7 @@ myprefix:output_group
doap:license <https://opensource.org/license/mit/> ; doap:license <https://opensource.org/license/mit/> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 1 ; lv2:minorVersion 1 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
ui:ui <http://two-play.com/plugins/toob-looper-one-ui>; ui:ui <http://two-play.com/plugins/toob-looper-one-ui>;
+1 -1
View File
@@ -67,7 +67,7 @@ toobml:sagGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
rdfs:comment """ rdfs:comment """
The TooB ML Amplifier plugin provides emulation of a variety of amplifiers and overdrive pedals that are implemented 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. using neural-network-based machine learning models of real amplifiers.
+3 -3
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
rdfs:comment """ rdfs:comment """
Remix a stereo input signal. Remix a stereo input signal.
@@ -57,7 +57,7 @@ Remix a stereo input signal.
lv2:index 0 ; lv2:index 0 ;
lv2:symbol "trimL" ; lv2:symbol "trimL" ;
lv2:name "Trim L"; lv2:name "Vol L";
lv2:default 0.0 ; lv2:default 0.0 ;
lv2:minimum -60.0 ; lv2:minimum -60.0 ;
@@ -89,7 +89,7 @@ Remix a stereo input signal.
lv2:index 2 ; lv2:index 2 ;
lv2:symbol "trimR" ; lv2:symbol "trimR" ;
lv2:name "Trim R"; lv2:name "Vol R";
lv2:default 0.0 ; lv2:default 0.0 ;
lv2:minimum -60.0 ; lv2:minimum -60.0 ;
@@ -61,33 +61,43 @@ toobNam:eqGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
rdfs:comment """ rdfs:comment """
A port of Steven Atkinson's Neural Amp Modeler to LV2. 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 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
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, 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
you can then select the uploaded file in the browser directly. 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. 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 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. 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 TooB Neural Amp Modeler uses code from the NeuralAmp Modeler Core project (https://www.neuralampmodeler.com/). The TooB team wishes
making this extraordinary technology available as open-source code. 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 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+1 -1
View File
@@ -54,7 +54,7 @@ noisegate:envelope_group
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
rdfs:comment """ rdfs:comment """
A noise gate is an audio processing tool that controls the volume of an audio signal 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. by allowing it to pass through only when it exceeds a set threshold.
+1 -1
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
rdfs:comment """ rdfs:comment """
A loose emulation of an MXR® Phase 90 Phaser. A loose emulation of an MXR® Phase 90 Phaser.
+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 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 ;
]
.
+7 -8
View File
@@ -21,7 +21,6 @@
@prefix urid: <http://lv2plug.in/ns/ext/urid#> . @prefix urid: <http://lv2plug.in/ns/ext/urid#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> . @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ui: <http://lv2plug.in/ns/extensions/ui#> . @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#> . @prefix pipedal_ui: <http://github.com/rerdavies/pipedal/ui#> .
@@ -52,7 +51,7 @@ recordPrefix:audioFile
doap:license <https://opensource.org/license/mit/> ; doap:license <https://opensource.org/license/mit/> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 1 ; lv2:minorVersion 1 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
ui:ui <http://two-play.com/plugins/toob-record-mono-ui>; ui:ui <http://two-play.com/plugins/toob-record-mono-ui>;
@@ -61,7 +60,7 @@ recordPrefix:audioFile
patch:writable patch:writable
recordPrefix:audioFile; recordPrefix:audioFile;
lv2:extensionData state:interface ; lv2:extensionData state:interface, work:interface ;
rdfs:comment """ rdfs:comment """
Record the plugin's audio input signal to a monophonic file. See also: TooB Record Input (stereo), which records a stereo file. 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:default 0.0 ;
lv2:minimum 0.0; lv2:minimum 0.0;
lv2:maximum 1.0; lv2:maximum 1.0;
lv2:portProperty lv2:toggled,pprop:trigger; lv2:portProperty lv2:toggled,epp:trigger;
], ],
[ [
a lv2:InputPort , 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:default 0.0 ;
lv2:minimum 0.0; lv2:minimum 0.0;
lv2:maximum 1.0; lv2:maximum 1.0;
lv2:portProperty lv2:toggled,pprop:trigger; lv2:portProperty lv2:toggled,epp:trigger;
], ],
[ [
a lv2:OutputPort , 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:index 3;
lv2:symbol "play" ; lv2:symbol "play" ;
lv2:name ""; lv2:name "";
rdfs:comment "Preview recorded file. Click again to stop."; rdfs:comment "Preview recorded file.";
lv2:default 0.0 ; lv2:default 0.0 ;
lv2:minimum 0.0; lv2:minimum 0.0;
lv2:maximum 1.0; lv2:maximum 1.0;
lv2:portProperty lv2:toggled,pprop:trigger; lv2:portProperty lv2:toggled,epp:trigger;
], ],
[ [
a lv2:OutputPort , 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:index 7;
lv2:symbol "level" ; lv2:symbol "level" ;
lv2:name "Level"; lv2:name "Rec Lvl";
rdfs:comment "Input trim level for recording"; rdfs:comment "Input trim level for recording";
lv2:default 0.0 ; lv2:default 0.0 ;
lv2:minimum -60.0; lv2:minimum -60.0;
+5 -5
View File
@@ -88,7 +88,7 @@ myprefix:loop3_group
doap:license <https://opensource.org/license/mit/> ; doap:license <https://opensource.org/license/mit/> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 1 ; lv2:minorVersion 1 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
ui:ui <http://two-play.com/plugins/toob-record-stereo-ui>; ui:ui <http://two-play.com/plugins/toob-record-stereo-ui>;
@@ -97,7 +97,7 @@ myprefix:loop3_group
patch:writable patch:writable
recordPrefix:audioFile; recordPrefix:audioFile;
lv2:extensionData state:interface ; lv2:extensionData state:interface, work:interface ;
rdfs:comment """ 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:index 3;
lv2:symbol "play" ; lv2:symbol "play" ;
lv2:name ""; lv2:name "";
rdfs:comment "Preview the recorded file. Click again to stop."; rdfs:comment "Preview the recorded file.";
lv2:default 0.0 ; lv2:default 0.0 ;
lv2:minimum 0.0; lv2:minimum 0.0;
lv2:maximum 1.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:index 7;
lv2:symbol "level" ; lv2:symbol "level" ;
lv2:name "Level"; lv2:name "Rec Lvl";
lv2:name "Input trim level for recording"; rdfs:comment "Input trim level for recording";
lv2:default 0.0 ; lv2:default 0.0 ;
lv2:minimum -60.0; lv2:minimum -60.0;
lv2:maximum 30.0; lv2:maximum 30.0;
+1 -1
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
rdfs:comment """ rdfs:comment """
TooB Tuner is a chromatic guitar tuner. TooB Tuner is a chromatic guitar tuner.
""" ; """ ;
+2 -2
View File
@@ -31,7 +31,7 @@
<http://two-play.com/plugins/toob-volume> <http://two-play.com/plugins/toob-volume>
a lv2:Plugin , a lv2:Plugin ,
lv2:UtilityPlugin ; lv2:MixerPlugin ;
doap:name "TooB Volume" , doap:name "TooB Volume" ,
"TooB Volume"@en-gb-gb "TooB Volume"@en-gb-gb
; ;
@@ -39,7 +39,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 62 ; lv2:microVersion 63 ;
rdfs:comment """ rdfs:comment """
Volume control. Volume control.
+4
View File
@@ -97,6 +97,10 @@
lv2:binary <ToobAmp.so> ; lv2:binary <ToobAmp.so> ;
rdfs:seeAlso <ToobMix.ttl> . 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 ; <http://two-play.com/plugins/toob-graphiceq> a lv2:Plugin ;
lv2:binary <ToobAmp.so> ; lv2:binary <ToobAmp.so> ;
rdfs:seeAlso <ToobGraphicEq.ttl> . rdfs:seeAlso <ToobGraphicEq.ttl> .
+48 -301
View File
@@ -144,8 +144,6 @@ namespace pipedal
return false; return false;
} }
struct AudioFormat struct AudioFormat
{ {
char name[40]; char name[40];
@@ -304,12 +302,9 @@ namespace pipedal
AlsaDriverImpl(AudioDriverHost *driverHost) AlsaDriverImpl(AudioDriverHost *driverHost)
: driverHost(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); 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() virtual ~AlsaDriverImpl()
{ {
@@ -415,14 +410,7 @@ namespace pipedal
snd_pcm_sw_params_free(playbackSwParams); snd_pcm_sw_params_free(playbackSwParams);
playbackSwParams = nullptr; playbackSwParams = nullptr;
} }
for (auto &midiState : this->midiDevices) this->alsaSequencer = nullptr;
{
if (midiState)
{
midiState->Close();
}
}
midiDevices.resize(0);
} }
std::string discover_alsa_using_apps() std::string discover_alsa_using_apps()
@@ -913,7 +901,7 @@ namespace pipedal
std::vector<float *> &buffers = this->playbackBuffers; std::vector<float *> &buffers = this->playbackBuffers;
int channels = this->playbackChannels; int channels = this->playbackChannels;
constexpr double scale = 0x00FFFFFF; constexpr double scale = 0x00FFFFFF;
for (size_t frame = 0; frame < frames; ++frame) for (size_t frame = 0; frame < frames; ++frame)
{ {
for (int channel = 0; channel < channels; ++channel) for (int channel = 0; channel < channels; ++channel)
@@ -1084,7 +1072,6 @@ namespace pipedal
open = true; open = true;
try try
{ {
OpenMidi(jackServerSettings, channelSelection);
OpenAudio(jackServerSettings, channelSelection); OpenAudio(jackServerSettings, channelSelection);
std::atomic_thread_fence(std::memory_order::release); std::atomic_thread_fence(std::memory_order::release);
} }
@@ -1531,18 +1518,43 @@ namespace pipedal
} while (frames > 0); } while (frames > 0);
return framesRead; return framesRead;
} }
protected:
void ReadMidiData(uint32_t audioFrame) 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( size_t messageSize = message.size;
this->midiEvents, if (messageSize == 0)
midiEventCount, {
audioFrame); continue;
midiEventCount += nRead; }
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) long WriteBuffer(snd_pcm_t *handle, uint8_t *buf, size_t frames)
{ {
@@ -1671,16 +1683,17 @@ namespace pipedal
pBuffer[j] = 0; pBuffer[j] = 0;
} }
} }
try { try
{
while (!terminateAudio()) while (!terminateAudio())
{ {
std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::this_thread::sleep_for(std::chrono::milliseconds(10));
// zero out input buffers. // zero out input buffers.
this->driverHost->OnProcess(this->bufferSize); this->driverHost->OnProcess(this->bufferSize);
} }
} catch (const std::exception &e) }
catch (const std::exception &e)
{ {
} }
} }
this->driverHost->OnAudioTerminated(); this->driverHost->OnAudioTerminated();
@@ -1766,288 +1779,22 @@ namespace pipedal
Lv2Log::debug("Audio thread joined."); Lv2Log::debug("Audio thread joined.");
} }
static constexpr size_t MAX_MIDI_EVENT_SIZE = 3; static constexpr size_t MIDI_MEMORY_BUFFER_SIZE = 32 * 1024;
static constexpr size_t MIDI_BUFFER_SIZE = 16 * 1024;
static constexpr size_t MAX_MIDI_EVENT = 4 * 1024; static constexpr size_t MAX_MIDI_EVENT = 4 * 1024;
size_t midiEventCount = 0; size_t midiEventCount = 0;
std::vector<MidiEvent> midiEvents; std::vector<MidiEvent> midiEvents;
size_t midiEventMemoryIndex = 0;
std::vector<uint8_t> midiEventMemory; std::vector<uint8_t> midiEventMemory;
AlsaSequencer::ptr alsaSequencer;
public: public:
class AlsaMidiDeviceImpl
virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override
{ {
private: this->alsaSequencer = alsaSequencer;
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<uint8_t> 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<MidiEvent> *pInputEventBuffer = nullptr;
size_t ReadMidiEvents(
std::vector<MidiEvent> &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<std::unique_ptr<AlsaMidiDeviceImpl>> 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<AlsaMidiDeviceImpl>();
midiDevice->Open(device);
midiDevices.push_back(std::move(midiDevice));
}
catch (const std::exception &e)
{
Lv2Log::error(e.what());
}
}
} }
virtual size_t InputBufferCount() const { return activeCaptureBuffers.size(); } virtual size_t InputBufferCount() const { return activeCaptureBuffers.size(); }
+2 -2
View File
@@ -25,7 +25,7 @@
#include "JackConfiguration.hpp" #include "JackConfiguration.hpp"
#include <functional> #include <functional>
#include "AlsaSequencer.hpp"
@@ -74,7 +74,7 @@ namespace pipedal {
virtual float*GetOutputBuffer(size_t channe) = 0; virtual float*GetOutputBuffer(size_t channe) = 0;
virtual void Open(const JackServerSettings & jackServerSettings,const JackChannelSelection &channelSelection) = 0; virtual void Open(const JackServerSettings & jackServerSettings,const JackChannelSelection &channelSelection) = 0;
virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) = 0;
virtual void Activate() = 0; virtual void Activate() = 0;
virtual void Deactivate() = 0; virtual void Deactivate() = 0;
virtual void Close() = 0; virtual void Close() = 0;
+4 -2
View File
@@ -33,6 +33,7 @@
#include "Lv2Log.hpp" #include "Lv2Log.hpp"
#include "ss.hpp" #include "ss.hpp"
#include "util.hpp" #include "util.hpp"
#include <algorithm>
#undef _GLIBCXX_DEBUG // Ensure we are not in debug mode, as this file is not compatible with it. #undef _GLIBCXX_DEBUG // Ensure we are not in debug mode, as this file is not compatible with it.
#include "SQLiteCpp/SQLiteCpp.h" #include "SQLiteCpp/SQLiteCpp.h"
@@ -112,8 +113,9 @@ namespace
static int64_t fileTimeToInt64(const fs::file_time_type &fileTime) static int64_t fileTimeToInt64(const fs::file_time_type &fileTime)
{ {
std::chrono::system_clock::time_point system_time = std::chrono::clock_cast<std::chrono::system_clock>(fileTime); auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
auto result = std::chrono::duration_cast<std::chrono::milliseconds>(system_time.time_since_epoch()).count(); fileTime - fs::file_time_type::clock::now() + std::chrono::system_clock::now());
auto result = std::chrono::duration_cast<std::chrono::milliseconds>(sctp.time_since_epoch()).count();
return result; return result;
} }
static int64_t GetLastWriteTime(const fs::path &file) static int64_t GetLastWriteTime(const fs::path &file)
+45 -3
View File
@@ -21,6 +21,7 @@
#include "util.hpp" #include "util.hpp"
#include <lv2/atom/atom.h> #include <lv2/atom/atom.h>
#include "SchedulerPriority.hpp" #include "SchedulerPriority.hpp"
#include "AlsaSequencer.hpp"
#include "Lv2Log.hpp" #include "Lv2Log.hpp"
@@ -388,6 +389,9 @@ bool SystemMidiBinding::IsMatch(const MidiEvent &event)
class AudioHostImpl : public AudioHost, private AudioDriverHost, private IPatchWriterCallback class AudioHostImpl : public AudioHost, private AudioDriverHost, private IPatchWriterCallback
{ {
private: private:
AlsaSequencer::ptr alsaSequencer;
AlsaSequencerDeviceMonitor::ptr alsaDeviceMonitor;
void OnWritePatchPropertyBuffer( void OnWritePatchPropertyBuffer(
PatchPropertyWriter::Buffer *); PatchPropertyWriter::Buffer *);
@@ -529,7 +533,7 @@ private:
std::string GetAtomObjectType(uint8_t *pData) 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) if (pAtom->atom.type != uris.atom_Object)
{ {
throw std::invalid_argument("Not an Lv2 Object"); throw std::invalid_argument("Not an Lv2 Object");
@@ -1248,12 +1252,26 @@ public:
lv2_atom_forge_init(&inputWriterForge, pHost->GetMapFeature().GetMap()); lv2_atom_forge_init(&inputWriterForge, pHost->GetMapFeature().GetMap());
cpuTemperatureMonitor = CpuTemperatureMonitor::Get(); 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() virtual ~AudioHostImpl()
{ {
alsaDeviceMonitor->StopMonitoring();
Close(); Close();
CleanRestartThreads(true); CleanRestartThreads(true);
audioDriver = nullptr; audioDriver = nullptr;
this->alsaSequencer = nullptr;
} }
virtual JackConfiguration GetServerConfiguration() virtual JackConfiguration GetServerConfiguration()
@@ -1268,7 +1286,21 @@ public:
{ {
return this->sampleRate; 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() void HandleAudioTerminatedAbnormally()
{ {
Lv2Log::error("Audio processing terminated unexpectedly."); Lv2Log::error("Audio processing terminated unexpectedly.");
@@ -1666,7 +1698,7 @@ public:
try try
{ {
audioDriver->Open(jackServerSettings, this->channelSelection); audioDriver->Open(jackServerSettings, this->channelSelection);
audioDriver->SetAlsaSequencer(this->alsaSequencer);
this->sampleRate = audioDriver->GetSampleRate(); this->sampleRate = audioDriver->GetSampleRate();
this->overrunGracePeriodSamples = (uint64_t)(((uint64_t)this->sampleRate) * OVERRUN_GRACE_PERIOD_S); 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( void OnNotifyPathPatchPropertyReceived(
int64_t instanceId, int64_t instanceId,
const std::string &pathPatchPropertyUri, const std::string &pathPatchPropertyUri,
@@ -2261,6 +2296,13 @@ void AudioHostImpl::OnWritePatchPropertyBuffer(
this->realtimeWriter.SendPathPropertyBuffer(buffer); this->realtimeWriter.SendPathPropertyBuffer(buffer);
} }
void AudioHostImpl::SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration)
{
this->alsaSequencer->SetConfiguration(alsaSequencerConfiguration);
}
JSON_MAP_BEGIN(JackHostStatus) JSON_MAP_BEGIN(JackHostStatus)
JSON_MAP_REFERENCE(JackHostStatus, active) JSON_MAP_REFERENCE(JackHostStatus, active)
JSON_MAP_REFERENCE(JackHostStatus, errorMessage) JSON_MAP_REFERENCE(JackHostStatus, errorMessage)
+4
View File
@@ -39,6 +39,7 @@ namespace pipedal
struct RealtimeNextMidiProgramRequest; struct RealtimeNextMidiProgramRequest;
class PluginHost; class PluginHost;
class Pedalboard; class Pedalboard;
class AlsaSequencerConfiguration;
using PortMonitorCallback = std::function<void(int64_t handle, float value)>; using PortMonitorCallback = std::function<void(int64_t handle, float value)>;
@@ -178,6 +179,8 @@ namespace pipedal
virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex,int64_t snapshotRequestId) = 0; virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex,int64_t snapshotRequestId) = 0;
virtual void OnAlsaDriverTerminatedAbnormally() = 0; virtual void OnAlsaDriverTerminatedAbnormally() = 0;
virtual void OnAlsaSequencerDeviceAdded(int client, const std::string &clientName) = 0;
virtual void OnAlsaSequencerDeviceRemoved(int client) = 0;
}; };
class JackHostStatus class JackHostStatus
@@ -225,6 +228,7 @@ namespace pipedal
virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) = 0; virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) = 0;
virtual void Close() = 0; virtual void Close() = 0;
virtual void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0;
virtual uint32_t GetSampleRate() = 0; virtual uint32_t GetSampleRate() = 0;
virtual JackConfiguration GetServerConfiguration() = 0; virtual JackConfiguration GetServerConfiguration() = 0;
+25 -6
View File
@@ -9,9 +9,19 @@ set (ENABLE_BACKTRACE 0)
set (USE_SANITIZE OFF) # seems to be broken on Ubuntu 24.10 set (USE_SANITIZE OFF) # seems to be broken on Ubuntu 24.10
set(CXX_STANDARD 20) 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) include(FetchContent)
@@ -196,6 +206,7 @@ else()
endif() endif()
set (PIPEDAL_SOURCES set (PIPEDAL_SOURCES
PipewireInputStream.cpp PipewireInputStream.hpp
AudioFiles.cpp AudioFiles.hpp AudioFiles.cpp AudioFiles.hpp
AudioFileMetadataReader.cpp AudioFileMetadataReader.hpp AudioFileMetadataReader.cpp AudioFileMetadataReader.hpp
AudioFileMetadata.hpp AudioFileMetadata.cpp AudioFileMetadata.hpp AudioFileMetadata.cpp
@@ -217,7 +228,6 @@ set (PIPEDAL_SOURCES
Lv2PluginChangeMonitor.cpp Lv2PluginChangeMonitor.hpp Lv2PluginChangeMonitor.cpp Lv2PluginChangeMonitor.hpp
WebServerConfig.cpp WebServerConfig.hpp WebServerConfig.cpp WebServerConfig.hpp
Locale.hpp Locale.cpp Locale.hpp Locale.cpp
Finally.hpp
ZipFile.cpp ZipFile.hpp ZipFile.cpp ZipFile.hpp
TemporaryFile.cpp TemporaryFile.hpp TemporaryFile.cpp TemporaryFile.hpp
FilePropertyDirectoryTree.cpp FilePropertyDirectoryTree.hpp FilePropertyDirectoryTree.cpp FilePropertyDirectoryTree.hpp
@@ -314,6 +324,7 @@ set (PIPEDAL_INCLUDES
${JACK_INCLUDE_DIRS} ${LILV_0_INCLUDE_DIRS} ${JACK_INCLUDE_DIRS} ${LILV_0_INCLUDE_DIRS}
${VST3_INCLUDES} ${VST3_INCLUDES}
${WEBSOCKETPP_INCLUDE_DIRS} ${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 pthread atomic stdc++fs asound avahi-common avahi-client systemd
${VST3_LIBRARIES} ${VST3_LIBRARIES}
${LILV_0_LIBRARIES} ${LILV_0_LIBRARIES}
asound
# ${JACK_LIBRARIES} - pending delete for JACK support. # ${JACK_LIBRARIES} - pending delete for JACK support.
) )
@@ -332,13 +344,17 @@ set(PIPEDAL_LIBS libpipedald zip
add_library(libpipedald STATIC ${PIPEDAL_SOURCES}) 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 target_link_libraries(libpipedald
PUBLIC PUBLIC
PiPedalCommon PiPedalCommon
SQLiteCpp) SQLiteCpp
${PipeWire_LIBRARIES}
)
if(${USE_PCH}) if(${USE_PCH})
target_precompile_headers(libpipedald PRIVATE pch.h) target_precompile_headers(libpipedald PRIVATE pch.h)
@@ -360,6 +376,7 @@ target_link_libraries(pipedal_kconfig
PRIVATE ftxui::dom PRIVATE ftxui::dom
PRIVATE ftxui::component # Not needed for this example. PRIVATE ftxui::component # Not needed for this example.
PRIVATE PiPedalCommon PRIVATE PiPedalCommon
asound
systemd systemd
) )
@@ -378,6 +395,7 @@ target_link_libraries(pipedald PRIVATE PiPedalCommon
${PIPEDAL_LIBS} ${PIPEDAL_LIBS}
) )
################################# #################################
add_executable(hotspotManagerTest add_executable(hotspotManagerTest
hotspotManagerTestMain.cpp hotspotManagerTestMain.cpp
@@ -385,7 +403,7 @@ add_executable(hotspotManagerTest
target_link_libraries(hotspotManagerTest PRIVATE ${PIPEDAL_LIBS}) 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 add_executable(AuxInTest
@@ -396,6 +414,7 @@ add_executable(AuxInTest
add_executable(pipedaltest add_executable(pipedaltest
testMain.cpp testMain.cpp
PipewireInputStreamTest.cpp
AudioFilesTest.cpp AudioFilesTest.cpp
LRUCacheTest.cpp LRUCacheTest.cpp
@@ -426,7 +445,7 @@ target_link_libraries(pipedaltest PRIVATE ${PIPEDAL_LIBS} ${ICU_LIBRARIES})
target_include_directories(pipedaltest PRIVATE ${PIPEDAL_INCLUDES} 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. 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} 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) target_link_libraries(jsonTest PRIVATE pthread)
+66 -7
View File
@@ -104,6 +104,11 @@ namespace pipedal
captureChannels = channels; captureChannels = channels;
playbackChannels = channels; playbackChannels = channels;
midiEventMemoryIndex = 0;
midiEventMemory.resize(MIDI_MEMORY_BUFFER_SIZE);
midiEvents.resize(MAX_MIDI_EVENT);
} }
virtual ~DummyDriverImpl() virtual ~DummyDriverImpl()
{ {
@@ -122,6 +127,16 @@ namespace pipedal
} }
JackServerSettings jackServerSettings; 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<MidiEvent> midiEvents;
size_t midiEventMemoryIndex = 0;
std::vector<uint8_t> midiEventMemory;
unsigned int periods = 0; unsigned int periods = 0;
@@ -158,11 +173,13 @@ namespace pipedal
} }
} }
virtual size_t GetMidiInputEventCount() override { virtual size_t GetMidiInputEventCount() override
return 0; {
return midiEventCount;
} }
virtual MidiEvent*GetMidiEvents() { virtual MidiEvent *GetMidiEvents() override
return nullptr; {
return this->midiEvents.data();
} }
@@ -190,7 +207,11 @@ namespace pipedal
throw; throw;
} }
} }
virtual std::string GetConfigurationDescription() virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override
{
this->alsaSequencer = alsaSequencer;
}
virtual std::string GetConfigurationDescription()
{ {
std::string result = SS( std::string result = SS(
"DUMMY, " "DUMMY, "
@@ -219,6 +240,43 @@ namespace pipedal
bool block = false; 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() void AudioThread()
{ {
SetThreadName("dummyAudioDriver"); SetThreadName("dummyAudioDriver");
@@ -231,11 +289,14 @@ namespace pipedal
while (true) while (true)
{ {
if (terminateAudio()) if (terminateAudio())
{ {
break; break;
} }
ReadMidiData((uint32_t)0);
ssize_t framesRead = this->bufferSize; ssize_t framesRead = this->bufferSize;
this->driverHost->OnProcess(framesRead); this->driverHost->OnProcess(framesRead);
@@ -335,8 +396,6 @@ namespace pipedal
Lv2Log::debug("Audio thread joined."); Lv2Log::debug("Audio thread joined.");
} }
static constexpr size_t MIDI_BUFFER_SIZE = 16 * 1024;
static constexpr size_t MAX_MIDI_EVENT = 4 * 1024;
public: public:
+38 -19
View File
@@ -80,7 +80,7 @@ namespace pipedal::impl
bool hasWifi = false; bool hasWifi = false;
HasWifiListener hasWifiListener; HasWifiListener hasWifiListener;
void SetHasWifi(bool hasWifi); void SetHasWifi(bool hasWifi);
virtual bool GetHasWifi() override ; virtual bool GetHasWifi() override;
void onClose(); void onClose();
void onError(const std::string &message); void onError(const std::string &message);
@@ -347,7 +347,7 @@ void HotspotManagerImpl::onStartMonitoring()
this->networkManager = NetworkManager::Create(dbusDispatcher); this->networkManager = NetworkManager::Create(dbusDispatcher);
if (!networkManager->WirelessEnabled()) if (!networkManager->WirelessEnabled() && wifiConfigSettings.NeedsWifi())
{ {
networkManager->WirelessEnabled(true); networkManager->WirelessEnabled(true);
} }
@@ -483,6 +483,7 @@ void HotspotManagerImpl::onReload()
// force a reload. // force a reload.
StopHotspot(); StopHotspot();
SetState(State::Monitoring); SetState(State::Monitoring);
StartScanTimer(); // or stop it as the case may be.
MaybeStartHotspot(); MaybeStartHotspot();
return; return;
} }
@@ -758,16 +759,26 @@ void HotspotManagerImpl::MaybeStartHotspot()
// devices are transitioning. Do nothing. // devices are transitioning. Do nothing.
return; return;
} }
std::vector<AccessPoint::ptr> allAccessPoints = GetAllAccessPoints();
std::vector<std::vector<uint8_t>> allAccessPointSsids = GetAccessPointSsids(allAccessPoints);
std::vector<std::vector<uint8_t>> 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<AccessPoint::ptr> allAccessPoints = GetAllAccessPoints();
std::vector<std::vector<uint8_t>> allAccessPointSsids = GetAccessPointSsids(allAccessPoints);
std::vector<std::vector<uint8_t>> 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) if (this->state == State::Monitoring && wantsHotspot)
{ {
StartHotspot(); StartHotspot();
@@ -996,8 +1007,14 @@ void HotspotManagerImpl::ScanNow()
try try
{ {
Lv2Log::debug("Scanning"); if (this->wifiConfigSettings.NeedsScan())
wlanWirelessDevice->RequestScan(options); {
Lv2Log::debug("Scanning");
wlanWirelessDevice->RequestScan(options);
} else {
return;
}
} }
catch (const std::exception &e) catch (const std::exception &e)
{ {
@@ -1036,7 +1053,8 @@ void HotspotManagerImpl::SetHasWifiListener(HasWifiListener &&listener)
this->hasWifiListener(this->hasWifi); this->hasWifiListener(this->hasWifi);
} }
} }
bool HotspotManagerImpl::GetHasWifi() { bool HotspotManagerImpl::GetHasWifi()
{
std::lock_guard<std::recursive_mutex> lock{this->networkChangingListenerMutex}; std::lock_guard<std::recursive_mutex> lock{this->networkChangingListenerMutex};
return hasWifi; return hasWifi;
} }
@@ -1112,17 +1130,20 @@ static bool IsNetworkManagerRunning()
return gUsingNetworkManager; return gUsingNetworkManager;
} }
bool bResult = false; bool bResult = false;
auto result = sysExecForOutput("systemctl","is-active NetworkManager"); auto result = sysExecForOutput("systemctl", "is-active NetworkManager");
if (result.exitCode == EXIT_SUCCESS) 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") if (text == "active")
{ {
bResult = true; bResult = true;
} else if (text == "inactive") }
else if (text == "inactive")
{ {
bResult = false; bResult = false;
} else { }
else
{
throw std::runtime_error(SS("UsingNetworkManager: unexpected result (" << text << ")")); throw std::runtime_error(SS("UsingNetworkManager: unexpected result (" << text << ")"));
} }
gNetworkManagerTestExecuted = true; gNetworkManagerTestExecuted = true;
@@ -1134,19 +1155,17 @@ static bool IsNetworkManagerRunning()
return false; return false;
} }
bool HotspotManager::HasWifiDevice() bool HotspotManager::HasWifiDevice()
{ {
// use procfs to decide this, as NetworkManager may not be available yet. // 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; return false;
} }
if (!IsNetworkManagerRunning()) if (!IsNetworkManagerRunning())
{ {
return false; return false;
} }
return true; return true;
} }
+19 -1
View File
@@ -23,6 +23,7 @@
#include "AlsaDriver.hpp" #include "AlsaDriver.hpp"
#include "Lv2Log.hpp" #include "Lv2Log.hpp"
#include "PiPedalException.hpp" #include "PiPedalException.hpp"
#include "AlsaSequencer.hpp"
#if JACK_HOST #if JACK_HOST
@@ -106,6 +107,23 @@ namespace pipedal {
#endif #endif
} }
static std::vector<AlsaMidiDeviceInfo> GetAlsaSequencers() {
std::vector<AlsaMidiDeviceInfo> result;
try {
auto sequencer = AlsaSequencer::Create();
if (sequencer)
{
std::vector<AlsaSequencerPort> 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( void JackConfiguration::AlsaInitialize(
const JackServerSettings &jackServerSettings) const JackServerSettings &jackServerSettings)
{ {
@@ -116,7 +134,7 @@ void JackConfiguration::AlsaInitialize(
{ {
this->inputMidiDevices_.clear(); this->inputMidiDevices_.clear();
} else { } else {
this->inputMidiDevices_ = GetAlsaMidiInputDevices(); this->inputMidiDevices_ = GetAlsaSequencers(); // NB: Sequencers, not rawmidi devices, anymore.
} }
if (jackServerSettings.IsValid()) if (jackServerSettings.IsValid())
{ {
+7 -1
View File
@@ -107,7 +107,13 @@ namespace pipedal
return outputAudioPorts_; return outputAudioPorts_;
} }
const std::vector<AlsaMidiDeviceInfo>& GetInputMidiDevices() const // replaced with AlsaSequencerConfiguration
const std::vector<AlsaMidiDeviceInfo>& LegacyGetInputMidiDevices() const
{
return inputMidiDevices_;
}
// replaced with AlsaSequencerConfiguration
std::vector<AlsaMidiDeviceInfo>& LegacyGetInputMidiDevices()
{ {
return inputMidiDevices_; return inputMidiDevices_;
} }
+217 -5
View File
@@ -47,6 +47,7 @@ void PiPedalAlsaDevices::cacheDevice(const std::string &name, const AlsaDeviceIn
std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices() std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
{ {
std::lock_guard guard{alsaMutex}; std::lock_guard guard{alsaMutex};
std::vector<AlsaDeviceInfo> result; std::vector<AlsaDeviceInfo> result;
@@ -118,7 +119,7 @@ std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
if (err == 0) if (err == 0)
{ {
unsigned int minRate = 0, maxRate = 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; int dir;
err = snd_pcm_hw_params_get_rate_min(params, &minRate, &dir); err = snd_pcm_hw_params_get_rate_min(params, &minRate, &dir);
if (err == 0) if (err == 0)
@@ -143,7 +144,8 @@ std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
info.sampleRates_.push_back(rate); info.sampleRates_.push_back(rate);
} }
} }
if (minBufferSize < 16) { if (minBufferSize < 16)
{
minBufferSize = 16; minBufferSize = 16;
} }
@@ -180,7 +182,207 @@ std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
return result; 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; std::vector<AlsaMidiDeviceInfo> result;
{ {
@@ -214,6 +416,8 @@ static std::vector<AlsaMidiDeviceInfo> GetAlsaDevices(const char *devname, const
result.push_back(AlsaMidiDeviceInfo(name, desc)); result.push_back(AlsaMidiDeviceInfo(name, desc));
} }
} }
// translate rawmidi device name to device number.
if (name && strcmp("null", name) != 0) if (name && strcmp("null", name) != 0)
free(name); free(name);
if (desc && strcmp("null", desc) != 0) if (desc && strcmp("null", desc) != 0)
@@ -229,11 +433,11 @@ static std::vector<AlsaMidiDeviceInfo> GetAlsaDevices(const char *devname, const
return result; return result;
} }
std::vector<AlsaMidiDeviceInfo> pipedal::GetAlsaMidiInputDevices() std::vector<AlsaMidiDeviceInfo> pipedal::LegacyGetAlsaMidiInputDevices()
{ {
return GetAlsaDevices("rawmidi", "Input"); return GetAlsaDevices("rawmidi", "Input");
} }
std::vector<AlsaMidiDeviceInfo> pipedal::GetAlsaMidiOutputDevices() std::vector<AlsaMidiDeviceInfo> pipedal::LegacyGetAlsaMidiOutputDevices()
{ {
return GetAlsaDevices("rawmidi", "Output"); 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_BEGIN(AlsaDeviceInfo)
JSON_MAP_REFERENCE(AlsaDeviceInfo, cardId) JSON_MAP_REFERENCE(AlsaDeviceInfo, cardId)
JSON_MAP_REFERENCE(AlsaDeviceInfo, id) JSON_MAP_REFERENCE(AlsaDeviceInfo, id)
+20 -2
View File
@@ -41,11 +41,26 @@ namespace pipedal {
}; };
class AlsaMidiDeviceInfo { class AlsaMidiDeviceInfo {
public: public:
enum Direction {
None = 0,
In = 1,
Out = 2,
InOut = 3
};
AlsaMidiDeviceInfo() { } AlsaMidiDeviceInfo() { }
AlsaMidiDeviceInfo(const char*name, const char*description); 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 name_;
std::string description_; 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); DECLARE_JSON_MAP(AlsaMidiDeviceInfo);
}; };
@@ -60,6 +75,9 @@ namespace pipedal {
std::vector<AlsaDeviceInfo> GetAlsaDevices(); std::vector<AlsaDeviceInfo> GetAlsaDevices();
}; };
std::vector<AlsaMidiDeviceInfo> GetAlsaMidiInputDevices(); // we use ALSA sequencers now instead of ALSA rawmidi devices.
std::vector<AlsaMidiDeviceInfo> GetAlsaMidiOutputDevices(); // Used by test suite to verify migration behaviour.
std::vector<AlsaMidiDeviceInfo> LegacyGetAlsaMidiInputDevices();
std::vector<AlsaMidiDeviceInfo> LegacyGetAlsaMidiOutputDevices();
} }
+39 -10
View File
@@ -38,10 +38,8 @@ static void DiscoveryTest()
auto result = devices.GetAlsaDevices(); auto result = devices.GetAlsaDevices();
std::cout << result.size() << " ALSA devices found." << std::endl; std::cout << result.size() << " ALSA devices found." << std::endl;
auto midiInputDevices = GetAlsaMidiInputDevices(); auto midiInputDevices = AlsaSequencer::EnumeratePorts();
std::cout << midiInputDevices.size() << " ALSA MIDI input devices found." << std::endl; std::cout << midiInputDevices.size() << " ALSA MIDI input sequencers found." << std::endl;
auto midiOutputDevices = GetAlsaMidiOutputDevices();
std::cout << midiOutputDevices.size() << " ALSA MIDI output devices found." << std::endl;
} }
void EnumerateSequencers() void EnumerateSequencers()
@@ -86,27 +84,58 @@ void EnumerateSequencers()
void ReadFromsequencerTest() void ReadFromsequencerTest()
{ {
cout << "--- Reading from ALSA Sequencer" << endl; cout << "--- Reading from ALSA Sequencer" << endl;
AlsaSequencer sequencer; AlsaSequencer::ptr sequencer = AlsaSequencer::Create();
sequencer.ConnectPort("V25 V25 In");
sequencer->ConnectPort("V25 V25 In");
AlsaMidiMessage message; AlsaMidiMessage message;
while (true) while (true)
{ {
if (sequencer.ReadMessage(message)) if (sequencer->ReadMessage(message,true))
{ {
cout << "Received MIDI message: " cout << "Received MIDI message: "
<< " " << hex << setfill('0') << setw(2) << static_cast<int>(message.cc0) << " " << hex << setfill('0') << setw(2) << static_cast<int>(message.cc0())
<< " " << hex << setfill('0') << setw(2) << static_cast<int>(message.cc1) << " " << hex << setfill('0') << setw(2) << static_cast<int>(message.cc1())
<< " " << hex << setfill('0') << setw(2) << static_cast<int>(message.cc2) << " " << hex << setfill('0') << setw(2) << static_cast<int>(message.cc2())
<< " Timestamp: " << setw(8) << dec << message.timestamp << " Timestamp: " << setw(8) << dec << message.timestamp
<< std::endl; << 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]") 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(); EnumerateSequencers();
TestConfigMigration();
ReadFromsequencerTest(); ReadFromsequencerTest();
} }
+161 -70
View File
@@ -296,6 +296,8 @@ void PiPedalModel::Load()
this->audioHost->SetSystemMidiBindings(this->systemMidiBindings); this->audioHost->SetSystemMidiBindings(this->systemMidiBindings);
audioHost->SetAlsaSequencerConfiguration(storage.GetAlsaSequencerConfiguration());
if (configuration.GetMLock()) if (configuration.GetMLock())
{ {
#ifndef NO_MLOCK #ifndef NO_MLOCK
@@ -509,7 +511,6 @@ void PiPedalModel::FireBanksChanged(int64_t clientId)
} }
} }
void PiPedalModel::FirePedalboardChanged(int64_t clientId, bool loadAudioThread) void PiPedalModel::FirePedalboardChanged(int64_t clientId, bool loadAudioThread)
{ {
if (loadAudioThread) if (loadAudioThread)
@@ -595,14 +596,11 @@ void PiPedalModel::UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalbo
UpdateVst3Settings(pedalboard); UpdateVst3Settings(pedalboard);
Lv2PedalboardErrorList errorMessages; Lv2PedalboardErrorList errorMessages;
std::shared_ptr<Lv2Pedalboard> lv2Pedalboard{ std::shared_ptr<Lv2Pedalboard> lv2Pedalboard{
this->pluginHost.UpdateLv2PedalboardStructure(pedalboard, this->lv2Pedalboard.get(), errorMessages) this->pluginHost.UpdateLv2PedalboardStructure(pedalboard, this->lv2Pedalboard.get(), errorMessages)};
};
this->lv2Pedalboard = lv2Pedalboard; this->lv2Pedalboard = lv2Pedalboard;
// apply the error messages to the lv2Pedalboard. // apply the error messages to the lv2Pedalboard.
// return true if the error messages have changed // return true if the error messages have changed
audioHost->SetPedalboard(lv2Pedalboard); audioHost->SetPedalboard(lv2Pedalboard);
@@ -614,9 +612,7 @@ void PiPedalModel::UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalbo
UpdateRealtimeVuSubscriptions(); UpdateRealtimeVuSubscriptions();
UpdateRealtimeMonitorPortSubscriptions(); UpdateRealtimeMonitorPortSubscriptions();
this->FirePedalboardChanged(clientId, false);
this->FirePedalboardChanged(clientId,false);
this->SetPresetChanged(clientId, true); 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<std::recursive_mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> 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<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnAlsaSequencerConfigurationChanged(alsaSequencerConfiguration);
}
}
}
AlsaSequencerConfiguration PiPedalModel::GetAlsaSequencerConfiguration()
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return this->storage.GetAlsaSequencerConfiguration();
}
std::vector<AlsaSequencerPortSelection> PiPedalModel::GetAlsaSequencerPorts()
{
auto ports = AlsaSequencer::EnumeratePorts();
std::vector<AlsaSequencerPortSelection> 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) void PiPedalModel::SetJackChannelSelection(int64_t clientId, const JackChannelSelection &channelSelection)
{ {
{ {
@@ -1618,7 +1685,7 @@ void PiPedalModel::SendSetPatchProperty(
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
if (!audioHost) if (!audioHost)
{ {
onError("Audio not running."); onError("Audio not running.");
return; return;
} }
@@ -1629,8 +1696,9 @@ void PiPedalModel::SendSetPatchProperty(
std::shared_ptr<Lv2PluginInfo> pluginInfo = GetPluginInfo(pedalboardItem->uri_); std::shared_ptr<Lv2PluginInfo> pluginInfo = GetPluginInfo(pedalboardItem->uri_);
auto pipedalUi = pluginInfo->piPedalUI(); auto pipedalUi = pluginInfo->piPedalUI();
auto fileProperty = pipedalUi->GetFileProperty(propertyUri); auto fileProperty = pipedalUi->GetFileProperty(propertyUri);
if (fileProperty && value.is_string()) { if (fileProperty && value.is_string())
{
json_variant abstractPath = pluginHost.AbstractPath(value); json_variant abstractPath = pluginHost.AbstractPath(value);
std::ostringstream ss; std::ostringstream ss;
json_writer writer(ss); json_writer writer(ss);
@@ -1680,12 +1748,11 @@ void PiPedalModel::SendSetPatchProperty(
}}; }};
LV2_URID urid = this->pluginHost.GetLv2Urid(propertyUri.c_str()); 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( RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest(
onRequestComplete, onRequestComplete,
clientId, instanceId, urid, atomValue, nullptr, onError, clientId, instanceId, urid, atomValue, nullptr, onError,
sampleTimeout sampleTimeout);
);
outstandingParameterRequests.push_back(request); outstandingParameterRequests.push_back(request);
if (this->audioHost) if (this->audioHost)
@@ -1753,10 +1820,10 @@ void PiPedalModel::SendGetPatchProperty(
{ {
onError("Audio stopped."); onError("Audio stopped.");
} }
size_t sampleTimeout = 0.3*audioHost->GetSampleRate(); size_t sampleTimeout = 0.3 * audioHost->GetSampleRate();
RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest( RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest(
onRequestComplete, onRequestComplete,
clientId, instanceId, urid, onSuccess, onError,sampleTimeout); clientId, instanceId, urid, onSuccess, onError, sampleTimeout);
outstandingParameterRequests.push_back(request); outstandingParameterRequests.push_back(request);
this->audioHost->sendRealtimeParameterRequest(request); this->audioHost->sendRealtimeParameterRequest(request);
@@ -1920,7 +1987,9 @@ void PiPedalModel::UpdateDefaults(PedalboardItem *pedalboardItem, std::unordered
{ {
pedalboardItem->midiChannelBinding(MidiChannelBinding::DefaultForMissingValue()); pedalboardItem->midiChannelBinding(MidiChannelBinding::DefaultForMissingValue());
} }
} else { }
else
{
if (pedalboardItem->midiChannelBinding()) if (pedalboardItem->midiChannelBinding())
{ {
pedalboardItem->midiChannelBinding(std::optional<MidiChannelBinding>()); // clear it. pedalboardItem->midiChannelBinding(std::optional<MidiChannelBinding>()); // clear it.
@@ -2196,22 +2265,23 @@ void PiPedalModel::OnNotifyMidiListen(uint8_t cc0, uint8_t cc1, uint8_t cc2)
{ {
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
bool isNote = (cc0 & 0xF0) == 0x90; // Note On bool isNote = (cc0 & 0xF0) == 0x90; // Note On
if (isNote && cc2 == 0) { if (isNote && cc2 == 0)
{
return; // Note off. Oopsie. return; // Note off. Oopsie.
} }
bool isControl = (cc0 & 0xF0) == 0xB0; // Control Change bool isControl = (cc0 & 0xF0) == 0xB0; // Control Change
if (!isNote && !isControl) { if (!isNote && !isControl)
{
return; // Not a note on or control change. 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 &listener = midiEventListeners[i];
auto subscriber = this->GetNotificationSubscriber(listener.clientId); auto subscriber = this->GetNotificationSubscriber(listener.clientId);
if (subscriber) if (subscriber)
{ {
subscriber->OnNotifyMidiListener(listener.clientHandle, cc0,cc1,cc2); subscriber->OnNotifyMidiListener(listener.clientHandle, cc0, cc1, cc2);
} }
else else
{ {
@@ -2296,6 +2366,7 @@ void PiPedalModel::CancelMonitorPatchProperty(int64_t clientId, int64_t clientHa
audioHost->SetListenForMidiEvent(false); audioHost->SetListenForMidiEvent(false);
} }
} }
std::vector<AlsaDeviceInfo> PiPedalModel::GetAlsaDevices() std::vector<AlsaDeviceInfo> PiPedalModel::GetAlsaDevices()
{ {
std::vector<AlsaDeviceInfo> result = this->alsaDevices.GetAlsaDevices(); std::vector<AlsaDeviceInfo> result = this->alsaDevices.GetAlsaDevices();
@@ -2353,9 +2424,9 @@ void PiPedalModel::SetSystemMidiBindings(std::vector<MidiBinding> &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())) if (pedalboardItem->pathProperties_.contains(fileProperty.patchProperty()))
{ {
@@ -2369,25 +2440,24 @@ FileRequestResult PiPedalModel::GetFileList2(const std::string &relativePath_, c
std::string relativePath = relativePath_; std::string relativePath = relativePath_;
try 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. // 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); PedalboardItem *pedalboardItem = GetPedalboardItemForFileProperty(fileProperty);
if (pedalboardItem) if (pedalboardItem)
{ {
auto pluginInfo = GetPluginInfo(pedalboardItem->uri()); auto pluginInfo = GetPluginInfo(pedalboardItem->uri());
if (pluginInfo) { if (pluginInfo)
std::filesystem::path resourcePath = fs::path(pluginInfo->bundle_path()) {
/ fileProperty.resourceDirectory(); std::filesystem::path resourcePath = fs::path(pluginInfo->bundle_path()) / fileProperty.resourceDirectory();
if (IsSubdirectory(relativePath,resourcePath)) if (IsSubdirectory(relativePath, resourcePath))
{ {
fs::path t = MakeRelativePath(relativePath,resourcePath); fs::path t = MakeRelativePath(relativePath, resourcePath);
t = fileProperty.directory() / t; t = fileProperty.directory() / t;
if (fs::exists(t)) if (fs::exists(t))
{ {
relativePath = t; relativePath = t;
} }
@@ -2420,10 +2490,9 @@ std::string PiPedalModel::CopyFilePropertyFile(
bool overwrite) bool overwrite)
{ {
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
return storage.CopyFilePropertyFile(oldRelativePath, newRelativePath, uiFileProperty,overwrite); return storage.CopyFilePropertyFile(oldRelativePath, newRelativePath, uiFileProperty, overwrite);
} }
void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName) void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName)
{ {
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
@@ -2435,28 +2504,29 @@ std::string PiPedalModel::CreateNewSampleDirectory(const std::string &relativePa
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
return storage.CreateNewSampleDirectory(relativePath, uiFileProperty); 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<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> 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(); auto pedalboardItems = pedalboard.GetAllPlugins();
for (const auto&pedalboardItem: pedalboardItems) { for (const auto &pedalboardItem : pedalboardItems)
{
if (pedalboardItem->instanceId() == instanceId) if (pedalboardItem->instanceId() == instanceId)
{ {
Lv2PluginInfo::ptr pluginInfo = GetPluginInfo(pedalboardItem->uri()); Lv2PluginInfo::ptr pluginInfo = GetPluginInfo(pedalboardItem->uri());
if (pluginInfo && pluginInfo->piPedalUI()) if (pluginInfo && pluginInfo->piPedalUI())
{ {
for (const auto&fileProperty: pluginInfo->piPedalUI()->fileProperties()) for (const auto &fileProperty : pluginInfo->piPedalUI()->fileProperties())
if (fileProperty->patchProperty() == patchPropertyUri) if (fileProperty->patchProperty() == patchPropertyUri)
{ {
return fileProperty; return fileProperty;
} }
} }
} }
} }
@@ -2464,9 +2534,9 @@ UiFileProperty::ptr PiPedalModel::FindLoadedPatchProperty(int64_t instanceId,con
throw std::runtime_error("Permission denied. Plugin not currently loaded."); 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) if (!fileProperty)
{ {
Lv2Log::error(SS("Upload fle: Permission denied. No currently-loaded plugin provides that patch property: " << patchProperty)); 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; return hasWifi;
} }
std::map<std::string,std::string> PiPedalModel::GetWifiRegulatoryDomains() std::map<std::string, std::string> PiPedalModel::GetWifiRegulatoryDomains()
{ {
std::map<std::string,std::string> result; std::map<std::string, std::string> result;
try { try
auto& regDb = RegDb::GetInstance(); {
auto &regDb = RegDb::GetInstance();
result = regDb.getRegulatoryDomains(storage.GetConfigRoot() / "iso_codes.json"); 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())); Lv2Log::warning(SS("Unable to query Wifi Regulatory domains. " << e.what()));
} }
return result; return result;
} }
void PiPedalModel::CancelAudioRetry() { void PiPedalModel::CancelAudioRetry()
{
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
if (audioRetryPostHandle) if (audioRetryPostHandle)
{ {
// don't think this can ever happen, but if it did, it would be bad. // don't think this can ever happen, but if it did, it would be bad.
this->CancelPost(audioRetryPostHandle); this->CancelPost(audioRetryPostHandle);
audioRetryPostHandle = 0; 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. // ALSA stream has broken. We want to restart.
// get off the service thread as promptly as possible // get off the service thread as promptly as possible
this->Post([&]() { this->Post([&]()
{
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
if (closed) return; if (closed) return;
@@ -2932,8 +3007,7 @@ void PiPedalModel::OnAlsaDriverTerminatedAbnormally() {
} else { } else {
Lv2Log::error(SS("Unable to reastart audio.")); Lv2Log::error(SS("Unable to reastart audio."));
} } });
});
} }
bool PiPedalModel::IsInUploadsDirectory(const std::string &path) bool PiPedalModel::IsInUploadsDirectory(const std::string &path)
@@ -2943,26 +3017,43 @@ bool PiPedalModel::IsInUploadsDirectory(const std::string &path)
void PiPedalModel::MoveAudioFile( void PiPedalModel::MoveAudioFile(
const std::string &directory, const std::string &directory,
int32_t fromPosition, int32_t fromPosition,
int32_t toPosition int32_t toPosition)
) { {
if (directory.empty()) if (directory.empty())
{ {
throw std::runtime_error("Directory is empty."); throw std::runtime_error("Directory is empty.");
} }
AudioDirectoryInfo::Ptr dir = AudioDirectoryInfo::Create(directory); 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) void PiPedalModel::SetPedalboardItemTitle(int64_t instanceId, const std::string &title)
{ {
std::lock_guard<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
if (!this->pedalboard.SetItemTitle(instanceId,title)) if (!this->pedalboard.SetItemTitle(instanceId, title))
{ {
return; return;
} }
// no need to reload the pedalboard, but we do need to notify subscribers. // no need to reload the pedalboard, but we do need to notify subscribers.
this->SetPresetChanged(-1, true); this->SetPresetChanged(-1, true);
this->FirePedalboardChanged(-1,false); this->FirePedalboardChanged(-1, false);
}
void PiPedalModel::SetTone3000Auth(const std::string &apiKey)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
storage.SetTone3000Auth(apiKey);
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
bool hasAuth = apiKey != "";
for (auto &subscriber : t)
{
subscriber->OnTone3000AuthChanged(hasAuth);
}
}
bool PiPedalModel::HasTone3000Auth() const
{
return storage.GetTone3000Auth() != "";
} }
+15
View File
@@ -94,7 +94,9 @@ namespace pipedal
virtual void OnNetworkChanging(bool hotspotConnected) = 0; virtual void OnNetworkChanging(bool hotspotConnected) = 0;
virtual void OnHasWifiChanged(bool hasWifi) = 0; virtual void OnHasWifiChanged(bool hasWifi) = 0;
virtual void OnAlsaSequencerConfigurationChanged(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0;
virtual void Close() = 0; virtual void Close() = 0;
virtual void OnTone3000AuthChanged(bool value) = 0;
}; };
@@ -168,6 +170,7 @@ namespace pipedal
std::vector<AtomOutputListener> atomOutputListeners; std::vector<AtomOutputListener> atomOutputListeners;
JackServerSettings jackServerSettings; JackServerSettings jackServerSettings;
PluginHost pluginHost; PluginHost pluginHost;
AtomConverter atomConverter; // must be AFTER pluginHost! AtomConverter atomConverter; // must be AFTER pluginHost!
@@ -238,6 +241,8 @@ namespace pipedal
virtual void OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) override; virtual void OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) override;
virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex,int64_t snapshotRequestId) override; virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex,int64_t snapshotRequestId) override;
virtual void OnAlsaDriverTerminatedAbnormally() override; virtual void OnAlsaDriverTerminatedAbnormally() override;
virtual void OnAlsaSequencerDeviceAdded(int client, const std::string &clientName) override;
virtual void OnAlsaSequencerDeviceRemoved(int client) override;
void OnNotifyPathPatchPropertyReceived( void OnNotifyPathPatchPropertyReceived(
int64_t instanceId, int64_t instanceId,
@@ -262,6 +267,7 @@ namespace pipedal
void CheckForResourceInitialization(Pedalboard &pedalboard); void CheckForResourceInitialization(Pedalboard &pedalboard);
UiFileProperty::ptr FindLoadedPatchProperty(int64_t instanceId,const std::string&patchPropertyUri); UiFileProperty::ptr FindLoadedPatchProperty(int64_t instanceId,const std::string&patchPropertyUri);
public: public:
PiPedalModel(); PiPedalModel();
virtual ~PiPedalModel(); virtual ~PiPedalModel();
@@ -387,6 +393,11 @@ namespace pipedal
void SetJackChannelSelection(int64_t clientId, const JackChannelSelection &channelSelection); void SetJackChannelSelection(int64_t clientId, const JackChannelSelection &channelSelection);
JackChannelSelection GetJackChannelSelection(); JackChannelSelection GetJackChannelSelection();
void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration);
AlsaSequencerConfiguration GetAlsaSequencerConfiguration();
std::vector<AlsaSequencerPortSelection> GetAlsaSequencerPorts();
void SetShowStatusMonitor(bool show); void SetShowStatusMonitor(bool show);
bool GetShowStatusMonitor(); bool GetShowStatusMonitor();
@@ -470,6 +481,10 @@ namespace pipedal
int32_t to); int32_t to);
void SetPedalboardItemTitle(int64_t instanceId, const std::string &title); void SetPedalboardItemTitle(int64_t instanceId, const std::string &title);
void SetTone3000Auth(const std::string &apiKey);
bool HasTone3000Auth() const;
}; };
} // namespace pipedal. } // namespace pipedal.
+29
View File
@@ -1416,6 +1416,10 @@ public:
pReader->read(&instanceId); pReader->read(&instanceId);
uint64_t result = model.DeleteBank(this->clientId, instanceId); uint64_t result = model.DeleteBank(this->clientId, instanceId);
this->Reply(replyTo, "deleteBankItem", result); this->Reply(replyTo, "deleteBankItem", result);
} else if (message == "getHasTone3000Auth")
{
bool result = model.HasTone3000Auth();
this->Reply(replyTo, "getHasTone3000Auth", result);
} }
else if (message == "renameBank") else if (message == "renameBank")
{ {
@@ -1730,6 +1734,22 @@ public:
{ {
auto regulatoryDomains = this->model.GetWifiRegulatoryDomains(); auto regulatoryDomains = this->model.GetWifiRegulatoryDomains();
this->Reply(replyTo, "getWifiRegulatoryDomains", regulatoryDomains); 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<AlsaSequencerPortSelection> result = model.GetAlsaSequencerPorts();
this->Reply(replyTo,"getAlsaSequencerPorts", result);
} }
else else
{ {
@@ -1831,6 +1851,11 @@ private:
Flush(); Flush();
} }
virtual void OnAlsaSequencerConfigurationChanged(const AlsaSequencerConfiguration &alsaSequencerConfiguration) override
{
Send("onAlsaSequencerConfigurationChanged", alsaSequencerConfiguration);
}
virtual void OnNetworkChanging(bool hotspotConnected) override virtual void OnNetworkChanging(bool hotspotConnected) override
{ {
try try
@@ -1842,6 +1867,10 @@ private:
{ {
} }
} }
virtual void OnTone3000AuthChanged(bool value)
{
Send("onTone3000AuthChanged", value);
}
virtual void OnErrorMessage(const std::string &message) virtual void OnErrorMessage(const std::string &message)
{ {
+244
View File
@@ -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 <thread>
#include <atomic>
extern "C"
{
#include <pipewire/pipewire.h>
#include <spa/param/audio/format-utils.h>
#include <spa/param/props.h>
}
#include <stdexcept>
#include <string>
using namespace pipedal;
namespace pipedal::impl
{
class PipeWireInputStreamImpl : public PipeWireInputStream
{
private:
pw_main_loop *loop = nullptr;
pw_stream *stream = nullptr;
Callback callback;
std::atomic<bool> 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<std::jthread> serviceThread;
virtual void Activate(Callback &&callback) override
{
serviceThread = std::make_unique<std::jthread>([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<PipeWireInputStreamImpl>(streamName, channels, sampleRate);
}
+50
View File
@@ -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 <memory>
#include <string>
#include <functional>
namespace pipedal
{
class PipeWireInputStream
{
protected:
PipeWireInputStream() {}
public:
virtual ~PipeWireInputStream() {}
using self = PipeWireInputStream;
using ptr = std::shared_ptr<self>;
using Callback = std::function<void(const float *data, size_t size)>;
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;
};
}
+61
View File
@@ -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 <sstream>
#include <cstdint>
#include <string>
#include <iostream>
#include "PipewireInputStream.hpp"
#include "PiPedalAlsa.hpp"
#include <atomic>
#include <thread>
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<size_t> frameCount = 0;
std::atomic<size_t> *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));
}
}
+157 -4
View File
@@ -22,6 +22,7 @@
#include "Storage.hpp" #include "Storage.hpp"
#include "AudioConfig.hpp" #include "AudioConfig.hpp"
#include "PiPedalException.hpp" #include "PiPedalException.hpp"
#include "AlsaSequencer.hpp"
#include <stdexcept> #include <stdexcept>
#include <sstream> #include <sstream>
#include "json.hpp" #include "json.hpp"
@@ -253,6 +254,7 @@ void Storage::Initialize()
LoadPluginPresetIndex(); LoadPluginPresetIndex();
LoadBankIndex(); LoadBankIndex();
LoadCurrentBank(); LoadCurrentBank();
LoadTone3000Auth();
try try
{ {
LoadChannelSelection(); LoadChannelSelection();
@@ -260,6 +262,8 @@ void Storage::Initialize()
catch (const std::exception &) catch (const std::exception &)
{ {
} }
LoadAlsaSequencerConfiguration();
LoadWifiConfigSettings(); LoadWifiConfigSettings();
LoadWifiDirectConfigSettings(); LoadWifiDirectConfigSettings();
LoadUserSettings(); LoadUserSettings();
@@ -308,10 +312,19 @@ std::filesystem::path Storage::GetCurrentPresetPath() const
return this->dataRoot / "currentPreset.json"; return this->dataRoot / "currentPreset.json";
} }
std::filesystem::path Storage::GetTone3000AuthPath() const
{
return this->dataRoot / "tone3000.json";
}
std::filesystem::path Storage::GetChannelSelectionFileName() std::filesystem::path Storage::GetChannelSelectionFileName()
{ {
return this->dataRoot / "JackChannelSelection.json"; return this->dataRoot / "JackChannelSelection.json";
} }
std::filesystem::path Storage::GetAlsaSequencerConfigurationFileName()
{
return this->dataRoot / "MidiDevices.json";
}
std::filesystem::path Storage::GetIndexFileName() const std::filesystem::path Storage::GetIndexFileName() const
{ {
return this->GetPresetsDirectory() / BANKS_FILENAME; return this->GetPresetsDirectory() / BANKS_FILENAME;
@@ -734,6 +747,103 @@ JackChannelSelection Storage::GetJackChannelSelection(const JackConfiguration &j
return jackChannelSelection.RemoveInvalidChannels(jackConfiguration); return jackChannelSelection.RemoveInvalidChannels(jackConfiguration);
} }
static AlsaSequencerConfiguration MigrateRawMidiToAlsaSequencer(std::vector<AlsaMidiDeviceInfo> &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() void Storage::LoadChannelSelection()
{ {
auto fileName = this->GetChannelSelectionFileName(); auto fileName = this->GetChannelSelectionFileName();
@@ -749,7 +859,6 @@ void Storage::LoadChannelSelection()
catch (const std::exception &e) catch (const std::exception &e)
{ {
Lv2Log::error("I/O error reading %s: %s", fileName.c_str(), e.what()); 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(); auto fileName = this->GetChannelSelectionFileName();
try try
{ {
// replaced with AlsaSequencerConfiguration. Delete legacy data.
this->jackChannelSelection.LegacyGetInputMidiDevices().clear();
pipedal::ofstream_synced s(fileName); pipedal::ofstream_synced s(fileName);
json_writer writer(s, false); json_writer writer(s, false);
writer.write(this->jackChannelSelection); writer.write(this->jackChannelSelection);
@@ -1745,12 +1856,14 @@ static void AddTracksToResult(
} }
const auto &path = dir_entry.path(); const auto &path = dir_entry.path();
auto name = path.filename().string(); auto name = path.filename().string();
try { try
{
if (dir_entry.is_directory()) if (dir_entry.is_directory())
{ {
resultFiles.push_back(FileEntry{path, name, true, dir_entry.is_symlink()}); 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())); Lv2Log::warning(SS("Failed to add directory entry: " << path.string() << " - " << e.what()));
} }
@@ -1799,7 +1912,6 @@ static void AddTracksToResult(
throw std::logic_error( throw std::logic_error(
SS("AddTracksToResult failed to enumerate audio files. " << rootPath.string() << " - " << error.what())); SS("AddTracksToResult failed to enumerate audio files. " << rootPath.string() << " - " << error.what()));
} }
} }
catch (const std::exception &error) catch (const std::exception &error)
{ {
@@ -2498,6 +2610,47 @@ const PluginPresetIndex &Storage::GetPluginPresetIndex()
return pluginPresetIndex; 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_BEGIN(UserSettings)
JSON_MAP_REFERENCE(UserSettings, governor) JSON_MAP_REFERENCE(UserSettings, governor)
JSON_MAP_REFERENCE(UserSettings, showStatusMonitor) JSON_MAP_REFERENCE(UserSettings, showStatusMonitor)
+21
View File
@@ -31,6 +31,7 @@
#include "FileEntry.hpp" #include "FileEntry.hpp"
#include <map> #include <map>
#include "FilePropertyDirectoryTree.hpp" #include "FilePropertyDirectoryTree.hpp"
#include "AlsaSequencer.hpp"
namespace pipedal { namespace pipedal {
@@ -72,6 +73,7 @@ private:
BankIndex bankIndex; BankIndex bankIndex;
BankFile currentBank; BankFile currentBank;
PluginPresetIndex pluginPresetIndex; PluginPresetIndex pluginPresetIndex;
std::string tone3000Auth;
private: private:
void FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std::filesystem::path&directory) const; void FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std::filesystem::path&directory) const;
@@ -84,7 +86,9 @@ private:
std::filesystem::path GetIndexFileName() const; std::filesystem::path GetIndexFileName() const;
std::filesystem::path GetBankFileName(const std::string & name) const; std::filesystem::path GetBankFileName(const std::string & name) const;
std::filesystem::path GetChannelSelectionFileName(); std::filesystem::path GetChannelSelectionFileName();
std::filesystem::path GetAlsaSequencerConfigurationFileName();
std::filesystem::path GetCurrentPresetPath() const; std::filesystem::path GetCurrentPresetPath() const;
std::filesystem::path GetTone3000AuthPath() const;
void LoadBankIndex(); void LoadBankIndex();
void SaveBankIndex(); void SaveBankIndex();
@@ -94,11 +98,19 @@ private:
void LoadChannelSelection(); void LoadChannelSelection();
void SaveChannelSelection(); void SaveChannelSelection();
void LoadAlsaSequencerConfiguration();
void SaveAlsaSequencerConfiguration();
void SaveBankFile(const std::string& name,const BankFile&bankFile); void SaveBankFile(const std::string& name,const BankFile&bankFile);
void LoadBankFile(const std::string &name,BankFile *pBank); void LoadBankFile(const std::string &name,BankFile *pBank);
std::string GetPresetCopyName(const std::string &name); std::string GetPresetCopyName(const std::string &name);
bool isJackChannelSelectionValid = false; bool isJackChannelSelectionValid = false;
JackChannelSelection jackChannelSelection; JackChannelSelection jackChannelSelection;
AlsaSequencerConfiguration alsaSequencerConfiguration;;
WifiConfigSettings wifiConfigSettings; WifiConfigSettings wifiConfigSettings;
WifiDirectConfigSettings wifiDirectConfigSettings; WifiDirectConfigSettings wifiDirectConfigSettings;
@@ -126,6 +138,7 @@ public:
void LoadWifiDirectConfigSettings(); void LoadWifiDirectConfigSettings();
void LoadUserSettings(); void LoadUserSettings();
void SaveUserSettings(); void SaveUserSettings();
void LoadBank(int64_t instanceId); void LoadBank(int64_t instanceId);
int64_t GetBankByMidiBankNumber(uint8_t bankNumber); int64_t GetBankByMidiBankNumber(uint8_t bankNumber);
const Pedalboard& GetCurrentPreset(); const Pedalboard& GetCurrentPreset();
@@ -181,6 +194,10 @@ public:
void SaveCurrentPreset(const CurrentPreset &currentPreset); void SaveCurrentPreset(const CurrentPreset &currentPreset);
bool RestoreCurrentPreset(CurrentPreset*pResult); bool RestoreCurrentPreset(CurrentPreset*pResult);
void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration);
AlsaSequencerConfiguration GetAlsaSequencerConfiguration() const;
//std::string MapPropertyFileName(Lv2PluginInfo*pluginInfo, const std::string&path); //std::string MapPropertyFileName(Lv2PluginInfo*pluginInfo, const std::string&path);
private: private:
@@ -239,6 +256,10 @@ public:
const UiFileProperty&uiFileProperty, const UiFileProperty&uiFileProperty,
bool overwrite = false); bool overwrite = false);
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty,const std::filesystem::path&selectedPath); FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty,const std::filesystem::path&selectedPath);
void LoadTone3000Auth();
void SetTone3000Auth(const std::string&apiKey);
std::string GetTone3000Auth() const;
}; };
+15 -1
View File
@@ -188,6 +188,10 @@ public:
{ {
return true; return true;
} }
else if (segment == "Tone3000Auth")
{
return true;
}
else if (segment == "PluginPresets") else if (segment == "PluginPresets")
{ {
return true; return true;
@@ -734,7 +738,17 @@ public:
{ {
std::string segment = request_uri.segment(1); 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; PluginPresets presets;
fs::path filePath = req.get_body_temporary_file(); fs::path filePath = req.get_body_temporary_file();
+37 -19
View File
@@ -48,8 +48,9 @@
#include "AudioFiles.hpp" #include "AudioFiles.hpp"
#include <systemd/sd-daemon.h> #include <systemd/sd-daemon.h>
#include "AlsaSequencer.hpp"
using namespace pipedal; using namespace pipedal;
#ifdef __ARM_ARCH_ISA_A64 #ifdef __ARM_ARCH_ISA_A64
#define AARCH64 #define AARCH64
@@ -81,9 +82,9 @@ static bool isJackServiceRunning()
return std::filesystem::exists(path); return std::filesystem::exists(path);
} }
#if ENABLE_BACKTRACE #if ENABLE_BACKTRACE
void segvHandler(int sig) { void segvHandler(int sig)
{
void *array[10]; void *array[10];
// Get void*'s for all entries on the stack // Get void*'s for all entries on the stack
@@ -92,26 +93,40 @@ void segvHandler(int sig) {
// Print out all the frames to stderr // Print out all the frames to stderr
const char *message = "Error: SEGV signal received.\n"; 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); _exit(EXIT_FAILURE);
} }
static void EnableBacktrace() static void EnableBacktrace()
{ {
signal(SIGSEGV, segvHandler); signal(SIGSEGV, segvHandler);
} }
#endif #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 == "debug")
if (strLogLevel == "info") {*result= LogLevel::Info; return true;} {
if (strLogLevel == "warning") {*result= LogLevel::Warning; return true;} *result = LogLevel::Debug;
if (strLogLevel == "error") { *result= LogLevel::Error; return true;} 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; *result = LogLevel::Info;
return false; return false;
} }
@@ -140,7 +155,7 @@ int main(int argc, char *argv[])
parser.AddOption("-h", &help); parser.AddOption("-h", &help);
parser.AddOption("--help", &help); parser.AddOption("--help", &help);
parser.AddOption("-systemd", &systemd); parser.AddOption("-systemd", &systemd);
parser.AddOption("-log-level",&logLevel); parser.AddOption("-log-level", &logLevel);
parser.AddOption("-port", &portOption); parser.AddOption("-port", &portOption);
parser.AddOption("-test-extra-device", &testExtraDevice); // advertise two different devices (for testing multi-device connect) 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) if (logLevel.length() != 0)
{ {
LogLevel lv2LogLevel; LogLevel lv2LogLevel;
if (TryGetLogLevel(logLevel,&lv2LogLevel)) if (TryGetLogLevel(logLevel, &lv2LogLevel))
{ {
Lv2Log::log_level(lv2LogLevel); Lv2Log::log_level(lv2LogLevel);
}
} else { else
{
Lv2Log::error(SS("Invalid log level: " << logLevel)); Lv2Log::error(SS("Invalid log level: " << logLevel));
return EXIT_SUCCESS; // indicate to systemd that we don't want a restart. 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. // clean up orphaned temporary files.
const std::filesystem::path webTempDirectory = "/var/pipedal/web_temp"; 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::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. // 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. return EXIT_SUCCESS; // indiate to systemd that we don't want a restart.
} }
try try
{ {
{ {
+8
View File
@@ -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
+53
View File
@@ -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[] = [];
};
+14 -3
View File
@@ -19,11 +19,13 @@
import React from 'react'; import React from 'react';
import { ThemeProvider, createTheme, StyledEngineProvider } from '@mui/material/styles'; import { ThemeProvider, createTheme, StyledEngineProvider } from '@mui/material/styles';
import { CssBaseline } from '@mui/material'; import CssBaseline from '@mui/material/CssBaseline';
import VirtualKeyboardHandler from './VirtualKeyboardHandler'; import VirtualKeyboardHandler from './VirtualKeyboardHandler';
import AppThemed from "./AppThemed"; import AppThemed from "./AppThemed";
import { isDarkMode } from './DarkMode'; import { isDarkMode } from './DarkMode';
import Tone3000AuthComplete from './Tone3000AuthComplete';
declare module '@mui/material/styles' { declare module '@mui/material/styles' {
interface Theme { 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 { const App = (class extends React.Component {
// Before the component mounts, we initialise our state // Before the component mounts, we initialise our state
@@ -240,8 +248,11 @@ const App = (class extends React.Component {
<StyledEngineProvider injectFirst> <StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
{isTone3000Auth() ? (
<AppThemed /> <Tone3000AuthComplete />
) : (
<AppThemed />
)}
</ThemeProvider> </ThemeProvider>
</StyledEngineProvider> </StyledEngineProvider>
); );
+98 -41
View File
@@ -21,6 +21,10 @@
import React from 'react'; import React from 'react';
import { createStyles } from './WithStyles'; 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 DraggableButtonBase from './DraggableButtonBase';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import { Theme } from '@mui/material/styles'; import { Theme } from '@mui/material/styles';
@@ -64,6 +68,8 @@ import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDial
import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata'; import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata';
const ToobNamModelFileUrl = "http://two-play.com/plugins/toob-nam#modelFile";
const AUTOSCROLL_TICK_DELAY = 30; const AUTOSCROLL_TICK_DELAY = 30;
const AUTOSCROLL_THRESHOLD = 48; const AUTOSCROLL_THRESHOLD = 48;
const AUTOSCROLL_SCROLL_PER_SECOND = 500; const AUTOSCROLL_SCROLL_PER_SECOND = 500;
@@ -161,6 +167,8 @@ export interface FilePropertyDialogState {
initialSelection: string; initialSelection: string;
multiSelect: boolean, multiSelect: boolean,
selectedFiles: string[], selectedFiles: string[],
//openTone3000Dialog: boolean,
openTone3000Help: boolean
}; };
@@ -231,7 +239,9 @@ export default withStyles(
copyDialogOpen: false, copyDialogOpen: false,
initialSelection: this.props.selectedFile, initialSelection: this.props.selectedFile,
multiSelect: false, multiSelect: false,
selectedFiles: [] selectedFiles: [],
//openTone3000Dialog: false,
openTone3000Help: false
}; };
this.requestScroll = true; this.requestScroll = true;
} }
@@ -362,7 +372,7 @@ export default withStyles(
dragElementDy: 0, dragElementDy: 0,
from: index, from: index,
to: index, to: index,
lastMousePoint: {...this.longPressStartPoint} lastMousePoint: { ...this.longPressStartPoint }
}; };
this.setState({ dragState: dragState }); 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" }}> style={{ marginLeft: 16, marginBottom: 8, marginRight: 8, textOverflow: "", display: "flex", flexFlow: "row wrap", alignItems: "center", justifyContent: "start" }}>
{breadcrumbs} {breadcrumbs}
</div> </div>
<Divider />
</div> </div>
); );
} }
@@ -998,8 +1007,7 @@ export default withStyles(
let dragElementDy = dragState.lastMousePoint.y - this.longPressStartPoint.y; let dragElementDy = dragState.lastMousePoint.y - this.longPressStartPoint.y;
let originalY = dragState.from * dragState.height; let originalY = dragState.from * dragState.height;
let newY = originalY + dragElementDy; let newY = originalY + dragElementDy;
if (newY < 0) if (newY < 0) {
{
newY = 0; newY = 0;
dragElementDy = -originalY; dragElementDy = -originalY;
} }
@@ -1040,9 +1048,26 @@ export default withStyles(
() => { this.handleAutoScrollTick() }, AUTOSCROLL_TICK_DELAY); () => { this.handleAutoScrollTick() }, AUTOSCROLL_TICK_DELAY);
} }
// handleTone3000Dialog(e: React.MouseEvent<HTMLButtonElement>) {
// e.stopPropagation();
// e.preventDefault();
// this.setState({ openTone3000Dialog: true });
// }
handleTone3000Help(e: React.MouseEvent<HTMLButtonElement>) {
e.stopPropagation();
e.preventDefault();
this.setState({ openTone3000Help: true });
}
render() { render() {
const isTracksDirectory = this.isTracksDirectory(); const isTracksDirectory = this.isTracksDirectory();
const isToobNamModelFile = this.props.fileProperty.patchProperty === ToobNamModelFileUrl;
const classes = withStyles.getClasses(this.props); const classes = withStyles.getClasses(this.props);
let columnWidth = this.state.columnWidth; let columnWidth = this.state.columnWidth;
@@ -1102,7 +1127,7 @@ export default withStyles(
background: this.state.reordering || this.state.multiSelect ? background: this.state.reordering || this.state.multiSelect ?
(isDarkMode() ? "#523" : "#EDF") (isDarkMode() ? "#523" : "#EDF")
: undefined, : undefined,
marginBottom: 16, paddingTop: 0 marginBottom: 8, paddingTop: 0
}} > }} >
{this.state.reordering && ( {this.state.reordering && (
<Toolbar style={{ padding: 0 }}> <Toolbar style={{ padding: 0 }}>
@@ -1257,7 +1282,7 @@ export default withStyles(
</div> </div>
</DialogTitle> </DialogTitle>
<DialogContent style={{ <DialogContent dividers style={{
paddingLeft: 0, paddingRight: 0, paddingTop: 0, colorScheme: (isDarkMode() ? "dark" : "light"), paddingLeft: 0, paddingRight: 0, paddingTop: 0, colorScheme: (isDarkMode() ? "dark" : "light"),
position: "relative" position: "relative"
}} }}
@@ -1284,7 +1309,8 @@ export default withStyles(
ref={(element) => { this.listContainerElementRef = element; this.onMeasureRef(element); }} ref={(element) => { this.listContainerElementRef = element; this.onMeasureRef(element); }}
style={{ style={{
flex: "1 1 100%", display: "flex", flexFlow: "row wrap", 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. (this.state.columns !== 0) && // don't render until we have number of columns derived from layout.
@@ -1461,45 +1487,64 @@ export default withStyles(
</DialogContent> </DialogContent>
{(!this.state.reordering && !this.state.multiSelect) && ( {(!this.state.reordering && !this.state.multiSelect) && (
<> <>
<Divider />
<DialogActions style={{ justifyContent: "stretch" }}> <DialogActions style={{ justifyContent: "stretch" }}>
{this.state.windowWidth > 500 ? ( {this.state.windowWidth > 500 ? (
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}> <div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "column nowrap" }}>
<IconButtonEx style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary" {isToobNamModelFile && (
tooltip="Delete selected file or folder" <div style={{
disabled={!this.state.hasSelection || this.state.selectedFile === "" || protectedItem} display: "flex", flexFlow: "row nowrap", justifyContent: "center",
onClick={() => this.handleDelete()} > alignItems: "center", width: "100%"
<OldDeleteIcon fontSize='small' /> }}>
</IconButtonEx> <Typography variant="body2" >
Download model files from <Link
href="https://www.tone3000.com/search" target="_blank">TONE3000</Link>
</Typography>
<IconButtonEx tooltip="Help"
onClick={(e) => { this.handleTone3000Help(e); }} aria-label="help" edge="end" color="inherit" style={{ opacity: 0.6, marginLeft: 8 }}
>
<HelpOutlineIcon />
</IconButtonEx>
</div>
<ButtonEx tooltip="Upload file" style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileUploadIcon />} )}
onClick={() => { this.setState({ openUploadFileDialog: true }) }} disabled={protectedDirectory}
>
<div>Upload</div>
</ButtonEx>
<ButtonEx tooltip="Download file" style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileDownloadIcon />} <div style={{ flex: "1 1 100%", display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
onClick={() => { this.handleDownloadFile(); }} disabled={protectedDirectory || !this.state.hasFileSelection} <IconButtonEx style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary"
> tooltip="Delete selected file or folder"
<div>Download</div> disabled={!this.state.hasSelection || this.state.selectedFile === "" || protectedItem}
</ButtonEx> onClick={() => this.handleDelete()} >
<OldDeleteIcon fontSize='small' />
</IconButtonEx>
<div style={{ flex: "1 1 auto" }}>&nbsp;</div> <ButtonEx tooltip="Upload file" style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileUploadIcon />}
onClick={() => { this.setState({ openUploadFileDialog: true }) }} disabled={protectedDirectory}
>
<div>Upload</div>
</ButtonEx>
<Button variant="dialogSecondary" onClick={() => { <ButtonEx tooltip="Download file" style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileDownloadIcon />}
this.props.onApply(this.props.fileProperty, this.state.initialSelection); onClick={() => { this.handleDownloadFile(); }} disabled={protectedDirectory || !this.state.hasFileSelection}
this.props.onCancel(); >
}} aria-label="cancel"> <div>Download</div>
Cancel </ButtonEx>
</Button>
<Button variant="dialogPrimary" style={{ flex: "0 0 auto" }}
onClick={() => { this.openSelectedFile(); }}
disabled={(!canSelectFile) <div style={{ flex: "1 1 auto" }}>&nbsp;</div>
} aria-label="select"
> <Button variant="dialogSecondary" onClick={() => {
{okButtonText} this.props.onApply(this.props.fileProperty, this.state.initialSelection);
</Button> this.props.onCancel();
}} aria-label="cancel">
Cancel
</Button>
<Button variant="dialogPrimary" style={{ flex: "0 0 auto" }}
onClick={() => { this.openSelectedFile(); }}
disabled={(!canSelectFile)
} aria-label="select"
>
{okButtonText}
</Button>
</div>
</div> </div>
) : ( ) : (
<div style={{ width: "100%" }}> <div style={{ width: "100%" }}>
@@ -1607,7 +1652,7 @@ export default withStyles(
{ {
this.state.renameDialogOpen && ( this.state.renameDialogOpen && (
<RenameDialog open={this.state.renameDialogOpen} defaultName={this.renameDefaultName()} <RenameDialog open={this.state.renameDialogOpen} defaultName={this.renameDefaultName()}
title="Rename" title="Rename"
onOk={(newName) => { this.setState({ renameDialogOpen: false }); this.onExecuteRename(newName) }} onOk={(newName) => { this.setState({ renameDialogOpen: false }); this.onExecuteRename(newName) }}
onClose={() => { this.setState({ renameDialogOpen: false }); }} onClose={() => { this.setState({ renameDialogOpen: false }); }}
acceptActionName="OK" acceptActionName="OK"
@@ -1645,6 +1690,18 @@ export default withStyles(
) )
) )
} }
{/* {this.state.openTone3000Dialog && (
<Tone3000Dialog
open={this.state.openTone3000Dialog}
onClose={() => this.setState({ openTone3000Dialog: false })}
/>
)} */}
{this.state.openTone3000Help && (
<Tone3000HelpDialog
open={this.state.openTone3000Help}
onClose={() => this.setState({ openTone3000Help: false })}
/>
)}
</DialogEx> </DialogEx>
); );
} }
+4 -4
View File
@@ -36,7 +36,7 @@ import Slider, { SliderProps } from "@mui/material/Slider";
import Checkbox from "@mui/material/Checkbox"; import Checkbox from "@mui/material/Checkbox";
import TimebaseSelectorDialog from "./TimebaseselectorDialog"; import TimebaseSelectorDialog from "./TimebaseselectorDialog";
import Timebase, { TimebaseUnits } from "./Timebase"; import Timebase, { TimebaseUnits } from "./Timebase";
import { useWindowHeight } from "@react-hook/window-size"; import useWindowSize from "./UseWindowSize";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import PlayArrow from "@mui/icons-material/PlayArrow"; import PlayArrow from "@mui/icons-material/PlayArrow";
import StopIcon from '@mui/icons-material/Stop'; 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 [loopEnd, setLoopEnd] = React.useState(props.value.loopEnd);
const height = useWindowHeight(); const [windowSize] = useWindowSize();
const fullScreen = height < 500; const fullScreen = windowSize.height < 500;
function cancelPlaying() { function cancelPlaying() {
props.onCancelPlaying(); props.onCancelPlaying();
@@ -444,7 +444,7 @@ export default function LoopDialog(props: LoopDialogProps) {
open={props.isOpen} open={props.isOpen}
onEnterKey={() => { onEnterKey={() => {
}} }}
fullScreen={height < 500} fullScreen={fullScreen}
sx={{ sx={{
"& .MuiDialog-container": { "& .MuiDialog-container": {
"& .MuiPaper-root": { "& .MuiPaper-root": {
+34
View File
@@ -43,6 +43,7 @@ import { ColorTheme, getColorScheme, setColorScheme } from './DarkMode';
import FilePropertyDirectoryTree from './FilePropertyDirectoryTree'; import FilePropertyDirectoryTree from './FilePropertyDirectoryTree';
import AudioFileMetadata from './AudioFileMetadata'; import AudioFileMetadata from './AudioFileMetadata';
import { pathFileName } from './FileUtils'; import { pathFileName } from './FileUtils';
import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSequencer';
export enum State { export enum State {
@@ -419,6 +420,8 @@ export class PiPedalModel //implements PiPedalModel
webSocket?: PiPedalSocket; webSocket?: PiPedalSocket;
hasTone3000Auth: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
hasWifiDevice: ObservableProperty<boolean> = new ObservableProperty<boolean>(false); hasWifiDevice: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
onSnapshotModified: ObservableEvent<SnapshotModifiedEvent> = new ObservableEvent<SnapshotModifiedEvent>(); onSnapshotModified: ObservableEvent<SnapshotModifiedEvent> = new ObservableEvent<SnapshotModifiedEvent>();
@@ -452,6 +455,7 @@ export class PiPedalModel //implements PiPedalModel
favorites: ObservableProperty<FavoritesList> = new ObservableProperty<FavoritesList>({}); favorites: ObservableProperty<FavoritesList> = new ObservableProperty<FavoritesList>({});
alsaSequencerConfiguration : ObservableProperty<AlsaSequencerConfiguration> = new ObservableProperty<AlsaSequencerConfiguration>(new AlsaSequencerConfiguration());
presets: ObservableProperty<PresetIndex> = new ObservableProperty<PresetIndex> presets: ObservableProperty<PresetIndex> = new ObservableProperty<PresetIndex>
( (
@@ -668,6 +672,9 @@ export class PiPedalModel //implements PiPedalModel
this.handlePluginPresetsChanged(pluginUri); this.handlePluginPresetsChanged(pluginUri);
} else if (message === "onJackConfigurationChanged") { } else if (message === "onJackConfigurationChanged") {
this.jackConfiguration.set(new JackConfiguration().deserialize(body)); this.jackConfiguration.set(new JackConfiguration().deserialize(body));
} else if (message === "onAlsaSequencerConfigurationChanged") {
this.alsaSequencerConfiguration.set(new AlsaSequencerConfiguration().deserialize(body));
} else if (message === "onLoadPluginPreset") { } else if (message === "onLoadPluginPreset") {
let instanceId = body.instanceId as number; let instanceId = body.instanceId as number;
let controlValues = ControlValue.deserializeArray(body.controlValues); let controlValues = ControlValue.deserializeArray(body.controlValues);
@@ -755,6 +762,8 @@ export class PiPedalModel //implements PiPedalModel
} else if (message === "onErrorMessage") { } else if (message === "onErrorMessage") {
this.showAlert(body as string); this.showAlert(body as string);
} else if (message == "onTone3000AuthChanged") {
this.hasTone3000Auth.set(body as boolean);
} }
else if (message === "onLv2PluginsChanging") { else if (message === "onLv2PluginsChanging") {
this.onLv2PluginsChanging(); this.onLv2PluginsChanging();
@@ -1134,6 +1143,12 @@ export class PiPedalModel //implements PiPedalModel
this.jackSettings.set(new JackChannelSelection().deserialize( this.jackSettings.set(new JackChannelSelection().deserialize(
await this.getWebSocket().request<any>("getJackSettings") await this.getWebSocket().request<any>("getJackSettings")
)); ));
this.alsaSequencerConfiguration.set(new AlsaSequencerConfiguration().deserialize(
await this.getWebSocket().request<any>("getAlsaSequencerConfiguration")
));
this.hasTone3000Auth.set(
await this.getWebSocket().request<boolean>("getHasTone3000Auth")
);
this.banks.set(new BankIndex().deserialize(await this.getWebSocket().request<any>("getBankIndex"))); this.banks.set(new BankIndex().deserialize(await this.getWebSocket().request<any>("getBankIndex")));
this.favorites.set(await this.getWebSocket().request<FavoritesList>("getFavorites")); this.favorites.set(await this.getWebSocket().request<FavoritesList>("getFavorites"));
@@ -3193,6 +3208,25 @@ export class PiPedalModel //implements PiPedalModel
// notify the server. // notify the server.
this.webSocket?.send("setPedalboardItemTitle", { instanceId: instanceId, title: title }); this.webSocket?.send("setPedalboardItemTitle", { instanceId: instanceId, title: title });
} }
setAlsaSequencerConfiguration(alsaSequencerConfiguration: AlsaSequencerConfiguration): void {
this.webSocket?.send("setAlsaSequencerConfiguration", alsaSequencerConfiguration);
}
async getAlsaSequencerConfiguration(): Promise<AlsaSequencerConfiguration> {
if (this.webSocket)
{
let result = await this.webSocket.request<any>("getAlsaSequencerConfiguration");
return new AlsaSequencerConfiguration().deserialize(result);
}
throw new Error("No connection.");
}
async getAlsaSequencerPorts(): Promise<AlsaSequencerPortSelection[]> {
if (this.webSocket) {
let result = await this.webSocket.request<AlsaSequencerPortSelection[]>("getAlsaSequencerPorts");
return AlsaSequencerPortSelection.deserialize_array(result);
}
throw new Error("No connection.");
}
}; };
let instance: PiPedalModel | undefined = undefined; let instance: PiPedalModel | undefined = undefined;
+156 -65
View File
@@ -17,105 +17,196 @@
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // 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. // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import React from 'react';
import { useState } from 'react'; import { useState } from 'react';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import List from '@mui/material/List'; import List from '@mui/material/List';
import ListItemButton from '@mui/material/ListItemButton'; import ListItemButton from '@mui/material/ListItemButton';
import DialogTitle from '@mui/material/DialogTitle'; import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions'; import DialogActions from '@mui/material/DialogActions';
import FormControlLabel from '@mui/material/FormControlLabel'; import FormControlLabel from '@mui/material/FormControlLabel';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Checkbox from '@mui/material/Checkbox'; import Checkbox from '@mui/material/Checkbox';
import {AlsaMidiDeviceInfo} from './AlsaMidiDeviceInfo'; import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSequencer';
import DialogEx from './DialogEx'; import DialogEx from './DialogEx';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
export interface SelectMidiChannelsDialogProps { export interface SelectMidiChannelsDialogProps {
open: boolean; open: boolean;
selectedChannels: AlsaMidiDeviceInfo[]; onClose: () => void;
availableChannels: AlsaMidiDeviceInfo[];
onClose: (selectedChannels: AlsaMidiDeviceInfo[] | null) => void;
} }
function isChecked(selectedChannels: AlsaMidiDeviceInfo[], channel: AlsaMidiDeviceInfo): boolean { interface DialogItem {
for (let i = 0; i < selectedChannels.length; ++i) { id: string;
if (selectedChannels[i].equals(channel)) return true; name: string;
} sortOrder: number
return false; offline: boolean;
} };
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;
}
function SelectMidiChannelsDialog(props: SelectMidiChannelsDialogProps) { function SelectMidiChannelsDialog(props: SelectMidiChannelsDialogProps) {
//const classes = useStyles(); //const classes = useStyles();
const { onClose, selectedChannels, availableChannels, open } = props; const { open, onClose } = props;
const [currentSelection, setCurrentSelection] = useState(selectedChannels); const [availablePorts, setAvailablePorts] = useState<AlsaSequencerPortSelection[] | null>(null);
const [configuration, setConfiguration] = useState<AlsaSequencerConfiguration | null>(null);
const [allPorts, setAllPorts] = useState<DialogItem[] | null>(null);
const [model] = useState<PiPedalModel>(PiPedalModelFactory.getInstance());
const [changed, setChanged] = useState<boolean>(false);
const [ readyToDisplay, setReadyToDisplay ] = useState<boolean>(false);
React.useEffect(() => {
if (open) {
let toggleSelect = (value: AlsaMidiDeviceInfo) => { setReadyToDisplay(false);
if (!isChecked(currentSelection, value)) { model.getAlsaSequencerPorts().then((ports) => {
setCurrentSelection(addPort(availableChannels, currentSelection, value)); 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 { } 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 => { const handleClose = (): void => {
onClose(null); onClose();
}; };
const handleOk = (): void => { const handleOk = (): void => {
onClose(currentSelection); if (changed && configuration !== null) {
model.setAlsaSequencerConfiguration(configuration);
}
onClose();
}; };
return ( return (
<DialogEx tag="midiChannels" onClose={handleClose} aria-labelledby="select-channels-title" open={open} <DialogEx tag="midiChannels" onClose={handleClose} aria-labelledby="select-midi-inputs"
onEnterKey={handleOk} open={open && readyToDisplay}
fullWidth maxWidth="xs"
onEnterKey={handleClose}
> >
<DialogTitle id="simple-dialog-title">Select MIDI Device</DialogTitle> <DialogTitle id="select-midi-inputs">Select MIDI Inputs</DialogTitle>
<List> <DialogContent dividers>
{availableChannels.map((channel) => ( <List>
<ListItemButton> {allPorts !== null && allPorts.length === 0 && (
<FormControlLabel <Typography variant="body2" style={{ marginLeft: 32, marginRight: 24, marginTop: 8, marginBottom: 16 }}>
control={ No MIDI devices found.
<Checkbox checked={isChecked(currentSelection, channel)} onClick={() => toggleSelect(channel)} key={channel.name} /> </Typography>)}
} {allPorts != null && allPorts.map((port) => (
label={channel.description} <ListItemButton key={port.id}>
/> <FormControlLabel
</ListItemButton> control={
) <Checkbox
checked={isChecked(port)}
onClick={() => toggleSelect(port)} />
}
label={
(
<div style={{ display: "flex", flexFlow: "row nowrap", alignItems: "center" }}>
<Typography
color={ port.offline ? "textSecondary" : "textPrimary" }
noWrap variant="body2"
style={{ flex: "1 1 auto" }}
>
{port.name}
</Typography>
{port.offline && (
<Typography color="textSecondary" variant="body2" >
(offline)
</Typography>
)}
</div>
)}
/>
</ListItemButton>
)
)} )}
{availableChannels.length === 0 && ( </List>
<Typography variant="body2" style={{ marginLeft: 32, marginRight: 24,marginTop:8, marginBottom: 16}}> </DialogContent>
No MIDI devices found.</Typography>
)}
</List>
<DialogActions> <DialogActions>
<Button onClick={handleClose} variant="dialogSecondary" > <Button onClick={handleClose} variant="dialogSecondary" >
Cancel Cancel
+20 -14
View File
@@ -44,9 +44,9 @@ import WifiConfigDialog from './WifiConfigDialog';
import WifiDirectConfigDialog from './WifiDirectConfigDialog'; import WifiDirectConfigDialog from './WifiDirectConfigDialog';
import DialogEx from './DialogEx' import DialogEx from './DialogEx'
import GovernorSettings from './GovernorSettings'; import GovernorSettings from './GovernorSettings';
import { AlsaMidiDeviceInfo } from './AlsaMidiDeviceInfo';
import SystemMidiBindingsDialog from './SystemMidiBindingsDialog'; import SystemMidiBindingsDialog from './SystemMidiBindingsDialog';
import SelectThemeDialog from './SelectThemeDialog'; import SelectThemeDialog from './SelectThemeDialog';
import {AlsaSequencerConfiguration} from './AlsaSequencer';
import Slide, { SlideProps } from '@mui/material/Slide'; import Slide, { SlideProps } from '@mui/material/Slide';
import {createStyles} from './WithStyles'; import {createStyles} from './WithStyles';
@@ -73,6 +73,8 @@ interface SettingsDialogState {
jackConfiguration: JackConfiguration; jackConfiguration: JackConfiguration;
jackSettings: JackChannelSelection; jackSettings: JackChannelSelection;
jackServerSettings: JackServerSettings; jackServerSettings: JackServerSettings;
alsaSequencerConfiguration: AlsaSequencerConfiguration;
jackStatus?: JackHostStatus; jackStatus?: JackHostStatus;
governorSettings: GovernorSettings; governorSettings: GovernorSettings;
continueDisabled: boolean; continueDisabled: boolean;
@@ -184,6 +186,7 @@ const SettingsDialog = withStyles(
jackConfiguration: this.model.jackConfiguration.get(), jackConfiguration: this.model.jackConfiguration.get(),
jackStatus: undefined, jackStatus: undefined,
jackSettings: this.model.jackSettings.get(), jackSettings: this.model.jackSettings.get(),
alsaSequencerConfiguration: this.model.alsaSequencerConfiguration.get(),
wifiConfigSettings: this.model.wifiConfigSettings.get(), wifiConfigSettings: this.model.wifiConfigSettings.get(),
wifiDirectConfigSettings: this.model.wifiDirectConfigSettings.get(), wifiDirectConfigSettings: this.model.wifiDirectConfigSettings.get(),
governorSettings: this.model.governorSettings.get(), governorSettings: this.model.governorSettings.get(),
@@ -207,6 +210,7 @@ const SettingsDialog = withStyles(
hasWifiDevice: this.model.hasWifiDevice.get() hasWifiDevice: this.model.hasWifiDevice.get()
}; };
this.handleJackConfigurationChanged = this.handleJackConfigurationChanged.bind(this); this.handleJackConfigurationChanged = this.handleJackConfigurationChanged.bind(this);
this.handleAlsaSequencerConfigurationChanged = this.handleAlsaSequencerConfigurationChanged.bind(this);
this.handleJackSettingsChanged = this.handleJackSettingsChanged.bind(this); this.handleJackSettingsChanged = this.handleJackSettingsChanged.bind(this);
this.handleJackServerSettingsChanged = this.handleJackServerSettingsChanged.bind(this); this.handleJackServerSettingsChanged = this.handleJackServerSettingsChanged.bind(this);
this.handleWifiConfigSettingsChanged = this.handleWifiConfigSettingsChanged.bind(this); this.handleWifiConfigSettingsChanged = this.handleWifiConfigSettingsChanged.bind(this);
@@ -306,6 +310,11 @@ const SettingsDialog = withStyles(
jackConfiguration: this.model.jackConfiguration.get() jackConfiguration: this.model.jackConfiguration.get()
}); });
} }
handleAlsaSequencerConfigurationChanged(): void {
this.setState({
alsaSequencerConfiguration: this.model.alsaSequencerConfiguration.get()
});
}
mounted: boolean = false; mounted: boolean = false;
active: boolean = false; active: boolean = false;
@@ -333,6 +342,7 @@ const SettingsDialog = withStyles(
this.model.showStatusMonitor.addOnChangedHandler(this.handleShowStatusMonitorChanged); this.model.showStatusMonitor.addOnChangedHandler(this.handleShowStatusMonitorChanged);
this.model.jackSettings.addOnChangedHandler(this.handleJackSettingsChanged); this.model.jackSettings.addOnChangedHandler(this.handleJackSettingsChanged);
this.model.jackConfiguration.addOnChangedHandler(this.handleJackConfigurationChanged); this.model.jackConfiguration.addOnChangedHandler(this.handleJackConfigurationChanged);
this.model.alsaSequencerConfiguration.addOnChangedHandler(this.handleAlsaSequencerConfigurationChanged);
this.model.jackServerSettings.addOnChangedHandler(this.handleJackServerSettingsChanged); this.model.jackServerSettings.addOnChangedHandler(this.handleJackServerSettingsChanged);
this.model.wifiConfigSettings.addOnChangedHandler(this.handleWifiConfigSettingsChanged); this.model.wifiConfigSettings.addOnChangedHandler(this.handleWifiConfigSettingsChanged);
this.model.wifiDirectConfigSettings.addOnChangedHandler(this.handleWifiDirectConfigSettingsChanged); this.model.wifiDirectConfigSettings.addOnChangedHandler(this.handleWifiDirectConfigSettingsChanged);
@@ -352,6 +362,7 @@ const SettingsDialog = withStyles(
}); });
this.handleConnectionStateChanged(); this.handleConnectionStateChanged();
this.handleJackConfigurationChanged(); this.handleJackConfigurationChanged();
this.handleAlsaSequencerConfigurationChanged();
this.handleJackSettingsChanged(); this.handleJackSettingsChanged();
this.handleShowStatusMonitorChanged(); this.handleShowStatusMonitorChanged();
this.handleJackServerSettingsChanged(); this.handleJackServerSettingsChanged();
@@ -368,6 +379,7 @@ const SettingsDialog = withStyles(
this.model.showStatusMonitor.removeOnChangedHandler(this.handleShowStatusMonitorChanged); this.model.showStatusMonitor.removeOnChangedHandler(this.handleShowStatusMonitorChanged);
this.model.jackConfiguration.removeOnChangedHandler(this.handleJackConfigurationChanged); this.model.jackConfiguration.removeOnChangedHandler(this.handleJackConfigurationChanged);
this.model.alsaSequencerConfiguration.removeOnChangedHandler(this.handleAlsaSequencerConfigurationChanged);
this.model.jackSettings.removeOnChangedHandler(this.handleJackSettingsChanged); this.model.jackSettings.removeOnChangedHandler(this.handleJackSettingsChanged);
this.model.jackServerSettings.removeOnChangedHandler(this.handleJackServerSettingsChanged); this.model.jackServerSettings.removeOnChangedHandler(this.handleJackServerSettingsChanged);
this.model.wifiConfigSettings.removeOnChangedHandler(this.handleWifiConfigSettingsChanged); this.model.wifiConfigSettings.removeOnChangedHandler(this.handleWifiConfigSettingsChanged);
@@ -437,13 +449,7 @@ const SettingsDialog = withStyles(
}); });
} }
handleSelectMidiDialogResult(channels: AlsaMidiDeviceInfo[] | null): void { handleSelectMidiDialogResult(): void {
if (channels) {
let newSelection: JackChannelSelection = this.state.jackSettings.clone();
newSelection.inputMidiDevices = channels;
this.model.setJackSettings(newSelection);
}
this.setState({ this.setState({
showMidiSelectDialog: false, showMidiSelectDialog: false,
@@ -477,7 +483,7 @@ const SettingsDialog = withStyles(
} }
midiSummary(): string { midiSummary(): string {
let ports = this.state.jackSettings.inputMidiDevices; let ports = this.state.alsaSequencerConfiguration.connections;
if (ports.length === 0) return "Disabled"; if (ports.length === 0) return "Disabled";
let result = ""; let result = "";
@@ -485,7 +491,7 @@ const SettingsDialog = withStyles(
if (result.length !== 0) { if (result.length !== 0) {
result += ", "; result += ", ";
} }
result += port.description; result += port.name;
} }
return result; return result;
} }
@@ -689,7 +695,9 @@ const SettingsDialog = withStyles(
<div style={{ width: "100%" }}> <div style={{ width: "100%" }}>
<Typography className={classes.primaryItem} display="block" variant="body2" noWrap>Select MIDI input</Typography> <Typography className={classes.primaryItem} display="block" variant="body2" noWrap>Select MIDI input</Typography>
<Typography className={classes.secondaryItem} display="block" variant="caption" color="textSecondary" noWrap>{this.midiSummary()}</Typography> <Typography className={classes.secondaryItem} display="block" variant="caption" color="textSecondary" noWrap>{
this.midiSummary()
}</Typography>
</div> </div>
</ButtonBase> </ButtonBase>
<ButtonBase className={classes.setting} disabled={!isConfigValid} onClick={() => this.handleMidiMessageSettings()} > <ButtonBase className={classes.setting} disabled={!isConfigValid} onClick={() => this.handleMidiMessageSettings()} >
@@ -929,9 +937,7 @@ const SettingsDialog = withStyles(
(this.state.showMidiSelectDialog) && (this.state.showMidiSelectDialog) &&
( (
<SelectMidiChannelsDialog open={this.state.showMidiSelectDialog} <SelectMidiChannelsDialog open={this.state.showMidiSelectDialog}
onClose={(selectedChannels: AlsaMidiDeviceInfo[] | null) => this.handleSelectMidiDialogResult(selectedChannels)} onClose={() => this.handleSelectMidiDialogResult()}
selectedChannels={this.state.jackSettings.inputMidiDevices}
availableChannels={this.state.jackConfiguration.inputMidiDevices}
/> />
) )
+85
View File
@@ -0,0 +1,85 @@
import React from 'react';
import Typography from '@mui/material/Typography';
import { styled } from '@mui/material/styles';
const Frame = styled('div', {
name: 'MuiStat', // The component name
slot: 'root', // The slot name
})(({ theme }) => ({
flex: "1 1 auto",
gap: theme.spacing(0.5),
padding: theme.spacing(3, 4),
backgroundColor: theme.palette.background.paper,
width: "100%",
height: "100%"
}));
interface Tone3000AuthCompleteProps {
}
function Tone3000AuthComplete(props: Tone3000AuthCompleteProps) {
function postApiKey() {
let url = new URL(window.location.href);
let apiKey = url.searchParams.get("api_key");
if (apiKey) {
let myUrl = new URL(window.location.href);
let port = myUrl.port;
if (port === "5173") { // when running debug server.
port = "8080";
}
let postUrl = "http://" + myUrl.hostname + ":" + port + "/var/Tone3000Auth?api_key=" + encodeURIComponent(apiKey);
fetch(postUrl, {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ api_key: apiKey })
}).then((response) => {
if (!response.ok) {
alert("Failed to post API key to Pipedal: " + response.statusText);
}
return response.json();
})
.catch((error) => {
alert("Error posting API key to Pipedal: " + error.message);
})
} else {
alert("No API key found in URL.");
}
}
React.useEffect(() => {
postApiKey();
return () => { };
})
return (
<Frame style={{
position: "absolute", top: 0, left: 0, right: 0, bottom: 0,
}}>
<div style={{ display: "flex", flexFlow: "row nowrap", alignItems: "start" }}>
<div style={{ flex: "1 1 1px" }} />
<div style={{
display: "flex", flexFlow: "column nowrap", alignItems: "start",
maxWidth: 600, marginLeft: "auto", marginRight: "auto", gap: 16, padding: 32
}}>
<Typography variant="h5">TONE3000 Authorization Complete</Typography>
<Typography variant="body1">
You have successfully obtained an access token from TONE3000, and can now download Toob Neural Amp models from TONE3000
directly.</Typography>
<Typography variant="body1">
Close this page and return to Pipedal in order to continue.</Typography>
</div>
<div style={{ flex: "2 2 1px" }} />
</div>
</Frame>
);
}
export default Tone3000AuthComplete;
+163
View File
@@ -0,0 +1,163 @@
import React from 'react';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import DialogEx from './DialogEx';
import Link from '@mui/material/Link';
import Toolbar from '@mui/material/Toolbar';
import IconButtonEx from './IconButtonEx';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
export interface Tone3000DialogProps {
open: boolean;
onClose: () => void;
}
function Tone3000Dialog(props: Tone3000DialogProps) {
const { open, onClose } = props;
const model: PiPedalModel = PiPedalModelFactory.getInstance();
let [openAuthDialog, setOpenAuthDialog] = React.useState(!model.hasTone3000Auth.get());
React.useEffect(()=> {
let authListener = (value: boolean) => {
setOpenAuthDialog(!value);
}
model.hasTone3000Auth.addOnChangedHandler(authListener);
return () => {
model.hasTone3000Auth.removeOnChangedHandler(authListener);
}
});
function RedirectUrl() {
let baseUrl = new URL(window.location.href);
let result = "http://" + baseUrl.hostname + ":" + baseUrl.port + "/";
return result;
}
function AuthUrl() {
//return "https://www.tone3000.com/api/v1/auth?redirect_url=" + encodeURIComponent(RedirectUrl()) + "&opt_only=true";
return "https://www.tone3000.com/api/v1/auth?redirect_url=" + encodeURIComponent(RedirectUrl()) + "&otp_only=true";
}
return (
<DialogEx
tag="tone3000"
fullScreen={true}
open={open}
onEnterKey={() => { onClose(); }}
onClose={() => { onClose(); }}
aria-labelledby="tone3000-dialog-title"
aria-describedby="tone3000-dialog-description"
>
<DialogTitle id="tone3000-dialog-title">
<Toolbar style={{ padding: 0 }}>
<IconButtonEx
tooltip="Close"
edge="start"
color="inherit"
aria-label="cancel"
style={{ opacity: 0.6 }}
onClick={() => { onClose(); }}
>
<ArrowBackIcon style={{ width: 24, height: 24 }} />
</IconButtonEx>
<Typography noWrap component="div" sx={{ flexGrow: 1 }}>
TONE3000 Models
</Typography>
</Toolbar>
</DialogTitle>
<DialogContent>
<Typography variant="body1">
This is a placeholder for the Tone 3000 dialog.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
Close
</Button>
</DialogActions>
{
openAuthDialog && (
<Dialog
fullScreen={true}
open={openAuthDialog}>
<DialogTitle id="tone3000-auth-dialog-title">
<Toolbar style={{ padding: 0 }}>
<IconButtonEx
tooltip="Close"
edge="start"
color="inherit"
aria-label="cancel"
style={{ opacity: 0.6 }}
onClick={() => { onClose(); }}
>
<ArrowBackIcon style={{ width: 24, height: 24 }} />
</IconButtonEx>
<Typography noWrap component="div" sx={{ flexGrow: 1 }}>
TONE3000 Authorization
</Typography>
</Toolbar>
</DialogTitle>
<DialogContent>
<div style={{ display: "flex", flexFlow: "row nowrap", alignItems: "start" }}>
<div style={{ flex: "1 1 1px" }} />
<div style={{
maxWidth: 500, marginLeft: "auto", marginRight: "auto",
display: "flex", flexFlow: "column nowrap", gap: 16, alignItems: "start",
}}>
<Typography variant="body1" component="div" display="block" >
The <Link href="https://www.tone3000.com" target="_blank">TONE3000 website</Link> 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.
</Typography>
<Typography variant="body1" component="div" display="block" >
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.
</Typography>
<Button variant="contained" color="primary" style={{ alignSelf: "end", marginRight: 32 }}
onClick={() => {
window.open(AuthUrl(), "_blank");
}}
>
Continue to TONE3000
</Button>
<Typography variant="body2" component="div" display="block" style={{ marginTop: 32 }}>
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 <Link href="https://www.tone3000.com/privacy" target="_blank">TONE3000 privacy policy</Link> for
information on how your data is used by TONE3000.
</Typography>
</div>
<div style={{ flex: "2 2 1px" }} />
</div>
</DialogContent>
</Dialog>
)}
</DialogEx>
);
}
export default Tone3000Dialog;
+92
View File
@@ -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 (
<DialogEx
tag="tone3000-help"
fullWidth={true}
maxWidth="sm"
onEnterKey={() => { onClose(); }}
onClose={() => { onClose(); }}
open={open}>
<DialogTitle >
<Toolbar style={{ padding: 0 }}>
<IconButtonEx
tooltip="Close"
edge="start"
color="inherit"
aria-label="cancel"
style={{ opacity: 0.6 }}
onClick={() => { onClose(); }}
>
<ArrowBackIcon style={{ width: 24, height: 24 }} />
</IconButtonEx>
<Typography id="tone3000-auth-dialog-title" noWrap component="div" sx={{ flexGrow: 1 }}>
TONE3000 Help
</Typography>
</Toolbar>
</DialogTitle>
<DialogContent>
<div style={{ display: "flex", flexFlow: "row nowrap", alignItems: "start" }}>
<div style={{ flex: "1 1 1px" }} />
<div style={{
maxWidth: 500, marginLeft: "auto", marginRight: "auto",
display: "flex", flexFlow: "column nowrap", gap: 16, alignItems: "start",
}}>
<Typography variant="body1" component="div" display="block" >
The <Link href="https://www.tone3000.com" target="_blank">TONE3000 website</Link> provides a
massive collection of neural amp models that can be used by TooB Neural Amp Modeler.
</Typography>
<Typography variant="body1" component="div" display="block">
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 <strong><em>Upload</em></strong> button. PiPedal
automatically extracts bundles of model files from zip archives, so manual extraction is unnecessary.
</Typography>
<Typography variant="body1" component="div" display="block">
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' <Link href="https://www.neuralampmodeler.com/">Neural Amp Modeler library</Link>, which forms the foundation and core of TooB Neural Amp Modeler and other NAM-based plugins.
</Typography>
<Typography variant="body1" component="div" display="block">
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 <Link href="https://www.tone3000.com/capture">TONE3000 website</Link> for more details.
</Typography>
<Typography variant="body1" component="div" display="block" >
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.
</Typography>
<Typography variant="body2" component="div" display="block" style={{ marginTop: 32 }}>
Privacy statement: PiPedal has no access to personal data used by the TONE3000 website. Please refer to
the <Link href="https://www.tone3000.com/privacy" target="_blank">TONE3000 privacy policy</Link> for
information on how your data is used by TONE3000.
</Typography>
</div>
<div style={{ flex: "2 2 1px" }} />
</div>
</DialogContent>
</DialogEx>
);
}
export default Tone3000HelpDialog;
+1 -1
View File
@@ -38,7 +38,7 @@ import ButtonBase from '@mui/material/ButtonBase';
import FilePropertyDialog from './FilePropertyDialog'; import FilePropertyDialog from './FilePropertyDialog';
import JsonAtom from './JsonAtom'; import JsonAtom from './JsonAtom';
import { UiFileProperty } from './Lv2Plugin'; import { UiFileProperty } from './Lv2Plugin';
import { Divider } from '@mui/material'; import Divider from '@mui/material/Divider';
import useWindowSize from './UseWindowSize'; import useWindowSize from './UseWindowSize';
import { getAlbumArtUri } from './AudioFileMetadata'; import { getAlbumArtUri } from './AudioFileMetadata';
import RepeatIcon from '@mui/icons-material/Repeat'; import RepeatIcon from '@mui/icons-material/Repeat';
+11 -14
View File
@@ -81,7 +81,7 @@ function ToolTipEx(props: ToolTipExProps) {
if (valueTooltip === undefined) // no timeout if there's a value tooltip if (valueTooltip === undefined) // no timeout if there's a value tooltip
{ {
if (t > 0) { if (t > 0) {
console.log("ToolTipEx: starting timeout for ", t); // console.log("ToolTipEx: starting timeout for ", t);
handle = window.setTimeout(() => { handle = window.setTimeout(() => {
setOpen(true); setOpen(true);
},t); },t);
@@ -89,7 +89,7 @@ function ToolTipEx(props: ToolTipExProps) {
} }
return () => { return () => {
if (handle !== null) { if (handle !== null) {
console.log("ToolTipEx: clearing timeout for ", t); // console.log("ToolTipEx: clearing timeout for ", t);
window.clearTimeout(handle); window.clearTimeout(handle);
} }
}; };
@@ -161,6 +161,7 @@ function ToolTipEx(props: ToolTipExProps) {
} }
} }
} }
let effectiveTitle: React.ReactNode | null = null; let effectiveTitle: React.ReactNode | null = null;
let placement: "top-start" | "right" = "top-start"; let placement: "top-start" | "right" = "top-start";
if (valueTooltip !== undefined && !isLongPress) { if (valueTooltip !== undefined && !isLongPress) {
@@ -181,44 +182,40 @@ function ToolTipEx(props: ToolTipExProps) {
return ( return (
<div style={{ display: 'inline-block' }} <div style={{ display: 'inline-block' }}
onClickCapture={(e) => { onClickCapture={(e) => {
console.log("ToolTipEx: onClickCapture"); // console.log("ToolTipEx: onClickCapture");
handleClickCapture(e); handleClickCapture(e);
setTimeout(0);
setOpen(false);
setIsLongPress(false);
setLongPressLeaving(true);
}} }}
onMouseEnter={(e)=> { 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 if (!longPressLeaving) {// Don't handle mouse enter if we're in a long press leaving state
handleMouseEnter(e); handleMouseEnter(e);
} }
}} }}
onMouseLeave={(e)=> { onMouseLeave={(e)=> {
console.log("ToolTipEx: onMouseLeave"); // console.log("ToolTipEx: onMouseLeave");
handleMouseLeave(e); handleMouseLeave(e);
}} }}
onPointerCancelCapture={(e) => { onPointerCancelCapture={(e) => {
console.log("ToolTipEx: onPointerCancelCapture"); // console.log("ToolTipEx: onPointerCancelCapture");
handlePointerCancel(e); handlePointerCancel(e);
}} }}
onContextMenuCapture={(e) => { onContextMenuCapture={(e) => {
console.log("ToolTipEx: onContextMenuCapture"); // console.log("ToolTipEx: onContextMenuCapture");
handleConextMenu(e); handleConextMenu(e);
return false; return false;
}} }}
onPointerDownCapture={(e) => { onPointerDownCapture={(e) => {
console.log("ToolTipEx: onPointerDownCapture"); // console.log("ToolTipEx: onPointerDownCapture");
handlePointerDownCapture(e); handlePointerDownCapture(e);
}} }}
onPointerMoveCapture={(e) => { onPointerMoveCapture={(e) => {
console.log("ToolTipEx: onPointerMoveCapture"); // console.log("ToolTipEx: onPointerMoveCapture");
handlePointerMoveCapture(e); handlePointerMoveCapture(e);
}} }}
onPointerUpCapture={(e) => { onPointerUpCapture={(e) => {
console.log("ToolTipEx: onPointerUpCapture"); // console.log("ToolTipEx: onPointerUpCapture");
handlePointerUpCapture(e); handlePointerUpCapture(e);
}} }}
> >
+30 -25
View File
@@ -21,38 +21,43 @@
* SOFTWARE. * SOFTWARE.
*/ */
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect } from 'react';
const getSize = () => { const getSize = () => {
return { return {
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight, height: window.innerHeight,
}; };
}; };
export interface WindowSize { export interface WindowSize {
width: number; width: number;
height: number; height: number;
} }
export default function useWindowSize() { export default function useWindowSize() {
const [size, setSize] = useState<WindowSize>(getSize());
const handleResize = useCallback(() => {
let ticking = false;
if (!ticking) {
window.requestAnimationFrame(() => {
setSize(getSize());
ticking = false;
});
ticking = true;
}
}, []);
useEffect(() => { const [size, setSize] = useState<WindowSize>(getSize());
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize); useEffect(() => {
}, []); let mounted = true;
let handleResizeT = () => {
return [size]; 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];
} }