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]"
// "[wifi_channels_test]"
// "[locale]"
"[pipedal_alsa_seq_test]"
"[pipewire_input_stream]"
],
"stopAtEntry": false,
+1
View File
@@ -11,6 +11,7 @@ set (DISPLAY_VERSION "PiPedal v1.4.76-Release")
set (PACKAGE_ARCHITECTURE ${DEBIAN_ARCHITECTURE})
set (CMAKE_INSTALL_PREFIX "/usr/")
set (PIPEDAL_EXCLUDE_TESTS false)
include(CTest)
enable_testing()
+775 -88
View File
@@ -24,18 +24,113 @@
#include "AlsaSequencer.hpp"
#include <vector>
#include <string>
#include <regex>
#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
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> ports;
snd_seq_t *seq;
if (snd_seq_open(&seq, "default", SND_SEQ_OPEN_INPUT, 0) < 0)
if (snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0)
{
return ports;
}
@@ -81,8 +176,11 @@ namespace pipedal
port.canReadSubscribe = capability & SND_SEQ_PORT_CAP_SUBS_READ;
port.canWriteSubscribe = capability & SND_SEQ_PORT_CAP_SUBS_WRITE;
auto typeBits = snd_seq_port_info_get_type(port_info);
#ifdef SND_SEQ_PORT_TYPE_MIDI_UMP
port.isUmp = (typeBits & SND_SEQ_PORT_TYPE_MIDI_UMP) != 0;
#else
port.isUmp = false; // UMP support is not available in all versions of ALSA
#endif
port.isSystemAnnounce = (typeBits & SND_SEQ_PORT_SYSTEM_ANNOUNCE) != 0;
port.isMidiSynth = (typeBits & SND_SEQ_PORT_TYPE_MIDI_GENERIC) != 0 ||
(typeBits & SND_SEQ_PORT_TYPE_MIDI_GM) != 0 ||
@@ -92,19 +190,61 @@ namespace pipedal
port.isSpecific = (typeBits & SND_SEQ_PORT_TYPE_SPECIFIC) != 0;
port.isSynth = (typeBits & SND_SEQ_PORT_TYPE_SYNTH) != 0;
port.isHardware = (typeBits & SND_SEQ_PORT_TYPE_HARDWARE) != 0;
port.isPort = (typeBits & SND_SEQ_PORT_TYPE_PORT) != 0;
port.isSoftware = (typeBits & SND_SEQ_PORT_TYPE_SOFTWARE) != 0;
port.isVirtual = port.name.starts_with("VirMIDI");
port.cardNumber = snd_seq_client_info_get_card(client_info);
port.port = snd_seq_port_info_get_port(port_info);
if (port.isKernelDevice & port.cardNumber >= 0 && port.port >= 0)
if (port.isKernelDevice && port.isPort && port.cardNumber >= 0 && port.port >= 0)
{
// For kernel devices, we can construct a raw MIDI device string
port.rawMidiDevice = SS("hw:" << port.cardNumber << ",0," << port.port);
std::string rawMidiDevice;
if (port.isVirtual)
{
// "VirMidI 2-1"
std::regex virtualDeviceRegex{"^(VirMIDI) (\\d+)-(\\d+)$"};
{
// Extract the card and device numbers from the match
std::smatch match;
std::regex_search(port.name, match, virtualDeviceRegex);
if (match.size() == 4)
{
try
{
std::string devName = match[1];
int card = std::stoi(match[2]);
int device = std::stoi(match[3]);
rawMidiDevice = SS("hw:CARD=" << devName << ",DEV=" << device);
}
catch (const std::exception &ignored)
{
}
}
}
}
else
{
rawMidiDevice = SS("hw:CARD=" << port.clientName << ",DEV=" << 0);
if (port.port > 0)
{
rawMidiDevice += SS("," << port.port);
}
}
port.rawMidiDevice = std::move(rawMidiDevice);
}
port.id = SS("seq:" << port.clientName << "/" << port.name);
ports.push_back(port);
port.displaySortOrder = port.client * 256 + port.port;
if (port.isVirtual)
{
port.displaySortOrder += 1 * 256 * 256; // MIDI virtual ports after real ports.
}
else if (port.clientName == "Midi Through")
{
port.displaySortOrder += 2 * 256 * 256; // MIDI Through at the very end because it's weird.
}
ports.push_back(std::move(port));
}
}
@@ -112,7 +252,7 @@ namespace pipedal
return ports;
}
AlsaSequencer::AlsaSequencer()
AlsaSequencerImpl::AlsaSequencerImpl()
{
seqHandle = nullptr;
queueId = -1;
@@ -125,24 +265,48 @@ namespace pipedal
// convert rc to message
throw std::runtime_error(SS("Failed to open ALSA sequencer:" << snd_strerror(rc)));
}
size_t inputBufferSize = snd_seq_get_input_buffer_size(seqHandle);
(void)inputBufferSize;
rc = snd_seq_set_input_buffer_size(seqHandle, 128 * 1024);
if (rc < 0)
{
Lv2Log::warning("Failed resize the ALSA sequencer input buffer: %s", snd_strerror(rc));
}
snd_seq_set_client_name(seqHandle, "PiPedal");
inPort = snd_seq_create_simple_port(seqHandle, "PiPedal:in",
SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE,
SND_SEQ_PORT_TYPE_MIDI_GENERIC |
SND_SEQ_PORT_TYPE_MIDI_GM | SND_SEQ_PORT_TYPE_APPLICATION);
SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE,
SND_SEQ_PORT_TYPE_MIDI_GENERIC |
SND_SEQ_PORT_TYPE_MIDI_GM | SND_SEQ_PORT_TYPE_APPLICATION);
if (inPort < 0)
{
// convert rc to message
throw std::runtime_error(SS("Failed to open ALSA sequencer:" << snd_strerror(inPort)));
}
}
AlsaSequencer::~AlsaSequencer()
{
for (const auto &connection : connections)
CreateRealtimeInputQueue();
snd_seq_nonblock(seqHandle, 1); // Set sequencer to non-blocking mode
// Get our client and port numbers for reference
myClientId = snd_seq_client_id(seqHandle);
if (myClientId < 0)
{
snd_seq_disconnect_from(seqHandle, inPort, connection.clientId, connection.portId);
throw std::runtime_error(SS("Failed to get client ID: " << snd_strerror(myClientId)));
}
}
void AlsaSequencerImpl::RemoveAllConnections()
{
while (connections.size() != 0)
{
auto connection = connections.back();
ModifyConnection(connection.clientId, connection.portId, ConnectAction::Unsubscribe);
}
}
AlsaSequencerImpl::~AlsaSequencerImpl()
{
RemoveAllConnections();
if (queueId >= 0)
{
snd_seq_free_queue(seqHandle, queueId);
@@ -160,25 +324,17 @@ namespace pipedal
}
}
void AlsaSequencer::ConnectPort(int clientId, int portId)
void AlsaSequencerImpl::ConnectPort(int clientId, int portId)
{
CreateRealtimeInputQueue();
// Connect this input port to the target client:port
int rc = snd_seq_connect_from(seqHandle, inPort, clientId, portId);
if (rc < 0)
{
throw std::runtime_error(SS("Failed to connect to ALSA port: " << snd_strerror(rc)));
}
this->connections.push_back({clientId, portId});
ModifyConnection(clientId, portId, ConnectAction::Subscribe);
}
void AlsaSequencer::ConnectPort(const std::string &name)
void AlsaSequencerImpl::ConnectPort(const std::string &id)
{
auto ports = EnumeratePorts();
for (const auto &port : ports)
{
if (port.name == name)
if (port.id == id)
{
ConnectPort(port.client, port.port);
return;
@@ -186,73 +342,303 @@ namespace pipedal
}
throw std::runtime_error("ALSA port not found");
}
void AlsaSequencerImpl::SetConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration)
{
this->RemoveAllConnections(); // Currently no configuration options to set
bool AlsaSequencer::ReadMessage(AlsaMidiMessage &message)
auto ports = EnumeratePorts();
for (const auto &connection : alsaSequencerConfiguration.connections())
{
// Connect to each port specified in the configuration
std::string id = connection.id();
for (const auto &port : ports)
{
if (port.id == id)
{
ConnectPort(port.client, port.port);
break;
}
}
}
}
bool AlsaSequencerImpl::WaitForMessage(int timeoutMs)
{
while (true)
{
auto fdCount = snd_seq_poll_descriptors_count(seqHandle, POLLIN);
if (fdCount == 0)
return true;
this->pollFds.resize(fdCount);
snd_seq_poll_descriptors(seqHandle, pollFds.data(), fdCount, POLLIN);
int rc = poll(pollFds.data(), fdCount, timeoutMs);
if (rc == 0)
{
return false; // timed out.
}
else if (rc < 0)
{
if (errno == EINTR)
{
continue;
}
else
{
throw std::runtime_error(SS("ALSA sequencer poll error: " << strerror(errno)));
}
}
else
{
return true;
}
}
}
bool AlsaSequencerImpl::ReadMessage(AlsaMidiMessage &message, int timeoutMs)
{
// Event loop
snd_seq_event_t *event = nullptr;
bool success = false;
if (snd_seq_event_input(seqHandle, &event) >= 0 && event)
while (true)
{
success = true;
// Extract timestamp information
message.timestamp = event->time.tick;
message.realtime_sec = event->time.time.tv_sec;
message.realtime_nsec = event->time.time.tv_nsec;
// Process MIDI event here, e.g. NOTEON, NOTEOFF, etc.
switch (event->type)
bool success = false;
int rc = snd_seq_event_input(seqHandle, &event);
if (rc < 0)
{
case SND_SEQ_EVENT_NOTEON:
message.cc0 = 0x90 | event->data.note.channel; // channel
message.cc1 = event->data.note.note; // note
message.cc2 = event->data.note.velocity; // velocity
break;
case SND_SEQ_EVENT_NOTEOFF:
// handle note-off
message.cc0 = 0x80 | event->data.note.channel; // channel
message.cc1 = event->data.note.note; // note
message.cc2 = event->data.note.off_velocity; // off velocity
break;
case SND_SEQ_EVENT_KEYPRESS:
message.cc0 = 0xA0 | event->data.note.channel; // polyphonic key pressure
message.cc1 = event->data.note.note; // note
message.cc2 = event->data.note.velocity; // pressure
break;
case SND_SEQ_EVENT_CONTROLLER:
message.cc0 = 0xB0 | event->data.control.channel; // control change
message.cc1 = event->data.control.param; // controller number
message.cc2 = event->data.control.value; // controller value
break;
case SND_SEQ_EVENT_PGMCHANGE:
message.cc0 = 0xC0 | event->data.control.channel; // program change
message.cc1 = event->data.control.value; // program number
message.cc2 = 0; // unused
break;
case SND_SEQ_EVENT_CHANPRESS:
message.cc0 = 0xD0 | event->data.control.channel; // channel pressure
message.cc1 = event->data.control.value; // pressure value
message.cc2 = 0; // unused
break;
case SND_SEQ_EVENT_PITCHBEND:
message.cc0 = 0xE0 | event->data.control.channel; // pitch bend
message.cc1 = (event->data.control.value >> 7) & 0x7F; // MSB
message.cc2 = event->data.control.value & 0x7F; // LSB
break;
case SND_SEQ_EVENT_CONTROL14:
case SND_SEQ_EVENT_NONREGPARAM:
case SND_SEQ_EVENT_REGPARAM:
case SND_SEQ_EVENT_SONGPOS:
default:
success = false;
break;
if (rc == -EAGAIN)
{
if (!WaitForMessage(timeoutMs))
{
return false;
}
}
else
{
// Handle other errors
throw std::runtime_error(SS("ALSA sequencer input error: " << snd_strerror(rc)));
}
}
else if (event)
{
success = true;
// Extract timestamp information
message.timestamp = event->time.tick;
message.realtime_sec = event->time.time.tv_sec;
message.realtime_nsec = event->time.time.tv_nsec;
// Process MIDI event here, e.g. NOTEON, NOTEOFF, etc.
switch (event->type)
{
case SND_SEQ_EVENT_NOTEON:
message.Set(
(uint8_t)(0x90 | event->data.note.channel),
(uint8_t)(event->data.note.note), // note
(uint8_t)(event->data.note.velocity)); // velocity
break;
case SND_SEQ_EVENT_NOTEOFF:
// handle note-off
message.Set(
uint8_t(0x80 | event->data.note.channel),
uint8_t(event->data.note.note),
uint8_t(event->data.note.off_velocity)); // off velocity
break;
case SND_SEQ_EVENT_KEYPRESS:
message.Set(
(uint8_t)(0xA0 | event->data.note.channel), // polyphonic key pressure
(uint8_t)(event->data.note.note), // note
(uint8_t)(event->data.note.velocity)); // pressure
break;
case SND_SEQ_EVENT_CONTROLLER:
message.Set(
(uint8_t)(0xB0 | event->data.control.channel), // control change
(uint8_t)(event->data.control.param), // controller number
(uint8_t)(event->data.control.value)); // controller value
break;
case SND_SEQ_EVENT_PGMCHANGE:
message.Set(
(uint8_t)(0xC0 | event->data.control.channel), // program change
(uint8_t)(event->data.control.value));
break;
case SND_SEQ_EVENT_CHANPRESS:
message.Set(
uint8_t(0xD0 | event->data.control.channel),
uint8_t(event->data.control.value));
break;
case SND_SEQ_EVENT_PITCHBEND:
message.Set(uint8_t(0xE0 | event->data.control.channel),
uint8_t((event->data.control.value >> 7) & 0x7F),
uint8_t(event->data.control.value & 0x7F));
break;
case SND_SEQ_EVENT_CONTROL14:
message.size = 6;
message.data = message.fixedBuffer;
message.fixedBuffer[0] = uint8_t(0xB0 | event->data.control.channel); // Control Change 14-bit
message.fixedBuffer[1] = uint8_t(event->data.control.param); // MSB
message.fixedBuffer[2] = uint8_t(event->data.control.param >> 7) & 0x7F; // MSB
message.fixedBuffer[3] = uint8_t(0xB0 | event->data.control.channel);
message.fixedBuffer[4] = uint8_t(event->data.control.value + 0x20);
message.fixedBuffer[5] = uint8_t(event->data.control.value) & 0x7F; // MSB value
break;
case SND_SEQ_EVENT_NONREGPARAM:
message.size = 12;
message.data = message.fixedBuffer;
message.fixedBuffer[0] = uint8_t(0xB0 | event->data.control.channel); // Non-registered parameter
message.fixedBuffer[1] = 0x63; // MSB
message.fixedBuffer[2] = uint8_t(event->data.control.param >> 7) & 0x7F; // MSB
message.fixedBuffer[3] = uint8_t(0xB0 | event->data.control.channel);
message.fixedBuffer[4] = 0x62; // LSB
message.fixedBuffer[5] = uint8_t(event->data.control.param) & 0x7F; // LSB
message.fixedBuffer[6] = uint8_t(0xB0 | event->data.control.channel);
message.fixedBuffer[7] = uint8_t(0x06); // Non-registered parameter value MSB
message.fixedBuffer[8] = uint8_t(event->data.control.value >> 7) & 0x7F; // MSB value
message.fixedBuffer[9] = uint8_t(0xB0 | event->data.control.channel);
message.fixedBuffer[10] = uint8_t(0x26); // Non-registered parameter value LSB
message.fixedBuffer[11] = uint8_t(event->data.control.value) & 0x7F; // LSb
break;
case SND_SEQ_EVENT_REGPARAM:
message.size = 12;
message.data = message.fixedBuffer;
message.fixedBuffer[0] = uint8_t(0xB0 | event->data.control.channel); // Registered parameter
message.fixedBuffer[1] = uint8_t(0x65); // MSB
message.fixedBuffer[2] = uint8_t(event->data.control.param >> 7) & 0x7F; // MSB
message.fixedBuffer[3] = uint8_t(0xB0 | event->data.control.channel);
message.fixedBuffer[4] = uint8_t(0x64); // LSB
message.fixedBuffer[5] = uint8_t(event->data.control.param) & 0x7F; // LSB
message.fixedBuffer[6] = uint8_t(0xB0 | event->data.control.channel);
message.fixedBuffer[7] = uint8_t(0x6); // Registered parameter value MSB
message.fixedBuffer[8] = uint8_t(event->data.control.value >> 7) & 0x7F; // MSB value
message.fixedBuffer[9] = uint8_t(0xB0 | event->data.control.channel);
message.fixedBuffer[10] = uint8_t(0x26); // Registered parameter value LSB
message.fixedBuffer[11] = uint8_t(event->data.control.value) & 0x7F; // LSB value
break;
case SND_SEQ_EVENT_SONGPOS:
message.Set(
0xF2,
(uint8_t)((event->data.control.value >> 7) & 0x7F), // MSB
(uint8_t)(event->data.control.value & 0x7F) // LSB
);
break;
case SND_SEQ_EVENT_SONGSEL:
message.Set(
0xF3,
(uint8_t)((event->data.control.value >> 7) & 0x7F), // MSB
(uint8_t)(event->data.control.value & 0x7F) // LSB
);
break;
case SND_SEQ_EVENT_QFRAME:
message.Set(
0xF1,
(uint8_t)((event->data.control.value >> 7) & 0x7F), // MSB
(uint8_t)(event->data.control.value & 0x7F) // LSB
);
break;
case SND_SEQ_EVENT_START:
message.Set(0xFA); // MIDI Real Time Start
break;
case SND_SEQ_EVENT_CONTINUE:
message.Set(0xFB); // MIDI Real Time Continue
break;
case SND_SEQ_EVENT_STOP:
message.Set(0xFC); // MIDI Real Time Stop
break;
case SND_SEQ_EVENT_TICK:
message.Set(0xF8); // MIDI Real Time Clock Tick
break;
case SND_SEQ_EVENT_SENSING:
message.Set(0xFE); // MIDI Real Time Active Sensing
break;
case SND_SEQ_EVENT_RESET:
message.Set(0xFF); // MIDI Real Time System Reset
break;
case SND_SEQ_EVENT_SYSEX:
// Handle SysEx messages
if (event->data.ext.len > 0 && event->data.ext.len + 2 <= sizeof(message.fixedBuffer))
{
message.size = event->data.ext.len + 1; // +1 for SysEx
message.data = message.fixedBuffer;
message.fixedBuffer[0] = 0xF0; // Start of SysEx
memcpy(message.fixedBuffer + 1, event->data.ext.ptr, event->data.ext.len);
message.fixedBuffer[event->data.ext.len + 1] = 0xF7; // End of SysEx
}
else
{
message.size = event->data.ext.len;
message.data = (uint8_t *)event->data.ext.ptr;
if (message.data[0] != 0xF0)
{
throw std::logic_error("Invalid SysEx message: does not start with 0xF0");
}
}
break;
case SND_SEQ_EVENT_SETPOS_TICK:
message.size = 3 + sizeof(event->data.queue.param.value); // Tempo events are usually 3 bytes
message.data = message.fixedBuffer;
message.fixedBuffer[0] = 0xFF; // Meta event type for tempo
message.fixedBuffer[1] = (uint8_t)MetaEventType::SetPositionTick; // Meta event subtype for tempo
message.fixedBuffer[2] = (uint8_t)(event->data.queue.queue); // MSB
memcpy(message.fixedBuffer + 3, &event->data.queue.param.value, sizeof(event->data.queue.param.value));
break;
case SND_SEQ_EVENT_SETPOS_TIME:
message.size = 3 + sizeof(event->data.queue.param.time); // Tempo events are usually 3 bytes
message.data = message.fixedBuffer;
message.fixedBuffer[0] = 0xFF; // Meta event type for tempo
message.fixedBuffer[1] = (uint8_t)MetaEventType::SetPositionTime; // Meta event subtype for tempo
message.fixedBuffer[2] = (uint8_t)(event->data.queue.queue);
memcpy(message.fixedBuffer + 3, &event->data.queue.param.time, sizeof(event->data.queue.param.time));
break;
case SND_SEQ_EVENT_TEMPO:
// Handle tempo events
message.size = sizeof(event->data.queue.param.value) + 3; // Tempo events are usually 3 bytes
if (message.size > sizeof(message.fixedBuffer))
{
throw std::logic_error("Tempo event size exceeds fixed buffer size");
}
message.data = message.fixedBuffer;
message.fixedBuffer[0] = 0xFF; // Meta event type for tempo
message.fixedBuffer[1] = (uint8_t)MetaEventType::Tempo; // Meta event subtype for tempo
message.fixedBuffer[2] = (uint8_t)(event->data.queue.queue);
memcpy(message.fixedBuffer + 3, &event->data.queue.param.value, sizeof(event->data.queue.param.value));
break;
case SND_SEQ_EVENT_CLOCK:
message.Set(0xF8); // MIDI Real Time Clock Tick
break;
#ifndef NDEBUG
#define MSG_DEBUG_LOG(x) \
case x: \
Lv2Log::debug("ALSA Sequencer Message" #x); \
break;
#else
#define MSG_DEBUG_LOG(x)
#endif
MSG_DEBUG_LOG(SND_SEQ_EVENT_CLIENT_START)
MSG_DEBUG_LOG(SND_SEQ_EVENT_CLIENT_EXIT)
MSG_DEBUG_LOG(SND_SEQ_EVENT_CLIENT_CHANGE)
MSG_DEBUG_LOG(SND_SEQ_EVENT_PORT_START)
MSG_DEBUG_LOG(SND_SEQ_EVENT_PORT_EXIT)
MSG_DEBUG_LOG(SND_SEQ_EVENT_PORT_CHANGE)
MSG_DEBUG_LOG(SND_SEQ_EVENT_PORT_SUBSCRIBED)
MSG_DEBUG_LOG(SND_SEQ_EVENT_PORT_UNSUBSCRIBED)
case SND_SEQ_EVENT_KEYSIGN:
case SND_SEQ_EVENT_TIMESIGN:
// and a PASSEL of others!
default:
success = false;
break;
}
snd_seq_free_event(event);
}
if (success)
{
return true;
}
snd_seq_free_event(event);
}
return success;
}
int AlsaSequencer::CreateRealtimeInputQueue()
int AlsaSequencerImpl::CreateRealtimeInputQueue()
{
if (!seqHandle)
{
@@ -291,8 +677,6 @@ namespace pipedal
throw std::runtime_error(SS("Failed to start queue: " << snd_strerror(rc)));
}
// Set the queue for input timestamping
snd_seq_port_info_t *port_info;
snd_seq_port_info_alloca(&port_info);
@@ -321,7 +705,7 @@ namespace pipedal
return queueId;
}
bool AlsaSequencer::GetQueueRealtime(uint64_t *sec, uint32_t *nsec)
bool AlsaSequencerImpl::GetQueueRealtime(uint64_t *sec, uint32_t *nsec)
{
if (!seqHandle || queueId < 0)
{
@@ -345,4 +729,307 @@ namespace pipedal
return true;
}
}
std::string RawMidiIdToSequencerId(const std::vector<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
add_library(PiPedalCommon STATIC
include/Finally.hpp
include/dbus/org.freedesktop.NetworkManager.Device.Statistics.hpp
include/dbus/org.freedesktop.NetworkManager.Settings.Connection.hpp
include/dbus/org.freedesktop.NetworkManager.IP6Config.hpp
+2 -2
View File
@@ -44,9 +44,9 @@ std::string HtmlHelper::timeToHttpDate(std::filesystem::file_time_type time)
{
// Convert to time_t.
std::chrono::system_clock::time_point system_time = std::chrono::clock_cast<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);
}
+112 -92
View File
@@ -89,7 +89,6 @@ bool WifiConfigSettings::ValidateCountryCode(const std::string &text)
return std::isalpha(text[0]) && std::isalpha(text[1]);
}
void WifiConfigSettings::Load()
{
try
@@ -115,9 +114,9 @@ void WifiConfigSettings::Load()
}
static void openWithPerms(
pipedal::ofstream_synced &f,
const std::filesystem::path &path,
std::filesystem::perms perms =
pipedal::ofstream_synced &f,
const std::filesystem::path &path,
std::filesystem::perms perms =
std::filesystem::perms::owner_read | std::filesystem::perms::owner_write |
std::filesystem::perms::group_read | std::filesystem::perms::group_write)
{
@@ -132,13 +131,16 @@ static void openWithPerms(
f.open(path);
f.close();
}
try {
try
{
// set the perms.
std::filesystem::permissions(
path,
perms,
std::filesystem::perm_options::replace);
} catch (const std::exception&) {
std::filesystem::perm_options::replace);
}
catch (const std::exception &)
{
Lv2Log::warning(SS("Failed to set permissions on" << path << "."));
}
}
@@ -147,10 +149,10 @@ static void openWithPerms(
f.open(path);
}
void WifiConfigSettings::Save()
void WifiConfigSettings::Save()
{
WifiConfigSettings newSettings {*this};
WifiConfigSettings newSettings{*this};
// sync legacy settings, just in case i don't know what.
newSettings.mdnsName_ = newSettings.hotspotName_;
newSettings.enable_ = newSettings.IsEnabled();
@@ -163,12 +165,12 @@ void WifiConfigSettings::Save()
newSettings.password_ = oldSettings.password_;
}
newSettings.hasSavedPassword_ = newSettings.hasPassword_;
try
{
ofstream_synced f;
openWithPerms(f,CONFIG_PATH);
json_writer writer(f,false);
openWithPerms(f, CONFIG_PATH);
json_writer writer(f, false);
writer.write(&newSettings);
}
catch (const std::exception &e)
@@ -300,17 +302,14 @@ namespace pipedal::priv
connection["ipv4"]["method"] = sdbus::Variant(std::string("shared"));
connection["ipv6"]["method"] = sdbus::Variant(std::string("ignore"));
connection["ipv4"]["address-data"] = sdbus::Variant(std::vector<std::map<std::string, sdbus::Variant>>{{
{"address", sdbus::Variant("192.168.4.1")},
{"prefix", sdbus::Variant(uint32_t(24))}
}});
connection["ipv4"]["address-data"] = sdbus::Variant(std::vector<std::map<std::string, sdbus::Variant>>{{{"address", sdbus::Variant("192.168.4.1")},
{"prefix", sdbus::Variant(uint32_t(24))}}});
connection["ipv4"]["dhcp-send-hostname"] = sdbus::Variant(true);
connection["ipv4"]["dhcp-hostname"] = sdbus::Variant("raspberrypi");
connection["ipv4"]["dhcp-options"] = sdbus::Variant(std::vector<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,192.168.4.1"
});
});
auto connection_path = AddConnection(connection);
return connection_path;
@@ -326,7 +325,6 @@ namespace pipedal::priv
return active_connection_path;
}
void RemoveConnection(const std::string &connection)
{
auto connection_proxy = sdbus::createProxy(
@@ -429,7 +427,6 @@ namespace pipedal::priv
};
}
JSON_MAP_BEGIN(WifiConfigSettings)
// v0
JSON_MAP_REFERENCE(WifiConfigSettings, valid)
@@ -560,52 +557,51 @@ bool WifiConfigSettings::ValidateChannel(const std::string &countryCode, const s
return true;
}
static const char* trueValues[] {
static const char *trueValues[]{
"true",
"on",
"yes"
"1",
nullptr
};
static const char* falseValues[] {
nullptr};
static const char *falseValues[]{
"false",
"off",
"no",
"0",
nullptr
};
nullptr};
static bool Matches(const std::string&value, const char**matches)
static bool Matches(const std::string &value, const char **matches)
{
while(*matches)
while (*matches)
{
if (value == *matches) return true;
if (value == *matches)
return true;
++matches;
}
return false;
}
static bool TryStringToBool(const std::string &value, bool*outputValue)
static bool TryStringToBool(const std::string &value, bool *outputValue)
{
if (Matches(value,trueValues))
if (Matches(value, trueValues))
{
*outputValue = true;
return true;
}
if (Matches(value, falseValues)) {
if (Matches(value, falseValues))
{
*outputValue = false;
return true;
}
*outputValue = false;
return false;
}
static WifiConfigSettings::ssid_t readSsid(std::istream&ss)
static WifiConfigSettings::ssid_t readSsid(std::istream &ss)
{
using ssid_t = WifiConfigSettings::ssid_t;
char c;
ssid_t result;
while (ss.peek() == ' ')
{
@@ -624,28 +620,33 @@ static WifiConfigSettings::ssid_t readSsid(std::istream&ss)
break;
}
ss >> c;
switch (c) {
case 'n':
result.push_back((uint8_t)'\n');
break;
case 'r':
result.push_back((uint8_t)'\r');
break;
case 't':
result.push_back((uint8_t)'\t');
break;
case 'b':
result.push_back((uint8_t)'\b');
break;
default:
result.push_back((uint8_t)c);
break;
switch (c)
{
case 'n':
result.push_back((uint8_t)'\n');
break;
case 'r':
result.push_back((uint8_t)'\r');
break;
case 't':
result.push_back((uint8_t)'\t');
break;
case 'b':
result.push_back((uint8_t)'\b');
break;
default:
result.push_back((uint8_t)c);
break;
}
} else {
}
else
{
result.push_back((uint8_t)c);
}
}
} else {
}
else
{
while (!ss.eof())
{
if (c == ':')
@@ -662,7 +663,7 @@ static WifiConfigSettings::ssid_t readSsid(std::istream&ss)
}
return result;
}
static std::vector<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;
std::istringstream ss(value);
@@ -671,7 +672,7 @@ static std::vector<WifiConfigSettings::ssid_t> stringToSsidArray(const std::stri
while (true)
{
ssid_t ssid = readSsid(ss);
if (ssid.size() == 0)
if (ssid.size() == 0)
{
break;
}
@@ -683,16 +684,15 @@ static std::vector<WifiConfigSettings::ssid_t> stringToSsidArray(const std::stri
}
}
return result;
}
void WifiConfigSettings::ParseArguments(
const std::vector<std::string> &argv,
HotspotAutoStartMode startMode,
const std::string homeNetworkSsid
)
)
{
this->valid_ = false ;
this->valid_ = false;
if (argv.size() != 4)
{
throw invalid_argument("Invalid number of arguments.");
@@ -714,7 +714,6 @@ void WifiConfigSettings::ParseArguments(
this->hasPassword_ = this->password_.length() != 0;
this->hasSavedPassword_ = oldSettings.hasSavedPassword_;
if (!ValidateCountryCode(this->countryCode_))
{
throw invalid_argument("Invalid country code.");
@@ -723,7 +722,7 @@ void WifiConfigSettings::ParseArguments(
throw invalid_argument("Hotspot name is too long.");
if (this->hotspotName_.length() < 1)
throw invalid_argument("Hotspot name is too short.");
if (this->password_.size() != 0 && this->password_.size() < 8)
throw invalid_argument("Passphrase must be at least 8 characters long.");
@@ -742,12 +741,12 @@ void WifiConfigSettings::ParseArguments(
this->valid_ = true;
}
bool WifiConfigSettings::ConfigurationChanged(const WifiConfigSettings&other) const
bool WifiConfigSettings::ConfigurationChanged(const WifiConfigSettings &other) const
{
return !(
this->valid_ == other.valid_ &&
//this->rebootRequired_ == other.rebootRequired_ &&
//this->enable_ == other.enable_ &&
// this->rebootRequired_ == other.rebootRequired_ &&
// this->enable_ == other.enable_ &&
this->countryCode_ == other.countryCode_ &&
this->hotspotName_ == other.hotspotName_ &&
this->hasPassword_ == other.hasPassword_ &&
@@ -756,20 +755,18 @@ bool WifiConfigSettings::ConfigurationChanged(const WifiConfigSettings&other) co
this->homeNetwork_ == other.homeNetwork_ &&
this->autoStartMode_ == other.autoStartMode_ &&
this->hasSavedPassword_ == other.hasSavedPassword_
);
this->hasSavedPassword_ == other.hasSavedPassword_);
}
bool WifiConfigSettings::operator==(const WifiConfigSettings&other) const
bool WifiConfigSettings::operator==(const WifiConfigSettings &other) const
{
return !ConfigurationChanged(other) && this->wifiWarningGiven_ == other.wifiWarningGiven_;
}
static bool CanSeeHomeNetwork(const std::string&home, const std::vector<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;
}
@@ -778,38 +775,39 @@ static bool CanSeeHomeNetwork(const std::string&home, const std::vector<std::str
}
bool WifiConfigSettings::WantsHotspot(
bool ethernetConnected,
bool ethernetConnected,
const std::vector<std::string> &availableRememberedNetworks,
const std::vector<std::string> &availableNetworks)
{
if ((!this->valid_))
if ((!this->valid_))
return false;
HotspotAutoStartMode autoStartMode = (HotspotAutoStartMode)this->autoStartMode_;
switch (autoStartMode)
{
case HotspotAutoStartMode::Never:
default:
return false;
case HotspotAutoStartMode::Never:
default:
return false;
case HotspotAutoStartMode::NoEthernetConnection:
return !ethernetConnected;
case HotspotAutoStartMode::NotAtHome:
return !CanSeeHomeNetwork(this->homeNetwork_, availableNetworks);
case HotspotAutoStartMode::NoRememberedWifiConections:
return availableRememberedNetworks.size() == 0;
case HotspotAutoStartMode::Always:
return true;
case HotspotAutoStartMode::NoEthernetConnection:
return !ethernetConnected;
case HotspotAutoStartMode::NotAtHome:
return !CanSeeHomeNetwork(this->homeNetwork_, availableNetworks);
case HotspotAutoStartMode::NoRememberedWifiConections:
return availableRememberedNetworks.size() == 0;
case HotspotAutoStartMode::Always:
return true;
}
}
std::string pipedal::ssidToString(const std::vector<uint8_t> &ssid)
{
std::stringstream s;
for (auto v: ssid)
for (auto v : ssid)
{
if (v == 0) break; // breaks for some arguably legal ssids, but I can live with that.
if (v == 0)
break; // breaks for some arguably legal ssids, but I can live with that.
s << (char)v;
}
return s.str();
@@ -819,21 +817,43 @@ std::vector<std::string> pipedal::ssidToStringVector(const std::vector<std::vect
{
std::vector<std::string> result;
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));
}
return result;
}
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>> &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> 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 <string>
#include <alsa/asoundlib.h>
#include <alsa/seq.h>
#include <stdexcept>
#include <memory>
#include <functional>
#include "json.hpp"
namespace pipedal
{
// event messages starting with 0xFF are meta events.
// data not in midi format, is variable length, and private and the second byte is the MetaEventType.
enum class MetaEventType
{
None,
Tempo,
TimeSignature,
KeySignature,
SetPositionTick,
SetPositionTime
};
class AlsaSequencerPortSelection {
private:
std::string id_;
std::string name_;
int32_t sortOrder_ = 0;
public:
AlsaSequencerPortSelection() = default;
AlsaSequencerPortSelection(const std::string &id, const std::string &name, int32_t displaySortOrder)
: id_(id), name_(name), sortOrder_(displaySortOrder) {}
AlsaSequencerPortSelection(const AlsaSequencerPortSelection &other) = default;
AlsaSequencerPortSelection(AlsaSequencerPortSelection &&other) = default;
AlsaSequencerPortSelection &operator=(const AlsaSequencerPortSelection &other) = default;
AlsaSequencerPortSelection &operator=(AlsaSequencerPortSelection &&other) = default;
const std::string& id() const { return id_; }
void id(const std::string &value) { id_ = value; }
const std::string &name() const { return name_; }
void name(const std::string &value) { name_ = value; }
DECLARE_JSON_MAP(AlsaSequencerPortSelection);
};
class AlsaSequencerConfiguration {
private:
std::vector<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
{
std::string id;
@@ -53,28 +118,181 @@ namespace pipedal
bool isSpecific;
bool isHardware = false;
bool isSoftware = false;
bool isPort = false;
bool isVirtual = false;
int cardNumber = -1;
int32_t displaySortOrder = 0;
std::string rawMidiDevice; // e.g. "hw:0,0,0" for kernel devices
};
struct AlsaMidiMessage
{
uint32_t timestamp; // in microseconds (tick time if using queue)
AlsaMidiMessage()
{
size = 0;
data = fixedBuffer;
}
AlsaMidiMessage(uint8_t cc0)
{
size = 1;
data = fixedBuffer;
fixedBuffer[0] = cc0;
}
AlsaMidiMessage(uint8_t cc0, uint8_t cc1)
{
size = 2;
data = fixedBuffer;
fixedBuffer[0] = cc0;
fixedBuffer[1] = cc1;
}
AlsaMidiMessage(uint8_t cc0, uint8_t cc1, uint8_t cc2)
{
size = 3;
data = fixedBuffer;
fixedBuffer[0] = cc0;
fixedBuffer[1] = cc1;
fixedBuffer[2] = cc2;
}
AlsaMidiMessage(uint8_t *data, size_t size)
{
this->size = size;
this->data = data;
}
AlsaMidiMessage(const AlsaMidiMessage &other)
{
this->timestamp = other.timestamp;
this->realtime_sec = other.realtime_sec;
this->realtime_nsec = other.realtime_nsec;
if (other.size > 3)
{
this->size = other.size;
this->data = other.data;
}
else
{
this->size = other.size;
this->data = fixedBuffer;
for (size_t i = 0; i < other.size; ++i)
{
fixedBuffer[i] = other.fixedBuffer[i];
}
}
}
AlsaMidiMessage &operator=(const AlsaMidiMessage &other)
{
this->timestamp = other.timestamp;
this->realtime_sec = other.realtime_sec;
this->realtime_nsec = other.realtime_nsec;
if (other.size > 3)
{
this->size = other.size;
this->data = other.data;
}
else
{
this->size = other.size;
this->data = fixedBuffer;
for (size_t i = 0; i < other.size; ++i)
{
fixedBuffer[i] = other.fixedBuffer[i];
}
}
return *this;
}
AlsaMidiMessage(AlsaMidiMessage &&other)
{
this->timestamp = other.timestamp;
this->realtime_sec = other.realtime_sec;
this->realtime_nsec = other.realtime_nsec;
if (other.size > 3)
{
this->size = other.size;
this->data = other.data;
}
else
{
this->size = other.size;
this->data = fixedBuffer;
for (size_t i = 0; i < other.size; ++i)
{
fixedBuffer[i] = other.fixedBuffer[i];
}
}
}
AlsaMidiMessage &operator=(AlsaMidiMessage &&other)
{
this->timestamp = other.timestamp;
this->realtime_sec = other.realtime_sec;
this->realtime_nsec = other.realtime_nsec;
if (other.size > 3)
{
this->size = other.size;
this->data = other.data;
}
else
{
this->size = other.size;
this->data = fixedBuffer;
for (size_t i = 0; i < other.size; ++i)
{
fixedBuffer[i] = other.fixedBuffer[i];
}
}
return *this;
}
void Set(uint8_t cc0)
{
size = 1;
data = fixedBuffer;
fixedBuffer[0] = cc0;
}
void Set(uint8_t cc0, uint8_t cc1)
{
size = 2;
data = fixedBuffer;
fixedBuffer[0] = cc0;
fixedBuffer[1] = cc1;
}
void Set(uint8_t cc0, uint8_t cc1, uint8_t cc2)
{
size = 3;
data = fixedBuffer;
fixedBuffer[0] = cc0;
fixedBuffer[1] = cc1;
fixedBuffer[2] = cc2;
}
void Set(uint8_t *data, size_t size)
{
this->size = size;
this->data = data;
}
uint8_t cc0() const { return size > 0 ? data[0] : 0; }
uint8_t cc1() const { return size > 1 ? data[1] : 0; }
uint8_t cc2() const { return size > 2 ? data[2] : 0; }
uint32_t timestamp; // in microseconds (tick time if using queue)
uint64_t realtime_sec; // real-time timestamp seconds
uint32_t realtime_nsec; // real-time timestamp nanoseconds
uint8_t cc0;
uint8_t cc1;
uint8_t cc2;
size_t size = 0;
uint8_t *data = fixedBuffer;
uint8_t fixedBuffer[12];
};
/*
* AlsaSequencer - ALSA MIDI sequencer interface with real-time timestamp support
*
*
* Example usage with real-time timestamps:
*
*
* AlsaSequencer sequencer;
* sequencer.connect(clientId, portId);
*
*
* AlsaMidiMessage message;
* while (sequencer.ReadMessage(&message)) {
* // message.realtime_sec and message.realtime_nsec contain precise real-time timestamps
@@ -82,45 +300,58 @@ namespace pipedal
* printf("MIDI event at %lu.%09u seconds\n", message.realtime_sec, message.realtime_nsec);
* }
*/
class AlsaSequencer
{
protected:
AlsaSequencer() {}
public:
virtual ~AlsaSequencer() = default;
using self = AlsaSequencer;
using ptr = std::shared_ptr<self>;
static ptr Create();
static std::vector<AlsaSequencerPort> EnumeratePorts();
AlsaSequencer();
~AlsaSequencer();
public:
virtual void ConnectPort(int clientId, int portId) = 0;
virtual void ConnectPort(const std::string &id) = 0;
void ConnectPort(int clientId, int portId);
void ConnectPort(const std::string&name);
virtual void SetConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0;
// Read a single MIDI message from the sequencer input port
bool ReadMessage(AlsaMidiMessage &message);
// Read a single MIDI message from the sequencer input port. A timeout of -1 blocks indefinitely.
// A timeout of 0 returns immediately.
virtual bool ReadMessage(AlsaMidiMessage &message, int timeoutMs = -1) = 0;
// currently non-functional
virtual bool GetQueueRealtime(uint64_t *sec, uint32_t *nsec) = 0;
// Get current real-time from the queue (useful for calculating precise timing)
bool GetQueueRealtime(uint64_t* sec, uint32_t* nsec);
// Get the current queue ID (returns -1 if no queue is active)
int GetQueueId() const { return queueId; }
private:
// Create an ALSA input queue with real-time timestamps for the given client/port
int CreateRealtimeInputQueue();
struct Connection
{
int clientId;
int portId;
};
std::vector<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);
virtual void RemoveAllConnections() = 0;
};
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 ConfigurationChanged(const WifiConfigSettings&other) const;
bool WantsHotspot(bool ethernetConnected)
{
return WantsHotspot(ethernetConnected, std::vector<std::string>{}, std::vector<std::string>{});
}
bool WantsHotspot(
bool ethernetConnected,
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>> &availableNetworks // all visible networks.
);
bool NeedsScan() const;
bool NeedsWifi() const;
public:
DECLARE_JSON_MAP(WifiConfigSettings);
};
+2
View File
@@ -35,6 +35,8 @@
#include <string_view>
#include <string.h>
#include <stdexcept>
#include <vector>
#include <memory>
#define DECLARE_JSON_MAP(CLASSNAME) \
static pipedal::json_map::storage_type<CLASSNAME> jmap
@@ -30,6 +30,7 @@
#include <string>
#include <stdexcept>
#include <utility>
#include <memory>
#include "json.hpp"
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 \
authbind libavahi-client-dev libnm-dev libicu-dev \
libsdbus-c++-dev libzip-dev google-perftools \
libgoogle-perftools-dev
libgoogle-perftools-dev \
libpipewire-0.3-dev
### Installing Sources
+1 -1
View File
@@ -93,7 +93,7 @@ cabir:impulseFile3
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
rdfs:comment """
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:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
mod:brand "TooB";
mod:label "TooB CabSim";
@@ -53,7 +53,7 @@ toobimpulse:impulseFile
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
rdfs:comment """
Convolution reverb is a notoriously compute-intensive effect. If you are having performance issues, use the Max T control to constrain the length of the impulse file to
@@ -51,7 +51,7 @@ toobimpulse:impulseFile
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
rdfs:comment """
+1 -1
View File
@@ -66,7 +66,7 @@ inputStage:filterGroup
doap:license <https://two-play.com/TooB/licenses/isc> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
mod:brand "TooB";
mod:label "TooB Input";
+1 -1
View File
@@ -68,7 +68,7 @@ pstage:stage3
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
mod:brand "TooB";
mod:label "Power Stage";
+1 -1
View File
@@ -57,7 +57,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
rdfs:comment "TooB spectrum analyzer" ;
mod:brand "TooB";
+16 -5
View File
@@ -57,7 +57,7 @@ tonestack:eqGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
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" ;
rdf:value 2.0
];
],[
a lv2:InputPort ,
lv2:ControlPort ;
lv2:index 4 ;
lv2:symbol "gain" ;
lv2:name "Gain";
lv2:default 0.0 ;
lv2:minimum -40.0 ;
lv2:maximum 30.0;
units:unit units:db;
],
[
a lv2:AudioPort ,
lv2:InputPort ;
lv2:index 4 ;
lv2:index 5 ;
lv2:symbol "in" ;
lv2:name "In"
], [
a lv2:AudioPort ,
lv2:OutputPort ;
lv2:index 5 ;
lv2:index 6 ;
lv2:symbol "out" ;
lv2:name "Out"
],[
@@ -150,7 +161,7 @@ tone stack used in Polytone and HiWatt amps.
atom:bufferType atom:Sequence ;
# atom:supports patch:Message;
lv2:designation lv2:control ;
lv2:index 6 ;
lv2:index 7 ;
lv2:symbol "control" ;
lv2:name "Control" ;
rdfs:comment "Plugin to GUI communication" ;
@@ -160,7 +171,7 @@ tone stack used in Polytone and HiWatt amps.
atom:bufferType atom:Sequence ;
# atom:supports patch:Message;
lv2:designation lv2:control ;
lv2:index 7 ;
lv2:index 8 ;
lv2:symbol "notify" ;
lv2:name "Notify" ;
rdfs:comment "Plugin to GUI communication" ;
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
rdfs:comment """
Emulation of a Boss CE-2 Chorus.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
rdfs:comment """
A straightforward no-frills digital delay.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
rdfs:comment """
Emulation of a Boss BF-2 Flanger, based on circuit analysis and simulation of the original.
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
rdfs:comment """
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:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
rdfs:comment """
Toob Freeverb is an Lv2 implementation of the famous Freeverb reverb effect. FreeVerb delivers a well-balanced reverb with very little tonal coloration.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
rdfs:comment """
A 7 octave graphic equalizer, with frequencies chosen to be useful for EQ-ing guitar signals.
""" ;
+1 -1
View File
@@ -92,7 +92,7 @@ myprefix:output_group
doap:license <https://opensource.org/license/mit/> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 1 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
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:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 1 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
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:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
rdfs:comment """
The TooB ML Amplifier plugin provides emulation of a variety of amplifiers and overdrive pedals that are implemented
using neural-network-based machine learning models of real amplifiers.
+3 -3
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
rdfs:comment """
Remix a stereo input signal.
@@ -57,7 +57,7 @@ Remix a stereo input signal.
lv2:index 0 ;
lv2:symbol "trimL" ;
lv2:name "Trim L";
lv2:name "Vol L";
lv2:default 0.0 ;
lv2:minimum -60.0 ;
@@ -89,7 +89,7 @@ Remix a stereo input signal.
lv2:index 2 ;
lv2:symbol "trimR" ;
lv2:name "Trim R";
lv2:name "Vol R";
lv2:default 0.0 ;
lv2:minimum -60.0 ;
@@ -61,33 +61,43 @@ toobNam:eqGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
rdfs:comment """
A port of Steven Atkinson's Neural Amp Modeler to LV2.
TooB Neural Amp Modeler uses uploadable .nam model files. Download .nam files from http://tonehunt.org, and then load them into TooB Neural Amp Modeler.
TooB Neural Amp Modeler uses uploadable .nam model files. Download .nam model files from http://tone3000.com, and then load them into TooB Neural Amp Modeler.
If you are using TooB Neural Amp Modeler from PiPedal, download the model files to the system on which you are running the client. You must then
upload the file to the PiPedal server. Click on the "Model" control and then search for the Upload button in the file browser. Once uploaded to the server,
you can then select the uploaded file in the browser directly.
The TONE3000 website contains a huge collection of community-developed amp models that can be used with TooB Neural Amp Modeler. You can also use .nam models from
other sources. Using TONE3000 models with TooB Neural Amp Modeler is a two-step process. First, download model files to your local system using a
web browser. Then upload the model files to the PiPedal server using the PiPedal web interface. You can find an "Upload" button in
the file browser when you click on the "Model" control in the PiPedal web interface.
If you are not using PiPedal, just click on the Model control, and use the file browser to select the .nam file on your local system.
TooB Neural Amp Modeler supports a much wider range of amp models than ToobML, but usually uses much more CPU.
If you are not using PiPedal, just click on the Model control, and use the file browser to select the .nam file directly on your local system.
TooB Neural Amp Modeler supports a much wider range of amp models than ToobML, but usually uses more CPU.
You will need at least a Pi 4 to use Toob Neural Amp Modeler, and you may need to increase your audio buffer sizes to prevent overruns.
If you are having trouble with CPU usage, tonehunt.org does contain some smaller amp models. Search for the "feather" tag to find
models that use less CPU.
If you are having trouble with CPU usage, tone3000.com does contain some smaller amp models. Search for the "feather" tag to find
models that use less CPU. Generally, you can use up to three NAM models at once in a given preset on a Pi 4, and up to 5 MAM models
simultaneously on a Pi 5. N100 micro PCs could presumably do even better. However, exact CPU use varies depending on the complexity of the amp models you are using.
Users have reported success using NAM on Raspberry Pi 3 devices, but this is not recommended (or really supported), as CPU usage is very high on a Raspberry Pi 3.
If you are interested in profiling your own amps and effect pedals, please visit https://www.tone3000.com/capture
If you are interested in building your own amp models, please visit https://www.neuralampmodeler.com/
TooB Neural Amp Modeler contains code optimizations that allow use of a third model on Raspberry PI 4 devices. These optimizations
were offered to the upstream NAM project, but we recommended that they not be merged into upstream sources, because they
significantly affect maintainability of the codebase. and the performance increase they provide (about 30%) is really only
relevant on Raspberry PI 4-class devices. If you are interested in these optimizations, they have been published in ToobAmp
project sources on GitHub.
TooB Neural Amp Modeler uses code from the NeuralAmp Modeler Core project. The TooB Team wishes to express gratitude to Steven Atkinson for
making this extraordinary technology available as open-source code.
TooB Neural Amp Modeler uses code from the NeuralAmp Modeler Core project (https://www.neuralampmodeler.com/). The TooB team wishes
to express gratitude to Steven Atkinson for making this revolutionary technology available as open-source code.
Code from the the NeuralAmpModelerCore project (https://github.com/sdatkinson/NeuralAmpModelerCore) is provided under the following license.
Code from the the NeuralAmpModelerCore project (https://github.com/sdatkinson/NeuralAmpModelerCore) is provided
under the following license.
MIT License
Copyright (c) 2023 Steven Atkinson
Copyright (c) 2025 Steven Atkinson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+1 -1
View File
@@ -54,7 +54,7 @@ noisegate:envelope_group
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
rdfs:comment """
A noise gate is an audio processing tool that controls the volume of an audio signal
by allowing it to pass through only when it exceeds a set threshold.
+1 -1
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
rdfs:comment """
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 xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ui: <http://lv2plug.in/ns/extensions/ui#> .
@prefix pprop: <http://lv2plug.in/ns/ext/port-props#> .
@prefix pipedal_ui: <http://github.com/rerdavies/pipedal/ui#> .
@@ -52,7 +51,7 @@ recordPrefix:audioFile
doap:license <https://opensource.org/license/mit/> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 1 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
ui:ui <http://two-play.com/plugins/toob-record-mono-ui>;
@@ -61,7 +60,7 @@ recordPrefix:audioFile
patch:writable
recordPrefix:audioFile;
lv2:extensionData state:interface ;
lv2:extensionData state:interface, work:interface ;
rdfs:comment """
Record the plugin's audio input signal to a monophonic file. See also: TooB Record Input (stereo), which records a stereo file.
@@ -83,7 +82,7 @@ Record the plugin's audio input signal to a monophonic file. See also: TooB Reco
lv2:default 0.0 ;
lv2:minimum 0.0;
lv2:maximum 1.0;
lv2:portProperty lv2:toggled,pprop:trigger;
lv2:portProperty lv2:toggled,epp:trigger;
],
[
a lv2:InputPort ,
@@ -96,7 +95,7 @@ Record the plugin's audio input signal to a monophonic file. See also: TooB Reco
lv2:default 0.0 ;
lv2:minimum 0.0;
lv2:maximum 1.0;
lv2:portProperty lv2:toggled,pprop:trigger;
lv2:portProperty lv2:toggled,epp:trigger;
],
[
a lv2:OutputPort ,
@@ -117,11 +116,11 @@ Record the plugin's audio input signal to a monophonic file. See also: TooB Reco
lv2:index 3;
lv2:symbol "play" ;
lv2:name "⏵";
rdfs:comment "Preview recorded file. Click again to stop.";
rdfs:comment "Preview recorded file.";
lv2:default 0.0 ;
lv2:minimum 0.0;
lv2:maximum 1.0;
lv2:portProperty lv2:toggled,pprop:trigger;
lv2:portProperty lv2:toggled,epp:trigger;
],
[
a lv2:OutputPort ,
@@ -186,7 +185,7 @@ Record the plugin's audio input signal to a monophonic file. See also: TooB Reco
lv2:index 7;
lv2:symbol "level" ;
lv2:name "Level";
lv2:name "Rec Lvl";
rdfs:comment "Input trim level for recording";
lv2:default 0.0 ;
lv2:minimum -60.0;
+5 -5
View File
@@ -88,7 +88,7 @@ myprefix:loop3_group
doap:license <https://opensource.org/license/mit/> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 1 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
ui:ui <http://two-play.com/plugins/toob-record-stereo-ui>;
@@ -97,7 +97,7 @@ myprefix:loop3_group
patch:writable
recordPrefix:audioFile;
lv2:extensionData state:interface ;
lv2:extensionData state:interface, work:interface ;
rdfs:comment """
@@ -154,7 +154,7 @@ Record the plugin's audio input signal to a stereophonic file. See also: TooB Re
lv2:index 3;
lv2:symbol "play" ;
lv2:name "⏵";
rdfs:comment "Preview the recorded file. Click again to stop.";
rdfs:comment "Preview the recorded file.";
lv2:default 0.0 ;
lv2:minimum 0.0;
lv2:maximum 1.0;
@@ -223,8 +223,8 @@ Record the plugin's audio input signal to a stereophonic file. See also: TooB Re
lv2:index 7;
lv2:symbol "level" ;
lv2:name "Level";
lv2:name "Input trim level for recording";
lv2:name "Rec Lvl";
rdfs:comment "Input trim level for recording";
lv2:default 0.0 ;
lv2:minimum -60.0;
lv2:maximum 30.0;
+1 -1
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
rdfs:comment """
TooB Tuner is a chromatic guitar tuner.
""" ;
+2 -2
View File
@@ -31,7 +31,7 @@
<http://two-play.com/plugins/toob-volume>
a lv2:Plugin ,
lv2:UtilityPlugin ;
lv2:MixerPlugin ;
doap:name "TooB Volume" ,
"TooB Volume"@en-gb-gb
;
@@ -39,7 +39,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 62 ;
lv2:microVersion 63 ;
rdfs:comment """
Volume control.
+4
View File
@@ -97,6 +97,10 @@
lv2:binary <ToobAmp.so> ;
rdfs:seeAlso <ToobMix.ttl> .
<http://two-play.com/plugins/toob-player> a lv2:Plugin ;
lv2:binary <ToobAmp.so> ;
rdfs:seeAlso <ToobPlayer.ttl> .
<http://two-play.com/plugins/toob-graphiceq> a lv2:Plugin ;
lv2:binary <ToobAmp.so> ;
rdfs:seeAlso <ToobGraphicEq.ttl> .
+48 -301
View File
@@ -144,8 +144,6 @@ namespace pipedal
return false;
}
struct AudioFormat
{
char name[40];
@@ -304,12 +302,9 @@ namespace pipedal
AlsaDriverImpl(AudioDriverHost *driverHost)
: driverHost(driverHost)
{
midiEventMemory.resize(MAX_MIDI_EVENT * MAX_MIDI_EVENT_SIZE);
midiEventMemoryIndex = 0;
midiEventMemory.resize(MIDI_MEMORY_BUFFER_SIZE);
midiEvents.resize(MAX_MIDI_EVENT);
for (size_t i = 0; i < midiEvents.size(); ++i)
{
midiEvents[i].buffer = midiEventMemory.data() + i * MAX_MIDI_EVENT_SIZE;
}
}
virtual ~AlsaDriverImpl()
{
@@ -415,14 +410,7 @@ namespace pipedal
snd_pcm_sw_params_free(playbackSwParams);
playbackSwParams = nullptr;
}
for (auto &midiState : this->midiDevices)
{
if (midiState)
{
midiState->Close();
}
}
midiDevices.resize(0);
this->alsaSequencer = nullptr;
}
std::string discover_alsa_using_apps()
@@ -913,7 +901,7 @@ namespace pipedal
std::vector<float *> &buffers = this->playbackBuffers;
int channels = this->playbackChannels;
constexpr double scale = 0x00FFFFFF;
constexpr double scale = 0x00FFFFFF;
for (size_t frame = 0; frame < frames; ++frame)
{
for (int channel = 0; channel < channels; ++channel)
@@ -1084,7 +1072,6 @@ namespace pipedal
open = true;
try
{
OpenMidi(jackServerSettings, channelSelection);
OpenAudio(jackServerSettings, channelSelection);
std::atomic_thread_fence(std::memory_order::release);
}
@@ -1531,18 +1518,43 @@ namespace pipedal
} while (frames > 0);
return framesRead;
}
protected:
void ReadMidiData(uint32_t audioFrame)
{
for (size_t i = 0; i < midiDevices.size(); ++i)
AlsaMidiMessage message;
midiEventCount = 0;
while(alsaSequencer->ReadMessage(message,0))
{
size_t nRead = midiDevices[i]->ReadMidiEvents(
this->midiEvents,
midiEventCount,
audioFrame);
midiEventCount += nRead;
size_t messageSize = message.size;
if (messageSize == 0)
{
continue;
}
if (midiEventMemoryIndex + messageSize >= this->midiEventMemory.size()) {
continue;
}
if (midiEventCount >= this->midiEvents.size()) {
midiEvents.resize(midiEventCount*2);
}
// for now, prevent META event messages from propagating.
if (message.data[0] == 0xFF && message.size > 1) {
continue;
}
MidiEvent *pEvent = midiEvents.data() + midiEventCount++;
pEvent->time = audioFrame;
pEvent->size = messageSize;
pEvent->buffer = midiEventMemory.data() + midiEventMemoryIndex;
memcpy(
midiEventMemory.data() + midiEventMemoryIndex,
message.data,
message.size);
midiEventMemoryIndex += messageSize;
}
}
private:
long WriteBuffer(snd_pcm_t *handle, uint8_t *buf, size_t frames)
{
@@ -1671,16 +1683,17 @@ namespace pipedal
pBuffer[j] = 0;
}
}
try {
try
{
while (!terminateAudio())
{
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// zero out input buffers.
this->driverHost->OnProcess(this->bufferSize);
}
} catch (const std::exception &e)
}
catch (const std::exception &e)
{
}
}
this->driverHost->OnAudioTerminated();
@@ -1766,288 +1779,22 @@ namespace pipedal
Lv2Log::debug("Audio thread joined.");
}
static constexpr size_t MAX_MIDI_EVENT_SIZE = 3;
static constexpr size_t MIDI_BUFFER_SIZE = 16 * 1024;
static constexpr size_t MIDI_MEMORY_BUFFER_SIZE = 32 * 1024;
static constexpr size_t MAX_MIDI_EVENT = 4 * 1024;
size_t midiEventCount = 0;
std::vector<MidiEvent> midiEvents;
size_t midiEventMemoryIndex = 0;
std::vector<uint8_t> midiEventMemory;
AlsaSequencer::ptr alsaSequencer;
public:
class AlsaMidiDeviceImpl
virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override
{
private:
snd_rawmidi_t *hIn = nullptr;
snd_rawmidi_params_t *hInParams = nullptr;
std::string deviceName;
// running status state.
uint8_t runningStatus = 0;
int dataLength = 0;
int dataIndex = 0;
size_t statusBytesRemaining = 0;
size_t data0 = 0;
size_t data1 = 0;
bool inputProcessingSysex = false;
size_t inputSysexBufferCount = 0;
std::vector<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());
}
}
this->alsaSequencer = alsaSequencer;
}
virtual size_t InputBufferCount() const { return activeCaptureBuffers.size(); }
+2 -2
View File
@@ -25,7 +25,7 @@
#include "JackConfiguration.hpp"
#include <functional>
#include "AlsaSequencer.hpp"
@@ -74,7 +74,7 @@ namespace pipedal {
virtual float*GetOutputBuffer(size_t channe) = 0;
virtual void Open(const JackServerSettings & jackServerSettings,const JackChannelSelection &channelSelection) = 0;
virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) = 0;
virtual void Activate() = 0;
virtual void Deactivate() = 0;
virtual void Close() = 0;
+4 -2
View File
@@ -33,6 +33,7 @@
#include "Lv2Log.hpp"
#include "ss.hpp"
#include "util.hpp"
#include <algorithm>
#undef _GLIBCXX_DEBUG // Ensure we are not in debug mode, as this file is not compatible with it.
#include "SQLiteCpp/SQLiteCpp.h"
@@ -112,8 +113,9 @@ namespace
static int64_t fileTimeToInt64(const fs::file_time_type &fileTime)
{
std::chrono::system_clock::time_point system_time = std::chrono::clock_cast<std::chrono::system_clock>(fileTime);
auto result = std::chrono::duration_cast<std::chrono::milliseconds>(system_time.time_since_epoch()).count();
auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
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;
}
static int64_t GetLastWriteTime(const fs::path &file)
+45 -3
View File
@@ -21,6 +21,7 @@
#include "util.hpp"
#include <lv2/atom/atom.h>
#include "SchedulerPriority.hpp"
#include "AlsaSequencer.hpp"
#include "Lv2Log.hpp"
@@ -388,6 +389,9 @@ bool SystemMidiBinding::IsMatch(const MidiEvent &event)
class AudioHostImpl : public AudioHost, private AudioDriverHost, private IPatchWriterCallback
{
private:
AlsaSequencer::ptr alsaSequencer;
AlsaSequencerDeviceMonitor::ptr alsaDeviceMonitor;
void OnWritePatchPropertyBuffer(
PatchPropertyWriter::Buffer *);
@@ -529,7 +533,7 @@ private:
std::string GetAtomObjectType(uint8_t *pData)
{
LV2_Atom_Object *pAtom = (LV2_Atom_Object *)pData;
LV2_Atom_Object *pAtom = (LV2_Atom_Object *)pData;
if (pAtom->atom.type != uris.atom_Object)
{
throw std::invalid_argument("Not an Lv2 Object");
@@ -1248,12 +1252,26 @@ public:
lv2_atom_forge_init(&inputWriterForge, pHost->GetMapFeature().GetMap());
cpuTemperatureMonitor = CpuTemperatureMonitor::Get();
this->alsaSequencer = AlsaSequencer::Create();
this->alsaDeviceMonitor = AlsaSequencerDeviceMonitor::Create();
this->alsaDeviceMonitor->StartMonitoring(
[this](
AlsaSequencerDeviceMonitor::MonitorAction action,
int client,
const std::string &clientName)
{
HandleAlsaSequencerDevicesChanged(action, client, clientName);
}
);
}
virtual ~AudioHostImpl()
{
alsaDeviceMonitor->StopMonitoring();
Close();
CleanRestartThreads(true);
audioDriver = nullptr;
this->alsaSequencer = nullptr;
}
virtual JackConfiguration GetServerConfiguration()
@@ -1268,7 +1286,21 @@ public:
{
return this->sampleRate;
}
void HandleAlsaSequencerDevicesChanged(
AlsaSequencerDeviceMonitor::MonitorAction action, int client, const std::string &clientName)
{
if (pNotifyCallbacks)
{
if (action == AlsaSequencerDeviceMonitor::MonitorAction::DeviceRemoved)
{
pNotifyCallbacks->OnAlsaSequencerDeviceRemoved(client);
}
else if (action == AlsaSequencerDeviceMonitor::MonitorAction::DeviceAdded)
{
pNotifyCallbacks->OnAlsaSequencerDeviceAdded(client,clientName);
}
}
}
void HandleAudioTerminatedAbnormally()
{
Lv2Log::error("Audio processing terminated unexpectedly.");
@@ -1666,7 +1698,7 @@ public:
try
{
audioDriver->Open(jackServerSettings, this->channelSelection);
audioDriver->SetAlsaSequencer(this->alsaSequencer);
this->sampleRate = audioDriver->GetSampleRate();
this->overrunGracePeriodSamples = (uint64_t)(((uint64_t)this->sampleRate) * OVERRUN_GRACE_PERIOD_S);
@@ -1814,6 +1846,9 @@ public:
}
}
virtual void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) override;
void OnNotifyPathPatchPropertyReceived(
int64_t instanceId,
const std::string &pathPatchPropertyUri,
@@ -2261,6 +2296,13 @@ void AudioHostImpl::OnWritePatchPropertyBuffer(
this->realtimeWriter.SendPathPropertyBuffer(buffer);
}
void AudioHostImpl::SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration)
{
this->alsaSequencer->SetConfiguration(alsaSequencerConfiguration);
}
JSON_MAP_BEGIN(JackHostStatus)
JSON_MAP_REFERENCE(JackHostStatus, active)
JSON_MAP_REFERENCE(JackHostStatus, errorMessage)
+4
View File
@@ -39,6 +39,7 @@ namespace pipedal
struct RealtimeNextMidiProgramRequest;
class PluginHost;
class Pedalboard;
class AlsaSequencerConfiguration;
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 OnAlsaDriverTerminatedAbnormally() = 0;
virtual void OnAlsaSequencerDeviceAdded(int client, const std::string &clientName) = 0;
virtual void OnAlsaSequencerDeviceRemoved(int client) = 0;
};
class JackHostStatus
@@ -225,6 +228,7 @@ namespace pipedal
virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) = 0;
virtual void Close() = 0;
virtual void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0;
virtual uint32_t GetSampleRate() = 0;
virtual JackConfiguration GetServerConfiguration() = 0;
+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(CXX_STANDARD 20)
find_package(PkgConfig REQUIRED)
pkg_check_modules(PipeWire REQUIRED libpipewire-0.3)
message(STATUS "PipeWire_INCLUDE_DIRS: ${PipeWire_INCLUDE_DIRS}"
"PipeWire_LIBRARIES: ${PipeWire_LIBRARIES}"
"PipeWire_CFLAGS: ${PipeWire_CFLAGS}"
"PipeWire_DEFINES: ${PipeWire_DEFINES}"
"PipeWire_VERSION: ${PipeWire_VERSION}")
include(FetchContent)
@@ -196,6 +206,7 @@ else()
endif()
set (PIPEDAL_SOURCES
PipewireInputStream.cpp PipewireInputStream.hpp
AudioFiles.cpp AudioFiles.hpp
AudioFileMetadataReader.cpp AudioFileMetadataReader.hpp
AudioFileMetadata.hpp AudioFileMetadata.cpp
@@ -217,7 +228,6 @@ set (PIPEDAL_SOURCES
Lv2PluginChangeMonitor.cpp Lv2PluginChangeMonitor.hpp
WebServerConfig.cpp WebServerConfig.hpp
Locale.hpp Locale.cpp
Finally.hpp
ZipFile.cpp ZipFile.hpp
TemporaryFile.cpp TemporaryFile.hpp
FilePropertyDirectoryTree.cpp FilePropertyDirectoryTree.hpp
@@ -314,6 +324,7 @@ set (PIPEDAL_INCLUDES
${JACK_INCLUDE_DIRS} ${LILV_0_INCLUDE_DIRS}
${VST3_INCLUDES}
${WEBSOCKETPP_INCLUDE_DIRS}
${PipeWire_INCLUDE_DIRS}
.
)
@@ -323,6 +334,7 @@ set(PIPEDAL_LIBS libpipedald zip
pthread atomic stdc++fs asound avahi-common avahi-client systemd
${VST3_LIBRARIES}
${LILV_0_LIBRARIES}
asound
# ${JACK_LIBRARIES} - pending delete for JACK support.
)
@@ -332,13 +344,17 @@ set(PIPEDAL_LIBS libpipedald zip
add_library(libpipedald STATIC ${PIPEDAL_SOURCES})
target_include_directories(libpipedald PRIVATE ${PIPEDAL_INCLUDES}
target_compile_definitions(libpipedald PUBLIC "_REENTRANT")
target_include_directories(libpipedald PUBLIC ${PIPEDAL_INCLUDES}
)
target_link_libraries(libpipedald
PUBLIC
PiPedalCommon
SQLiteCpp)
SQLiteCpp
${PipeWire_LIBRARIES}
)
if(${USE_PCH})
target_precompile_headers(libpipedald PRIVATE pch.h)
@@ -360,6 +376,7 @@ target_link_libraries(pipedal_kconfig
PRIVATE ftxui::dom
PRIVATE ftxui::component # Not needed for this example.
PRIVATE PiPedalCommon
asound
systemd
)
@@ -378,6 +395,7 @@ target_link_libraries(pipedald PRIVATE PiPedalCommon
${PIPEDAL_LIBS}
)
#################################
add_executable(hotspotManagerTest
hotspotManagerTestMain.cpp
@@ -385,7 +403,7 @@ add_executable(hotspotManagerTest
target_link_libraries(hotspotManagerTest PRIVATE ${PIPEDAL_LIBS})
set_target_properties(hotspotManagerTest PROPERTIES EXCLUDE_FROM_ALL true)
set_target_properties(hotspotManagerTest PROPERTIES EXCLUDE_FROM_ALL ${PIPEDAL_EXCLUDE_TESTS})
add_executable(AuxInTest
@@ -396,6 +414,7 @@ add_executable(AuxInTest
add_executable(pipedaltest
testMain.cpp
PipewireInputStreamTest.cpp
AudioFilesTest.cpp
LRUCacheTest.cpp
@@ -426,7 +445,7 @@ target_link_libraries(pipedaltest PRIVATE ${PIPEDAL_LIBS} ${ICU_LIBRARIES})
target_include_directories(pipedaltest PRIVATE ${PIPEDAL_INCLUDES}
)
set_target_properties(pipedaltest PROPERTIES EXCLUDE_FROM_ALL true)
set_target_properties(pipedaltest PROPERTIES EXCLUDE_FROM_ALL ${PIPEDAL_EXCLUDE_TESTS})
if (NOT GITHUB_ACTIONS) # Google perftools are not available on Github action servers.
@@ -462,7 +481,7 @@ target_link_libraries(jsonTest PRIVATE PiPedalCommon)
target_include_directories(jsonTest PRIVATE ${PIPEDAL_INCLUDES}
)
set_target_properties(jsonTest PROPERTIES EXCLUDE_FROM_ALL true)
set_target_properties(jsonTest PROPERTIES EXCLUDE_FROM_ALL ${PIPEDAL_EXCLUDE_TESTS})
target_link_libraries(jsonTest PRIVATE pthread)
+66 -7
View File
@@ -104,6 +104,11 @@ namespace pipedal
captureChannels = channels;
playbackChannels = channels;
midiEventMemoryIndex = 0;
midiEventMemory.resize(MIDI_MEMORY_BUFFER_SIZE);
midiEvents.resize(MAX_MIDI_EVENT);
}
virtual ~DummyDriverImpl()
{
@@ -122,6 +127,16 @@ namespace pipedal
}
JackServerSettings jackServerSettings;
AlsaSequencer::ptr alsaSequencer;
static constexpr size_t MIDI_MEMORY_BUFFER_SIZE = 32 * 1024;
static constexpr size_t MAX_MIDI_EVENT = 4 * 1024;
size_t midiEventCount = 0;
std::vector<MidiEvent> midiEvents;
size_t midiEventMemoryIndex = 0;
std::vector<uint8_t> midiEventMemory;
unsigned int periods = 0;
@@ -158,11 +173,13 @@ namespace pipedal
}
}
virtual size_t GetMidiInputEventCount() override {
return 0;
virtual size_t GetMidiInputEventCount() override
{
return midiEventCount;
}
virtual MidiEvent*GetMidiEvents() {
return nullptr;
virtual MidiEvent *GetMidiEvents() override
{
return this->midiEvents.data();
}
@@ -190,7 +207,11 @@ namespace pipedal
throw;
}
}
virtual std::string GetConfigurationDescription()
virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override
{
this->alsaSequencer = alsaSequencer;
}
virtual std::string GetConfigurationDescription()
{
std::string result = SS(
"DUMMY, "
@@ -219,6 +240,43 @@ namespace pipedal
bool block = false;
void ReadMidiData(uint32_t audioFrame)
{
AlsaMidiMessage message;
midiEventCount = 0;
while(alsaSequencer->ReadMessage(message,0))
{
size_t messageSize = message.size;
if (messageSize == 0)
{
continue;
}
if (midiEventMemoryIndex + messageSize >= this->midiEventMemory.size()) {
continue;
}
if (midiEventCount >= this->midiEvents.size()) {
midiEvents.resize(midiEventCount*2);
}
// for now, prevent META event messages from propagating.
if (message.data[0] == 0xFF && message.size > 1) {
continue;
}
MidiEvent *pEvent = midiEvents.data() + midiEventCount++;
pEvent->time = audioFrame;
pEvent->size = messageSize;
pEvent->buffer = midiEventMemory.data() + midiEventMemoryIndex;
memcpy(
midiEventMemory.data() + midiEventMemoryIndex,
message.data,
message.size);
midiEventMemoryIndex += messageSize;
}
}
void AudioThread()
{
SetThreadName("dummyAudioDriver");
@@ -231,11 +289,14 @@ namespace pipedal
while (true)
{
if (terminateAudio())
{
break;
}
ReadMidiData((uint32_t)0);
ssize_t framesRead = this->bufferSize;
this->driverHost->OnProcess(framesRead);
@@ -335,8 +396,6 @@ namespace pipedal
Lv2Log::debug("Audio thread joined.");
}
static constexpr size_t MIDI_BUFFER_SIZE = 16 * 1024;
static constexpr size_t MAX_MIDI_EVENT = 4 * 1024;
public:
+38 -19
View File
@@ -80,7 +80,7 @@ namespace pipedal::impl
bool hasWifi = false;
HasWifiListener hasWifiListener;
void SetHasWifi(bool hasWifi);
virtual bool GetHasWifi() override ;
virtual bool GetHasWifi() override;
void onClose();
void onError(const std::string &message);
@@ -347,7 +347,7 @@ void HotspotManagerImpl::onStartMonitoring()
this->networkManager = NetworkManager::Create(dbusDispatcher);
if (!networkManager->WirelessEnabled())
if (!networkManager->WirelessEnabled() && wifiConfigSettings.NeedsWifi())
{
networkManager->WirelessEnabled(true);
}
@@ -483,6 +483,7 @@ void HotspotManagerImpl::onReload()
// force a reload.
StopHotspot();
SetState(State::Monitoring);
StartScanTimer(); // or stop it as the case may be.
MaybeStartHotspot();
return;
}
@@ -758,16 +759,26 @@ void HotspotManagerImpl::MaybeStartHotspot()
// devices are transitioning. Do nothing.
return;
}
std::vector<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)
{
StartHotspot();
@@ -996,8 +1007,14 @@ void HotspotManagerImpl::ScanNow()
try
{
Lv2Log::debug("Scanning");
wlanWirelessDevice->RequestScan(options);
if (this->wifiConfigSettings.NeedsScan())
{
Lv2Log::debug("Scanning");
wlanWirelessDevice->RequestScan(options);
} else {
return;
}
}
catch (const std::exception &e)
{
@@ -1036,7 +1053,8 @@ void HotspotManagerImpl::SetHasWifiListener(HasWifiListener &&listener)
this->hasWifiListener(this->hasWifi);
}
}
bool HotspotManagerImpl::GetHasWifi() {
bool HotspotManagerImpl::GetHasWifi()
{
std::lock_guard<std::recursive_mutex> lock{this->networkChangingListenerMutex};
return hasWifi;
}
@@ -1112,17 +1130,20 @@ static bool IsNetworkManagerRunning()
return gUsingNetworkManager;
}
bool bResult = false;
auto result = sysExecForOutput("systemctl","is-active NetworkManager");
auto result = sysExecForOutput("systemctl", "is-active NetworkManager");
if (result.exitCode == EXIT_SUCCESS)
{
std::string text = result.output.erase(result.output.find_last_not_of(" \n\r\t")+1);
std::string text = result.output.erase(result.output.find_last_not_of(" \n\r\t") + 1);
if (text == "active")
{
bResult = true;
} else if (text == "inactive")
}
else if (text == "inactive")
{
bResult = false;
} else {
}
else
{
throw std::runtime_error(SS("UsingNetworkManager: unexpected result (" << text << ")"));
}
gNetworkManagerTestExecuted = true;
@@ -1134,19 +1155,17 @@ static bool IsNetworkManagerRunning()
return false;
}
bool HotspotManager::HasWifiDevice()
{
// use procfs to decide this, as NetworkManager may not be available yet.
if ( !(get_wireless_interfaces_sysfs().empty()) )
if (!(get_wireless_interfaces_sysfs().empty()))
{
return false;
}
if (!IsNetworkManagerRunning())
if (!IsNetworkManagerRunning())
{
return false;
}
return true;
}
+19 -1
View File
@@ -23,6 +23,7 @@
#include "AlsaDriver.hpp"
#include "Lv2Log.hpp"
#include "PiPedalException.hpp"
#include "AlsaSequencer.hpp"
#if JACK_HOST
@@ -106,6 +107,23 @@ namespace pipedal {
#endif
}
static std::vector<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(
const JackServerSettings &jackServerSettings)
{
@@ -116,7 +134,7 @@ void JackConfiguration::AlsaInitialize(
{
this->inputMidiDevices_.clear();
} else {
this->inputMidiDevices_ = GetAlsaMidiInputDevices();
this->inputMidiDevices_ = GetAlsaSequencers(); // NB: Sequencers, not rawmidi devices, anymore.
}
if (jackServerSettings.IsValid())
{
+7 -1
View File
@@ -107,7 +107,13 @@ namespace pipedal
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_;
}
+217 -5
View File
@@ -47,6 +47,7 @@ void PiPedalAlsaDevices::cacheDevice(const std::string &name, const AlsaDeviceIn
std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
{
std::lock_guard guard{alsaMutex};
std::vector<AlsaDeviceInfo> result;
@@ -118,7 +119,7 @@ std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
if (err == 0)
{
unsigned int minRate = 0, maxRate = 0;
snd_pcm_uframes_t minBufferSize = 0, maxBufferSize = 0;
snd_pcm_uframes_t minBufferSize = 0, maxBufferSize = 0;
int dir;
err = snd_pcm_hw_params_get_rate_min(params, &minRate, &dir);
if (err == 0)
@@ -143,7 +144,8 @@ std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
info.sampleRates_.push_back(rate);
}
}
if (minBufferSize < 16) {
if (minBufferSize < 16)
{
minBufferSize = 16;
}
@@ -180,7 +182,207 @@ std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
return result;
}
static std::vector<AlsaMidiDeviceInfo> GetAlsaDevices(const char *devname, const char *direction)
static void AddMidiCardDevicesToList(snd_ctl_t *ctl, int card, int device, AlsaMidiDeviceInfo::Direction direction, std::vector<AlsaMidiDeviceInfo> *result)
{
snd_rawmidi_info_t *info = nullptr;
const char *name = nullptr;
const char *sub_name = nullptr;
int subs = 0, subs_in = 0, subs_out = 0;
int sub = 0;
int err = 0;
;
snd_rawmidi_info_alloca(&info);
snd_rawmidi_info_set_device(info, device);
snd_rawmidi_info_set_stream(info, SND_RAWMIDI_STREAM_INPUT);
err = snd_ctl_rawmidi_info(ctl, info);
if (err >= 0)
subs_in = snd_rawmidi_info_get_subdevices_count(info);
else
subs_in = 0;
snd_rawmidi_info_set_stream(info, SND_RAWMIDI_STREAM_OUTPUT);
err = snd_ctl_rawmidi_info(ctl, info);
if (err >= 0)
subs_out = snd_rawmidi_info_get_subdevices_count(info);
else
subs_out = 0;
subs = subs_in > subs_out ? subs_in : subs_out;
if (!subs)
return;
switch (direction)
{
case AlsaMidiDeviceInfo::In:
if (subs_out == 0) // out for the device, in for us.
{
return; // no input devices to add
}
break;
case AlsaMidiDeviceInfo::Out: // in for the device, out for us.
if (subs_in == 0)
{
return; // no output devices to add
}
break;
case AlsaMidiDeviceInfo::InOut:
if (subs_in == 0 || subs_out == 0)
{
return; // no input or output devices to add
}
break;
case AlsaMidiDeviceInfo::None:
return; // no devices to add
}
for (sub = 0; sub < subs; ++sub)
{
snd_rawmidi_info_set_stream(info, direction == AlsaMidiDeviceInfo::Direction::Out ? SND_RAWMIDI_STREAM_INPUT : SND_RAWMIDI_STREAM_OUTPUT);
snd_rawmidi_info_set_subdevice(info, sub);
err = snd_ctl_rawmidi_info(ctl, info);
if (err < 0)
{
throw std::runtime_error(
SS("snd_ctl_rawmidi_info failed for card " << card << ", device " << device << ", subdevice " << sub << ": " << snd_strerror(err)));
}
name = snd_rawmidi_info_get_name(info);
sub_name = snd_rawmidi_info_get_subdevice_name(info);
// get card name.
std::string cardName;
snd_ctl_card_info_t *card_info = nullptr;
snd_ctl_card_info_malloc(&card_info);
if (snd_ctl_card_info(ctl, card_info) == 0) {
cardName = snd_ctl_card_info_get_name(card_info);
}
snd_ctl_card_info_free(card_info);
if (cardName.length() == 0)
{
return;
}
if (sub == 0 && sub_name[0] == '\0')
{
AlsaMidiDeviceInfo info;
info.name_ = SS("hw:CARD=" << cardName << ",DEV=" << device);
info.description_ = SS("Virtual MIDI " << card << "-" << device);
info.card_ = card;
info.device_ = device;
info.subdevice_ = 0;
info.isVirtual_ = true;
info.subDevices_ = subs;
if (subs_in > 0 && subs_out > 0)
{
info.direction_ = AlsaMidiDeviceInfo::InOut;
}
else if (subs_in > 0)
{
info.direction_ = AlsaMidiDeviceInfo::In;
}
else if (subs_out > 0)
{
info.direction_ = AlsaMidiDeviceInfo::Out;
}
else
{
info.direction_ = AlsaMidiDeviceInfo::None;
}
result->push_back(info);
break;
}
else
{
AlsaMidiDeviceInfo info;
info.name_ = SS("hw:CARD=" << cardName << ",DEV=" << device);
if (sub != 0) {
info.name_ = SS(info.name_ << "," << sub);
}
info.description_ = sub_name;
info.card_ = card;
info.device_ = device;
info.subdevice_ = sub;
info.isVirtual_ = false;
info.subDevices_ = 1;
result->push_back(info);
}
}
}
static void AddMidiCardToList(int card, std::vector<AlsaMidiDeviceInfo> *result, AlsaMidiDeviceInfo::Direction direction)
{
snd_ctl_t *ctl = nullptr;
char name[32];
int device;
int err;
sprintf(name, "hw:%d", card);
if ((err = snd_ctl_open(&ctl, name, 0)) < 0)
{
throw std::runtime_error(SS("cannot open control for card " << card << ": " << snd_strerror(err)));
}
device = -1;
for (;;)
{
if ((err = snd_ctl_rawmidi_next_device(ctl, &device)) < 0)
{
snd_ctl_close(ctl);
throw std::runtime_error(SS("cannot get next rawmidi device for card " << card << ": " << snd_strerror(err)));
}
if (device < 0)
break;
AddMidiCardDevicesToList(ctl, card, device, direction, result);
}
snd_ctl_close(ctl);
}
static std::vector<AlsaMidiDeviceInfo> GetAlsaDevices(const char *devname, const char *direction_)
{
std::vector<AlsaMidiDeviceInfo> result;
int card, err;
AlsaMidiDeviceInfo::Direction direction;
if (strcmp(direction_, "Input") == 0)
{
direction = AlsaMidiDeviceInfo::In;
}
else if (strcmp(direction_, "Output") == 0)
{
direction = AlsaMidiDeviceInfo::Out;
}
else if (strcmp(direction_, "InOut") == 0)
{
direction = AlsaMidiDeviceInfo::InOut;
}
else
{
direction = AlsaMidiDeviceInfo::None;
return result;
}
card = -1;
if ((err = snd_card_next(&card)) < 0)
{
throw std::runtime_error(SS("snd_card_next failed.: " << snd_strerror(err)));
}
if (card < 0)
{
// no devices!
return result;
}
do
{
AddMidiCardToList(card, &result, direction);
if ((err = snd_card_next(&card)) < 0)
{
throw std::runtime_error(SS("snd_card_next failed: " << snd_strerror(err)));
}
} while (card >= 0);
return result;
}
static std::vector<AlsaMidiDeviceInfo> OldGetAlsaDevices(const char *devname, const char *direction)
{
std::vector<AlsaMidiDeviceInfo> result;
{
@@ -214,6 +416,8 @@ static std::vector<AlsaMidiDeviceInfo> GetAlsaDevices(const char *devname, const
result.push_back(AlsaMidiDeviceInfo(name, desc));
}
}
// translate rawmidi device name to device number.
if (name && strcmp("null", name) != 0)
free(name);
if (desc && strcmp("null", desc) != 0)
@@ -229,11 +433,11 @@ static std::vector<AlsaMidiDeviceInfo> GetAlsaDevices(const char *devname, const
return result;
}
std::vector<AlsaMidiDeviceInfo> pipedal::GetAlsaMidiInputDevices()
std::vector<AlsaMidiDeviceInfo> pipedal::LegacyGetAlsaMidiInputDevices()
{
return GetAlsaDevices("rawmidi", "Input");
}
std::vector<AlsaMidiDeviceInfo> pipedal::GetAlsaMidiOutputDevices()
std::vector<AlsaMidiDeviceInfo> pipedal::LegacyGetAlsaMidiOutputDevices()
{
return GetAlsaDevices("rawmidi", "Output");
}
@@ -260,6 +464,14 @@ AlsaMidiDeviceInfo::AlsaMidiDeviceInfo(const char *name, const char *description
}
}
AlsaMidiDeviceInfo::AlsaMidiDeviceInfo(const char *name, const char *description, int card, int device, int subdevice)
: AlsaMidiDeviceInfo(name, description)
{
this->card_ = card;
this->device_ = device;
this->subdevice_ = subdevice;
}
JSON_MAP_BEGIN(AlsaDeviceInfo)
JSON_MAP_REFERENCE(AlsaDeviceInfo, cardId)
JSON_MAP_REFERENCE(AlsaDeviceInfo, id)
+20 -2
View File
@@ -41,11 +41,26 @@ namespace pipedal {
};
class AlsaMidiDeviceInfo {
public:
enum Direction {
None = 0,
In = 1,
Out = 2,
InOut = 3
};
AlsaMidiDeviceInfo() { }
AlsaMidiDeviceInfo(const char*name, const char*description);
AlsaMidiDeviceInfo(const char*name, const char*description, int card, int device, int subdevice);
std::string name_;
std::string description_;
// non-serialized.
int card_ = -1;
int device_ = -1;
int subdevice_ = -1;
bool isVirtual_ = false;
int subDevices_ = -1;
Direction direction_ = Direction::None;
DECLARE_JSON_MAP(AlsaMidiDeviceInfo);
};
@@ -60,6 +75,9 @@ namespace pipedal {
std::vector<AlsaDeviceInfo> GetAlsaDevices();
};
std::vector<AlsaMidiDeviceInfo> GetAlsaMidiInputDevices();
std::vector<AlsaMidiDeviceInfo> GetAlsaMidiOutputDevices();
// we use ALSA sequencers now instead of ALSA rawmidi devices.
// 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();
std::cout << result.size() << " ALSA devices found." << std::endl;
auto midiInputDevices = GetAlsaMidiInputDevices();
std::cout << midiInputDevices.size() << " ALSA MIDI input devices found." << std::endl;
auto midiOutputDevices = GetAlsaMidiOutputDevices();
std::cout << midiOutputDevices.size() << " ALSA MIDI output devices found." << std::endl;
auto midiInputDevices = AlsaSequencer::EnumeratePorts();
std::cout << midiInputDevices.size() << " ALSA MIDI input sequencers found." << std::endl;
}
void EnumerateSequencers()
@@ -86,27 +84,58 @@ void EnumerateSequencers()
void ReadFromsequencerTest()
{
cout << "--- Reading from ALSA Sequencer" << endl;
AlsaSequencer sequencer;
sequencer.ConnectPort("V25 V25 In");
AlsaSequencer::ptr sequencer = AlsaSequencer::Create();
sequencer->ConnectPort("V25 V25 In");
AlsaMidiMessage message;
while (true)
{
if (sequencer.ReadMessage(message))
if (sequencer->ReadMessage(message,true))
{
cout << "Received MIDI message: "
<< " " << hex << setfill('0') << setw(2) << static_cast<int>(message.cc0)
<< " " << 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.cc0())
<< " " << hex << setfill('0') << setw(2) << static_cast<int>(message.cc1())
<< " " << hex << setfill('0') << setw(2) << static_cast<int>(message.cc2())
<< " Timestamp: " << setw(8) << dec << message.timestamp
<< std::endl;
}
}
}
void TestConfigMigration()
{
cout << "--- Testing ALSA Config Migration" << endl;
// older versions of PiPedal uses ALSA rawmidi.
// this code test coversion of rawmidi device IDs to ALSA Sequencer IDs.
auto rawDevices = LegacyGetAlsaMidiInputDevices();
auto seqDevices = AlsaSequencer::EnumeratePorts();
for (const auto&seqDevice: seqDevices) {
cout << seqDevice.id << " -> " << seqDevice.rawMidiDevice << endl;
}
for (const auto&rawDevice: rawDevices) {
std::string sequencerId = RawMidiIdToSequencerId(seqDevices,rawDevice.name_);
REQUIRE(!sequencerId.empty());
cout << rawDevice.name_ << "->" << sequencerId << std::endl;
}
cout << endl;
}
TEST_CASE("ALSA Seq Test", "[pipedal_alsa_seq_test][Build][Dev]")
{
// hw:CARD=VirMIDI,DEV=0 VirMIDI
// hw:CARD=VirMIDI,DEV=1 VirMIDI
// hw:CARD=VirMIDI,DEV=2 VirMIDI
// hw:CARD=VirMIDI,DEV=3 VirMIDI
// hw:CARD=M2,DEV=0 M2
// hw:CARD=V25,DEV=0 V25
EnumerateSequencers();
TestConfigMigration();
ReadFromsequencerTest();
}
+161 -70
View File
@@ -296,6 +296,8 @@ void PiPedalModel::Load()
this->audioHost->SetSystemMidiBindings(this->systemMidiBindings);
audioHost->SetAlsaSequencerConfiguration(storage.GetAlsaSequencerConfiguration());
if (configuration.GetMLock())
{
#ifndef NO_MLOCK
@@ -509,7 +511,6 @@ void PiPedalModel::FireBanksChanged(int64_t clientId)
}
}
void PiPedalModel::FirePedalboardChanged(int64_t clientId, bool loadAudioThread)
{
if (loadAudioThread)
@@ -595,14 +596,11 @@ void PiPedalModel::UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalbo
UpdateVst3Settings(pedalboard);
Lv2PedalboardErrorList errorMessages;
std::shared_ptr<Lv2Pedalboard> lv2Pedalboard{
this->pluginHost.UpdateLv2PedalboardStructure(pedalboard, this->lv2Pedalboard.get(), errorMessages)
};
this->pluginHost.UpdateLv2PedalboardStructure(pedalboard, this->lv2Pedalboard.get(), errorMessages)};
this->lv2Pedalboard = lv2Pedalboard;
// apply the error messages to the lv2Pedalboard.
// return true if the error messages have changed
audioHost->SetPedalboard(lv2Pedalboard);
@@ -614,9 +612,7 @@ void PiPedalModel::UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalbo
UpdateRealtimeVuSubscriptions();
UpdateRealtimeMonitorPortSubscriptions();
this->FirePedalboardChanged(clientId,false);
this->FirePedalboardChanged(clientId, false);
this->SetPresetChanged(clientId, true);
}
}
@@ -1400,6 +1396,77 @@ void PiPedalModel::RestartAudio(bool useDummyAudioDriver)
}
}
void PiPedalModel::OnAlsaSequencerDeviceAdded(int client, const std::string &clientName)
{
std::lock_guard<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)
{
{
@@ -1618,7 +1685,7 @@ void PiPedalModel::SendSetPatchProperty(
std::lock_guard<std::recursive_mutex> lock(mutex);
if (!audioHost)
{
onError("Audio not running.");
onError("Audio not running.");
return;
}
@@ -1629,8 +1696,9 @@ void PiPedalModel::SendSetPatchProperty(
std::shared_ptr<Lv2PluginInfo> pluginInfo = GetPluginInfo(pedalboardItem->uri_);
auto pipedalUi = pluginInfo->piPedalUI();
auto fileProperty = pipedalUi->GetFileProperty(propertyUri);
if (fileProperty && value.is_string()) {
if (fileProperty && value.is_string())
{
json_variant abstractPath = pluginHost.AbstractPath(value);
std::ostringstream ss;
json_writer writer(ss);
@@ -1680,12 +1748,11 @@ void PiPedalModel::SendSetPatchProperty(
}};
LV2_URID urid = this->pluginHost.GetLv2Urid(propertyUri.c_str());
size_t sampleTimeout = 0.5*audioHost->GetSampleRate();
size_t sampleTimeout = 0.5 * audioHost->GetSampleRate();
RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest(
onRequestComplete,
clientId, instanceId, urid, atomValue, nullptr, onError,
sampleTimeout
);
sampleTimeout);
outstandingParameterRequests.push_back(request);
if (this->audioHost)
@@ -1753,10 +1820,10 @@ void PiPedalModel::SendGetPatchProperty(
{
onError("Audio stopped.");
}
size_t sampleTimeout = 0.3*audioHost->GetSampleRate();
size_t sampleTimeout = 0.3 * audioHost->GetSampleRate();
RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest(
onRequestComplete,
clientId, instanceId, urid, onSuccess, onError,sampleTimeout);
clientId, instanceId, urid, onSuccess, onError, sampleTimeout);
outstandingParameterRequests.push_back(request);
this->audioHost->sendRealtimeParameterRequest(request);
@@ -1920,7 +1987,9 @@ void PiPedalModel::UpdateDefaults(PedalboardItem *pedalboardItem, std::unordered
{
pedalboardItem->midiChannelBinding(MidiChannelBinding::DefaultForMissingValue());
}
} else {
}
else
{
if (pedalboardItem->midiChannelBinding())
{
pedalboardItem->midiChannelBinding(std::optional<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);
bool isNote = (cc0 & 0xF0) == 0x90; // Note On
if (isNote && cc2 == 0) {
if (isNote && cc2 == 0)
{
return; // Note off. Oopsie.
}
bool isControl = (cc0 & 0xF0) == 0xB0; // Control Change
if (!isNote && !isControl) {
bool isControl = (cc0 & 0xF0) == 0xB0; // Control Change
if (!isNote && !isControl)
{
return; // Not a note on or control change.
}
for (int i = 0; i < midiEventListeners.size(); ++i)
for (int i = 0; i < midiEventListeners.size(); ++i)
{
auto &listener = midiEventListeners[i];
auto subscriber = this->GetNotificationSubscriber(listener.clientId);
if (subscriber)
{
subscriber->OnNotifyMidiListener(listener.clientHandle, cc0,cc1,cc2);
subscriber->OnNotifyMidiListener(listener.clientHandle, cc0, cc1, cc2);
}
else
{
@@ -2296,6 +2366,7 @@ void PiPedalModel::CancelMonitorPatchProperty(int64_t clientId, int64_t clientHa
audioHost->SetListenForMidiEvent(false);
}
}
std::vector<AlsaDeviceInfo> PiPedalModel::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()))
{
@@ -2369,25 +2440,24 @@ FileRequestResult PiPedalModel::GetFileList2(const std::string &relativePath_, c
std::string relativePath = relativePath_;
try
{
if (!storage.IsInUploadsDirectory(relativePath))
if (!storage.IsInUploadsDirectory(relativePath))
{
// if relativePath is in a resource directory of the plugin, then we have loaded a factory preset or are using a default property.
// map the resource path to the corresponding file in the uploads directory.
// map the resource path to the corresponding file in the uploads directory.
// :-(
PedalboardItem *pedalboardItem = GetPedalboardItemForFileProperty(fileProperty);
if (pedalboardItem)
{
auto pluginInfo = GetPluginInfo(pedalboardItem->uri());
if (pluginInfo) {
std::filesystem::path resourcePath = fs::path(pluginInfo->bundle_path())
/ fileProperty.resourceDirectory();
if (IsSubdirectory(relativePath,resourcePath))
if (pluginInfo)
{
std::filesystem::path resourcePath = fs::path(pluginInfo->bundle_path()) / fileProperty.resourceDirectory();
if (IsSubdirectory(relativePath, resourcePath))
{
fs::path t = MakeRelativePath(relativePath,resourcePath);
t = fileProperty.directory() / t;
if (fs::exists(t))
fs::path t = MakeRelativePath(relativePath, resourcePath);
t = fileProperty.directory() / t;
if (fs::exists(t))
{
relativePath = t;
}
@@ -2420,10 +2490,9 @@ std::string PiPedalModel::CopyFilePropertyFile(
bool overwrite)
{
std::lock_guard<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)
{
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);
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);
return storage.GetFilePropertydirectoryTree(uiFileProperty,selectedPath);
return storage.GetFilePropertydirectoryTree(uiFileProperty, selectedPath);
}
UiFileProperty::ptr PiPedalModel::FindLoadedPatchProperty(int64_t instanceId,const std::string&patchPropertyUri)
UiFileProperty::ptr PiPedalModel::FindLoadedPatchProperty(int64_t instanceId, const std::string &patchPropertyUri)
{
auto pedalboardItems = pedalboard.GetAllPlugins();
for (const auto&pedalboardItem: pedalboardItems) {
for (const auto &pedalboardItem : pedalboardItems)
{
if (pedalboardItem->instanceId() == instanceId)
{
Lv2PluginInfo::ptr pluginInfo = GetPluginInfo(pedalboardItem->uri());
if (pluginInfo && pluginInfo->piPedalUI())
{
for (const auto&fileProperty: pluginInfo->piPedalUI()->fileProperties())
if (fileProperty->patchProperty() == patchPropertyUri)
{
return fileProperty;
}
for (const auto &fileProperty : pluginInfo->piPedalUI()->fileProperties())
if (fileProperty->patchProperty() == patchPropertyUri)
{
return fileProperty;
}
}
}
}
@@ -2464,9 +2534,9 @@ UiFileProperty::ptr PiPedalModel::FindLoadedPatchProperty(int64_t instanceId,con
throw std::runtime_error("Permission denied. Plugin not currently loaded.");
}
std::string PiPedalModel::UploadUserFile(const std::string &directory, int64_t instanceId,const std::string &patchProperty, const std::string &filename, std::istream &stream, size_t contentLength)
std::string PiPedalModel::UploadUserFile(const std::string &directory, int64_t instanceId, const std::string &patchProperty, const std::string &filename, std::istream &stream, size_t contentLength)
{
UiFileProperty::ptr fileProperty = FindLoadedPatchProperty(instanceId,patchProperty);
UiFileProperty::ptr fileProperty = FindLoadedPatchProperty(instanceId, patchProperty);
if (!fileProperty)
{
Lv2Log::error(SS("Upload fle: Permission denied. No currently-loaded plugin provides that patch property: " << patchProperty));
@@ -2856,35 +2926,40 @@ bool PiPedalModel::GetHasWifi()
return hasWifi;
}
std::map<std::string,std::string> PiPedalModel::GetWifiRegulatoryDomains()
std::map<std::string, std::string> PiPedalModel::GetWifiRegulatoryDomains()
{
std::map<std::string,std::string> result;
try {
auto& regDb = RegDb::GetInstance();
std::map<std::string, std::string> result;
try
{
auto &regDb = RegDb::GetInstance();
result = regDb.getRegulatoryDomains(storage.GetConfigRoot() / "iso_codes.json");
} catch (const std::exception&e)
}
catch (const std::exception &e)
{
Lv2Log::warning(SS("Unable to query Wifi Regulatory domains. " << e.what()));
}
return result;
}
void PiPedalModel::CancelAudioRetry() {
void PiPedalModel::CancelAudioRetry()
{
std::lock_guard<std::recursive_mutex> lock(mutex);
if (audioRetryPostHandle)
if (audioRetryPostHandle)
{
// don't think this can ever happen, but if it did, it would be bad.
this->CancelPost(audioRetryPostHandle);
audioRetryPostHandle = 0;
}
}
void PiPedalModel::OnAlsaDriverTerminatedAbnormally() {
// notification from the realtime thread, via the audiohost that the
void PiPedalModel::OnAlsaDriverTerminatedAbnormally()
{
// notification from the realtime thread, via the audiohost that the
// ALSA stream has broken. We want to restart.
// get off the service thread as promptly as possible
this->Post([&]() {
this->Post([&]()
{
std::lock_guard<std::recursive_mutex> lock(mutex);
if (closed) return;
@@ -2932,8 +3007,7 @@ void PiPedalModel::OnAlsaDriverTerminatedAbnormally() {
} else {
Lv2Log::error(SS("Unable to reastart audio."));
}
});
} });
}
bool PiPedalModel::IsInUploadsDirectory(const std::string &path)
@@ -2943,26 +3017,43 @@ bool PiPedalModel::IsInUploadsDirectory(const std::string &path)
void PiPedalModel::MoveAudioFile(
const std::string &directory,
int32_t fromPosition,
int32_t toPosition
) {
int32_t fromPosition,
int32_t toPosition)
{
if (directory.empty())
{
throw std::runtime_error("Directory is empty.");
}
AudioDirectoryInfo::Ptr dir = AudioDirectoryInfo::Create(directory);
dir->MoveAudioFile(directory, fromPosition, toPosition);
dir->MoveAudioFile(directory, fromPosition, toPosition);
}
void PiPedalModel::SetPedalboardItemTitle(int64_t instanceId, const std::string &title)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
if (!this->pedalboard.SetItemTitle(instanceId,title))
if (!this->pedalboard.SetItemTitle(instanceId, title))
{
return;
}
// no need to reload the pedalboard, but we do need to notify subscribers.
this->SetPresetChanged(-1, true);
this->FirePedalboardChanged(-1,false);
this->SetPresetChanged(-1, true);
this->FirePedalboardChanged(-1, false);
}
void PiPedalModel::SetTone3000Auth(const std::string &apiKey)
{
std::lock_guard<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 OnHasWifiChanged(bool hasWifi) = 0;
virtual void OnAlsaSequencerConfigurationChanged(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0;
virtual void Close() = 0;
virtual void OnTone3000AuthChanged(bool value) = 0;
};
@@ -168,6 +170,7 @@ namespace pipedal
std::vector<AtomOutputListener> atomOutputListeners;
JackServerSettings jackServerSettings;
PluginHost pluginHost;
AtomConverter atomConverter; // must be AFTER pluginHost!
@@ -238,6 +241,8 @@ namespace pipedal
virtual void OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) override;
virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex,int64_t snapshotRequestId) override;
virtual void OnAlsaDriverTerminatedAbnormally() override;
virtual void OnAlsaSequencerDeviceAdded(int client, const std::string &clientName) override;
virtual void OnAlsaSequencerDeviceRemoved(int client) override;
void OnNotifyPathPatchPropertyReceived(
int64_t instanceId,
@@ -262,6 +267,7 @@ namespace pipedal
void CheckForResourceInitialization(Pedalboard &pedalboard);
UiFileProperty::ptr FindLoadedPatchProperty(int64_t instanceId,const std::string&patchPropertyUri);
public:
PiPedalModel();
virtual ~PiPedalModel();
@@ -387,6 +393,11 @@ namespace pipedal
void SetJackChannelSelection(int64_t clientId, const JackChannelSelection &channelSelection);
JackChannelSelection GetJackChannelSelection();
void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration);
AlsaSequencerConfiguration GetAlsaSequencerConfiguration();
std::vector<AlsaSequencerPortSelection> GetAlsaSequencerPorts();
void SetShowStatusMonitor(bool show);
bool GetShowStatusMonitor();
@@ -470,6 +481,10 @@ namespace pipedal
int32_t to);
void SetPedalboardItemTitle(int64_t instanceId, const std::string &title);
void SetTone3000Auth(const std::string &apiKey);
bool HasTone3000Auth() const;
};
} // namespace pipedal.
+29
View File
@@ -1416,6 +1416,10 @@ public:
pReader->read(&instanceId);
uint64_t result = model.DeleteBank(this->clientId, instanceId);
this->Reply(replyTo, "deleteBankItem", result);
} else if (message == "getHasTone3000Auth")
{
bool result = model.HasTone3000Auth();
this->Reply(replyTo, "getHasTone3000Auth", result);
}
else if (message == "renameBank")
{
@@ -1730,6 +1734,22 @@ public:
{
auto regulatoryDomains = this->model.GetWifiRegulatoryDomains();
this->Reply(replyTo, "getWifiRegulatoryDomains", regulatoryDomains);
} else if (message == "setAlsaSequencerConfiguration")
{
AlsaSequencerConfiguration config;
pReader->read(&config);
this->model.SetAlsaSequencerConfiguration(config);
this->Reply(replyTo, "setAlsaSequencerConfiguration");
}
else if (message == "getAlsaSequencerConfiguration")
{
AlsaSequencerConfiguration config = this->model.GetAlsaSequencerConfiguration();
this->Reply(replyTo, "getAlsaSequencerConfiguration", config);
}
else if (message == "getAlsaSequencerPorts")
{
std::vector<AlsaSequencerPortSelection> result = model.GetAlsaSequencerPorts();
this->Reply(replyTo,"getAlsaSequencerPorts", result);
}
else
{
@@ -1831,6 +1851,11 @@ private:
Flush();
}
virtual void OnAlsaSequencerConfigurationChanged(const AlsaSequencerConfiguration &alsaSequencerConfiguration) override
{
Send("onAlsaSequencerConfigurationChanged", alsaSequencerConfiguration);
}
virtual void OnNetworkChanging(bool hotspotConnected) override
{
try
@@ -1842,6 +1867,10 @@ private:
{
}
}
virtual void OnTone3000AuthChanged(bool value)
{
Send("onTone3000AuthChanged", value);
}
virtual void OnErrorMessage(const std::string &message)
{
+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 "AudioConfig.hpp"
#include "PiPedalException.hpp"
#include "AlsaSequencer.hpp"
#include <stdexcept>
#include <sstream>
#include "json.hpp"
@@ -253,6 +254,7 @@ void Storage::Initialize()
LoadPluginPresetIndex();
LoadBankIndex();
LoadCurrentBank();
LoadTone3000Auth();
try
{
LoadChannelSelection();
@@ -260,6 +262,8 @@ void Storage::Initialize()
catch (const std::exception &)
{
}
LoadAlsaSequencerConfiguration();
LoadWifiConfigSettings();
LoadWifiDirectConfigSettings();
LoadUserSettings();
@@ -308,10 +312,19 @@ std::filesystem::path Storage::GetCurrentPresetPath() const
return this->dataRoot / "currentPreset.json";
}
std::filesystem::path Storage::GetTone3000AuthPath() const
{
return this->dataRoot / "tone3000.json";
}
std::filesystem::path Storage::GetChannelSelectionFileName()
{
return this->dataRoot / "JackChannelSelection.json";
}
std::filesystem::path Storage::GetAlsaSequencerConfigurationFileName()
{
return this->dataRoot / "MidiDevices.json";
}
std::filesystem::path Storage::GetIndexFileName() const
{
return this->GetPresetsDirectory() / BANKS_FILENAME;
@@ -734,6 +747,103 @@ JackChannelSelection Storage::GetJackChannelSelection(const JackConfiguration &j
return jackChannelSelection.RemoveInvalidChannels(jackConfiguration);
}
static AlsaSequencerConfiguration MigrateRawMidiToAlsaSequencer(std::vector<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()
{
auto fileName = this->GetChannelSelectionFileName();
@@ -749,7 +859,6 @@ void Storage::LoadChannelSelection()
catch (const std::exception &e)
{
Lv2Log::error("I/O error reading %s: %s", fileName.c_str(), e.what());
throw PiPedalStateException("Unexpected error reading Jack settings file.");
}
}
}
@@ -758,6 +867,8 @@ void Storage::SaveChannelSelection()
auto fileName = this->GetChannelSelectionFileName();
try
{
// replaced with AlsaSequencerConfiguration. Delete legacy data.
this->jackChannelSelection.LegacyGetInputMidiDevices().clear();
pipedal::ofstream_synced s(fileName);
json_writer writer(s, false);
writer.write(this->jackChannelSelection);
@@ -1745,12 +1856,14 @@ static void AddTracksToResult(
}
const auto &path = dir_entry.path();
auto name = path.filename().string();
try {
try
{
if (dir_entry.is_directory())
{
resultFiles.push_back(FileEntry{path, name, true, dir_entry.is_symlink()});
}
} catch (const std::exception &e)
}
catch (const std::exception &e)
{
Lv2Log::warning(SS("Failed to add directory entry: " << path.string() << " - " << e.what()));
}
@@ -1799,7 +1912,6 @@ static void AddTracksToResult(
throw std::logic_error(
SS("AddTracksToResult failed to enumerate audio files. " << rootPath.string() << " - " << error.what()));
}
}
catch (const std::exception &error)
{
@@ -2498,6 +2610,47 @@ const PluginPresetIndex &Storage::GetPluginPresetIndex()
return pluginPresetIndex;
}
void Storage::LoadTone3000Auth()
{
fs::path path = GetTone3000AuthPath();
try
{
if (!fs::exists(path))
{
this->tone3000Auth = "";
return;
}
std::ifstream s(path);
json_reader reader(s);
reader.read(&(this->tone3000Auth));
}
catch (const std::exception &e)
{
Lv2Log::error("Failed to load tone3000Auth: %s", e.what());
}
}
void Storage::SetTone3000Auth(const std::string &apiKey)
{
if (tone3000Auth != apiKey)
{
tone3000Auth = apiKey;
pipedal::ofstream_synced os(this->GetTone3000AuthPath());
if (!os.is_open())
{
Lv2Log::error("Failed to open Tone3000 auth file for writing.");
return;
}
json_writer writer(os);
writer.write(apiKey);
}
}
std::string Storage::GetTone3000Auth() const
{
return tone3000Auth;
}
JSON_MAP_BEGIN(UserSettings)
JSON_MAP_REFERENCE(UserSettings, governor)
JSON_MAP_REFERENCE(UserSettings, showStatusMonitor)
+21
View File
@@ -31,6 +31,7 @@
#include "FileEntry.hpp"
#include <map>
#include "FilePropertyDirectoryTree.hpp"
#include "AlsaSequencer.hpp"
namespace pipedal {
@@ -72,6 +73,7 @@ private:
BankIndex bankIndex;
BankFile currentBank;
PluginPresetIndex pluginPresetIndex;
std::string tone3000Auth;
private:
void FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std::filesystem::path&directory) const;
@@ -84,7 +86,9 @@ private:
std::filesystem::path GetIndexFileName() const;
std::filesystem::path GetBankFileName(const std::string & name) const;
std::filesystem::path GetChannelSelectionFileName();
std::filesystem::path GetAlsaSequencerConfigurationFileName();
std::filesystem::path GetCurrentPresetPath() const;
std::filesystem::path GetTone3000AuthPath() const;
void LoadBankIndex();
void SaveBankIndex();
@@ -94,11 +98,19 @@ private:
void LoadChannelSelection();
void SaveChannelSelection();
void LoadAlsaSequencerConfiguration();
void SaveAlsaSequencerConfiguration();
void SaveBankFile(const std::string& name,const BankFile&bankFile);
void LoadBankFile(const std::string &name,BankFile *pBank);
std::string GetPresetCopyName(const std::string &name);
bool isJackChannelSelectionValid = false;
JackChannelSelection jackChannelSelection;
AlsaSequencerConfiguration alsaSequencerConfiguration;;
WifiConfigSettings wifiConfigSettings;
WifiDirectConfigSettings wifiDirectConfigSettings;
@@ -126,6 +138,7 @@ public:
void LoadWifiDirectConfigSettings();
void LoadUserSettings();
void SaveUserSettings();
void LoadBank(int64_t instanceId);
int64_t GetBankByMidiBankNumber(uint8_t bankNumber);
const Pedalboard& GetCurrentPreset();
@@ -181,6 +194,10 @@ public:
void SaveCurrentPreset(const CurrentPreset &currentPreset);
bool RestoreCurrentPreset(CurrentPreset*pResult);
void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration);
AlsaSequencerConfiguration GetAlsaSequencerConfiguration() const;
//std::string MapPropertyFileName(Lv2PluginInfo*pluginInfo, const std::string&path);
private:
@@ -239,6 +256,10 @@ public:
const UiFileProperty&uiFileProperty,
bool overwrite = false);
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty,const std::filesystem::path&selectedPath);
void LoadTone3000Auth();
void SetTone3000Auth(const std::string&apiKey);
std::string GetTone3000Auth() const;
};
+15 -1
View File
@@ -188,6 +188,10 @@ public:
{
return true;
}
else if (segment == "Tone3000Auth")
{
return true;
}
else if (segment == "PluginPresets")
{
return true;
@@ -734,7 +738,17 @@ public:
{
std::string segment = request_uri.segment(1);
if (segment == "uploadPluginPresets")
if (segment == "Tone3000Auth") {
// https://www.tone3000.com/api/v1/auth?redirect_url=http://10.0.0.151:8080/var/Tone3000Auth&otp_only=true
std::string apiKey = request_uri.query("api_key");
model->SetTone3000Auth(apiKey);
res.set(HttpField::content_type, "application/json");
res.set(HttpField::cache_control, "no-cache");
res.setBody("\"OK\"");
} else if (segment == "uploadPluginPresets")
{
PluginPresets presets;
fs::path filePath = req.get_body_temporary_file();
+37 -19
View File
@@ -48,8 +48,9 @@
#include "AudioFiles.hpp"
#include <systemd/sd-daemon.h>
#include "AlsaSequencer.hpp"
using namespace pipedal;
using namespace pipedal;
#ifdef __ARM_ARCH_ISA_A64
#define AARCH64
@@ -81,9 +82,9 @@ static bool isJackServiceRunning()
return std::filesystem::exists(path);
}
#if ENABLE_BACKTRACE
void segvHandler(int sig) {
void segvHandler(int sig)
{
void *array[10];
// Get void*'s for all entries on the stack
@@ -92,26 +93,40 @@ void segvHandler(int sig) {
// Print out all the frames to stderr
const char *message = "Error: SEGV signal received.\n";
auto _ = write(STDERR_FILENO,message,strlen(message));
auto _ = write(STDERR_FILENO, message, strlen(message));
backtrace_symbols_fd(array+2, size-2, STDERR_FILENO);
backtrace_symbols_fd(array + 2, size - 2, STDERR_FILENO);
_exit(EXIT_FAILURE);
}
static void EnableBacktrace()
{
signal(SIGSEGV, segvHandler);
signal(SIGSEGV, segvHandler);
}
#endif
static bool TryGetLogLevel(const std::string&strLogLevel,LogLevel*result)
static bool TryGetLogLevel(const std::string &strLogLevel, LogLevel *result)
{
if (strLogLevel == "debug") {*result= LogLevel::Debug; return true;}
if (strLogLevel == "info") {*result= LogLevel::Info; return true;}
if (strLogLevel == "warning") {*result= LogLevel::Warning; return true;}
if (strLogLevel == "error") { *result= LogLevel::Error; return true;}
if (strLogLevel == "debug")
{
*result = LogLevel::Debug;
return true;
}
if (strLogLevel == "info")
{
*result = LogLevel::Info;
return true;
}
if (strLogLevel == "warning")
{
*result = LogLevel::Warning;
return true;
}
if (strLogLevel == "error")
{
*result = LogLevel::Error;
return true;
}
*result = LogLevel::Info;
return false;
}
@@ -140,7 +155,7 @@ int main(int argc, char *argv[])
parser.AddOption("-h", &help);
parser.AddOption("--help", &help);
parser.AddOption("-systemd", &systemd);
parser.AddOption("-log-level",&logLevel);
parser.AddOption("-log-level", &logLevel);
parser.AddOption("-port", &portOption);
parser.AddOption("-test-extra-device", &testExtraDevice); // advertise two different devices (for testing multi-device connect)
@@ -220,11 +235,12 @@ int main(int argc, char *argv[])
if (logLevel.length() != 0)
{
LogLevel lv2LogLevel;
if (TryGetLogLevel(logLevel,&lv2LogLevel))
if (TryGetLogLevel(logLevel, &lv2LogLevel))
{
Lv2Log::log_level(lv2LogLevel);
} else {
}
else
{
Lv2Log::error(SS("Invalid log level: " << logLevel));
return EXIT_SUCCESS; // indicate to systemd that we don't want a restart.
}
@@ -237,9 +253,10 @@ int main(int argc, char *argv[])
// clean up orphaned temporary files.
const std::filesystem::path webTempDirectory = "/var/pipedal/web_temp";
if (!webTempDirectory.empty()) {
if (!webTempDirectory.empty())
{
std::filesystem::remove_all(webTempDirectory); //// user must belong to the pipedald grop when debugging.
std::filesystem::create_directories(webTempDirectory);
std::filesystem::create_directories(webTempDirectory);
}
// configure AudiDirectoryInfo to use the correct directories.
@@ -273,6 +290,7 @@ int main(int argc, char *argv[])
return EXIT_SUCCESS; // indiate to systemd that we don't want a restart.
}
try
{
{
+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 { ThemeProvider, createTheme, StyledEngineProvider } from '@mui/material/styles';
import { CssBaseline } from '@mui/material';
import CssBaseline from '@mui/material/CssBaseline';
import VirtualKeyboardHandler from './VirtualKeyboardHandler';
import AppThemed from "./AppThemed";
import { isDarkMode } from './DarkMode';
import Tone3000AuthComplete from './Tone3000AuthComplete';
declare module '@mui/material/styles' {
interface Theme {
@@ -221,6 +223,12 @@ type AppThemeProps = {
};
function isTone3000Auth() {
let url = new URL(window.location.href);
let param = url.searchParams.get("api_key");
return (param !== null && param !== "")
}
const App = (class extends React.Component {
// Before the component mounts, we initialise our state
@@ -240,8 +248,11 @@ const App = (class extends React.Component {
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>
<CssBaseline />
<AppThemed />
{isTone3000Auth() ? (
<Tone3000AuthComplete />
) : (
<AppThemed />
)}
</ThemeProvider>
</StyledEngineProvider>
);
+98 -41
View File
@@ -21,6 +21,10 @@
import React from 'react';
import { createStyles } from './WithStyles';
// import Tone3000Dialog from './Tone3000Dialog';
import Tone3000HelpDialog from './Tone3000HelpDialog';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
import Link from '@mui/material/Link';
import DraggableButtonBase from './DraggableButtonBase';
import CloseIcon from '@mui/icons-material/Close';
import { Theme } from '@mui/material/styles';
@@ -64,6 +68,8 @@ import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDial
import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata';
const ToobNamModelFileUrl = "http://two-play.com/plugins/toob-nam#modelFile";
const AUTOSCROLL_TICK_DELAY = 30;
const AUTOSCROLL_THRESHOLD = 48;
const AUTOSCROLL_SCROLL_PER_SECOND = 500;
@@ -161,6 +167,8 @@ export interface FilePropertyDialogState {
initialSelection: string;
multiSelect: boolean,
selectedFiles: string[],
//openTone3000Dialog: boolean,
openTone3000Help: boolean
};
@@ -231,7 +239,9 @@ export default withStyles(
copyDialogOpen: false,
initialSelection: this.props.selectedFile,
multiSelect: false,
selectedFiles: []
selectedFiles: [],
//openTone3000Dialog: false,
openTone3000Help: false
};
this.requestScroll = true;
}
@@ -362,7 +372,7 @@ export default withStyles(
dragElementDy: 0,
from: index,
to: index,
lastMousePoint: {...this.longPressStartPoint}
lastMousePoint: { ...this.longPressStartPoint }
};
this.setState({ dragState: dragState });
}
@@ -882,7 +892,6 @@ export default withStyles(
style={{ marginLeft: 16, marginBottom: 8, marginRight: 8, textOverflow: "", display: "flex", flexFlow: "row wrap", alignItems: "center", justifyContent: "start" }}>
{breadcrumbs}
</div>
<Divider />
</div>
);
}
@@ -998,8 +1007,7 @@ export default withStyles(
let dragElementDy = dragState.lastMousePoint.y - this.longPressStartPoint.y;
let originalY = dragState.from * dragState.height;
let newY = originalY + dragElementDy;
if (newY < 0)
{
if (newY < 0) {
newY = 0;
dragElementDy = -originalY;
}
@@ -1040,9 +1048,26 @@ export default withStyles(
() => { this.handleAutoScrollTick() }, AUTOSCROLL_TICK_DELAY);
}
// handleTone3000Dialog(e: React.MouseEvent<HTMLButtonElement>) {
// e.stopPropagation();
// e.preventDefault();
// this.setState({ openTone3000Dialog: true });
// }
handleTone3000Help(e: React.MouseEvent<HTMLButtonElement>) {
e.stopPropagation();
e.preventDefault();
this.setState({ openTone3000Help: true });
}
render() {
const isTracksDirectory = this.isTracksDirectory();
const isToobNamModelFile = this.props.fileProperty.patchProperty === ToobNamModelFileUrl;
const classes = withStyles.getClasses(this.props);
let columnWidth = this.state.columnWidth;
@@ -1102,7 +1127,7 @@ export default withStyles(
background: this.state.reordering || this.state.multiSelect ?
(isDarkMode() ? "#523" : "#EDF")
: undefined,
marginBottom: 16, paddingTop: 0
marginBottom: 8, paddingTop: 0
}} >
{this.state.reordering && (
<Toolbar style={{ padding: 0 }}>
@@ -1257,7 +1282,7 @@ export default withStyles(
</div>
</DialogTitle>
<DialogContent style={{
<DialogContent dividers style={{
paddingLeft: 0, paddingRight: 0, paddingTop: 0, colorScheme: (isDarkMode() ? "dark" : "light"),
position: "relative"
}}
@@ -1284,7 +1309,8 @@ export default withStyles(
ref={(element) => { this.listContainerElementRef = element; this.onMeasureRef(element); }}
style={{
flex: "1 1 100%", display: "flex", flexFlow: "row wrap",
position: "relative", justifyContent: "flex-start", alignContent: "flex-start", paddingLeft: 16, paddingBottom: 16
position: "relative", justifyContent: "flex-start", alignContent: "flex-start",
paddingLeft: 16, paddingBottom: 16, paddingTop: 16,
}}>
{
(this.state.columns !== 0) && // don't render until we have number of columns derived from layout.
@@ -1461,45 +1487,64 @@ export default withStyles(
</DialogContent>
{(!this.state.reordering && !this.state.multiSelect) && (
<>
<Divider />
<DialogActions style={{ justifyContent: "stretch" }}>
{this.state.windowWidth > 500 ? (
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
<IconButtonEx style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary"
tooltip="Delete selected file or folder"
disabled={!this.state.hasSelection || this.state.selectedFile === "" || protectedItem}
onClick={() => this.handleDelete()} >
<OldDeleteIcon fontSize='small' />
</IconButtonEx>
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "column nowrap" }}>
{isToobNamModelFile && (
<div style={{
display: "flex", flexFlow: "row nowrap", justifyContent: "center",
alignItems: "center", width: "100%"
}}>
<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 />}
onClick={() => { this.handleDownloadFile(); }} disabled={protectedDirectory || !this.state.hasFileSelection}
>
<div>Download</div>
</ButtonEx>
<div style={{ flex: "1 1 100%", display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
<IconButtonEx style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary"
tooltip="Delete selected file or folder"
disabled={!this.state.hasSelection || this.state.selectedFile === "" || protectedItem}
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={() => {
this.props.onApply(this.props.fileProperty, this.state.initialSelection);
this.props.onCancel();
}} aria-label="cancel">
Cancel
</Button>
<Button variant="dialogPrimary" style={{ flex: "0 0 auto" }}
onClick={() => { this.openSelectedFile(); }}
<ButtonEx tooltip="Download file" style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileDownloadIcon />}
onClick={() => { this.handleDownloadFile(); }} disabled={protectedDirectory || !this.state.hasFileSelection}
>
<div>Download</div>
</ButtonEx>
disabled={(!canSelectFile)
} aria-label="select"
>
{okButtonText}
</Button>
<div style={{ flex: "1 1 auto" }}>&nbsp;</div>
<Button variant="dialogSecondary" onClick={() => {
this.props.onApply(this.props.fileProperty, this.state.initialSelection);
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 style={{ width: "100%" }}>
@@ -1607,7 +1652,7 @@ export default withStyles(
{
this.state.renameDialogOpen && (
<RenameDialog open={this.state.renameDialogOpen} defaultName={this.renameDefaultName()}
title="Rename"
title="Rename"
onOk={(newName) => { this.setState({ renameDialogOpen: false }); this.onExecuteRename(newName) }}
onClose={() => { this.setState({ renameDialogOpen: false }); }}
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>
);
}
+4 -4
View File
@@ -36,7 +36,7 @@ import Slider, { SliderProps } from "@mui/material/Slider";
import Checkbox from "@mui/material/Checkbox";
import TimebaseSelectorDialog from "./TimebaseselectorDialog";
import Timebase, { TimebaseUnits } from "./Timebase";
import { useWindowHeight } from "@react-hook/window-size";
import useWindowSize from "./UseWindowSize";
import Button from "@mui/material/Button";
import PlayArrow from "@mui/icons-material/PlayArrow";
import StopIcon from '@mui/icons-material/Stop';
@@ -405,8 +405,8 @@ export default function LoopDialog(props: LoopDialogProps) {
const [loopEnd, setLoopEnd] = React.useState(props.value.loopEnd);
const height = useWindowHeight();
const fullScreen = height < 500;
const [windowSize] = useWindowSize();
const fullScreen = windowSize.height < 500;
function cancelPlaying() {
props.onCancelPlaying();
@@ -444,7 +444,7 @@ export default function LoopDialog(props: LoopDialogProps) {
open={props.isOpen}
onEnterKey={() => {
}}
fullScreen={height < 500}
fullScreen={fullScreen}
sx={{
"& .MuiDialog-container": {
"& .MuiPaper-root": {
+34
View File
@@ -43,6 +43,7 @@ import { ColorTheme, getColorScheme, setColorScheme } from './DarkMode';
import FilePropertyDirectoryTree from './FilePropertyDirectoryTree';
import AudioFileMetadata from './AudioFileMetadata';
import { pathFileName } from './FileUtils';
import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSequencer';
export enum State {
@@ -419,6 +420,8 @@ export class PiPedalModel //implements PiPedalModel
webSocket?: PiPedalSocket;
hasTone3000Auth: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
hasWifiDevice: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
onSnapshotModified: ObservableEvent<SnapshotModifiedEvent> = new ObservableEvent<SnapshotModifiedEvent>();
@@ -452,6 +455,7 @@ export class PiPedalModel //implements PiPedalModel
favorites: ObservableProperty<FavoritesList> = new ObservableProperty<FavoritesList>({});
alsaSequencerConfiguration : ObservableProperty<AlsaSequencerConfiguration> = new ObservableProperty<AlsaSequencerConfiguration>(new AlsaSequencerConfiguration());
presets: ObservableProperty<PresetIndex> = new ObservableProperty<PresetIndex>
(
@@ -668,6 +672,9 @@ export class PiPedalModel //implements PiPedalModel
this.handlePluginPresetsChanged(pluginUri);
} else if (message === "onJackConfigurationChanged") {
this.jackConfiguration.set(new JackConfiguration().deserialize(body));
} else if (message === "onAlsaSequencerConfigurationChanged") {
this.alsaSequencerConfiguration.set(new AlsaSequencerConfiguration().deserialize(body));
} else if (message === "onLoadPluginPreset") {
let instanceId = body.instanceId as number;
let controlValues = ControlValue.deserializeArray(body.controlValues);
@@ -755,6 +762,8 @@ export class PiPedalModel //implements PiPedalModel
} else if (message === "onErrorMessage") {
this.showAlert(body as string);
} else if (message == "onTone3000AuthChanged") {
this.hasTone3000Auth.set(body as boolean);
}
else if (message === "onLv2PluginsChanging") {
this.onLv2PluginsChanging();
@@ -1134,6 +1143,12 @@ export class PiPedalModel //implements PiPedalModel
this.jackSettings.set(new JackChannelSelection().deserialize(
await this.getWebSocket().request<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.favorites.set(await this.getWebSocket().request<FavoritesList>("getFavorites"));
@@ -3193,6 +3208,25 @@ export class PiPedalModel //implements PiPedalModel
// notify the server.
this.webSocket?.send("setPedalboardItemTitle", { instanceId: instanceId, title: title });
}
setAlsaSequencerConfiguration(alsaSequencerConfiguration: AlsaSequencerConfiguration): void {
this.webSocket?.send("setAlsaSequencerConfiguration", alsaSequencerConfiguration);
}
async getAlsaSequencerConfiguration(): Promise<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;
+156 -65
View File
@@ -17,105 +17,196 @@
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import React from 'react';
import { useState } from 'react';
import Button from '@mui/material/Button';
import List from '@mui/material/List';
import ListItemButton from '@mui/material/ListItemButton';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import FormControlLabel from '@mui/material/FormControlLabel';
import Typography from '@mui/material/Typography';
import Checkbox from '@mui/material/Checkbox';
import {AlsaMidiDeviceInfo} from './AlsaMidiDeviceInfo';
import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSequencer';
import DialogEx from './DialogEx';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
export interface SelectMidiChannelsDialogProps {
open: boolean;
selectedChannels: AlsaMidiDeviceInfo[];
availableChannels: AlsaMidiDeviceInfo[];
onClose: (selectedChannels: AlsaMidiDeviceInfo[] | null) => void;
onClose: () => void;
}
function isChecked(selectedChannels: AlsaMidiDeviceInfo[], channel: AlsaMidiDeviceInfo): boolean {
for (let i = 0; i < selectedChannels.length; ++i) {
if (selectedChannels[i].equals(channel)) return true;
}
return false;
}
function addPort(availableChannels: AlsaMidiDeviceInfo[], selectedChannels: AlsaMidiDeviceInfo[], newChannel: AlsaMidiDeviceInfo)
:AlsaMidiDeviceInfo[]
{
let result: AlsaMidiDeviceInfo[] = [];
for (let i = 0; i < availableChannels.length; ++i) {
let channel = availableChannels[i];
if (isChecked(selectedChannels, channel) || channel.equals(newChannel)) {
result.push(channel);
}
}
return result;
}
function removePort(selectedChannels: AlsaMidiDeviceInfo[], channel: AlsaMidiDeviceInfo)
:AlsaMidiDeviceInfo[]
{
let result: AlsaMidiDeviceInfo[] = [];
for (let i = 0; i < selectedChannels.length; ++i) {
if (!selectedChannels[i].equals(channel)) {
result.push(selectedChannels[i]);
}
}
return result;
}
interface DialogItem {
id: string;
name: string;
sortOrder: number
offline: boolean;
};
function SelectMidiChannelsDialog(props: SelectMidiChannelsDialogProps) {
//const classes = useStyles();
const { onClose, selectedChannels, availableChannels, open } = props;
const [currentSelection, setCurrentSelection] = useState(selectedChannels);
const { open, onClose } = props;
const [availablePorts, setAvailablePorts] = useState<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);
let toggleSelect = (value: AlsaMidiDeviceInfo) => {
if (!isChecked(currentSelection, value)) {
setCurrentSelection(addPort(availableChannels, currentSelection, value));
React.useEffect(() => {
if (open) {
setReadyToDisplay(false);
model.getAlsaSequencerPorts().then((ports) => {
setAvailablePorts(ports);
}).catch((error) => {
model.showAlert(error);
setReadyToDisplay(true);
setAvailablePorts(null);
});
model.getAlsaSequencerConfiguration().then((config) => {
setConfiguration(config);
}).catch((error) => {
model.showAlert(error);
setReadyToDisplay(true);
setConfiguration(null);
});
return () => {
}
} else {
setCurrentSelection(removePort(currentSelection, value));
return () => { };
}
}, [open]);
React.useEffect(() => {
if (availablePorts !== null && configuration !== null) {
let result: DialogItem[] = [];
setReadyToDisplay(true);
for (let port of availablePorts) {
result.push({
id: port.id,
name: port.name,
sortOrder: port.sortOrder,
offline: false
});
}
// include ports that have been previously selected but are not in the current list of available ports
for (let port of configuration.connections) {
if (!availablePorts.some((p) => p.id === port.id)) {
result.push(
{
id: port.id,
name: port.name,
sortOrder: port.sortOrder,
offline: true
}
);
}
}
result.sort((a, b) => {
return a.sortOrder - b.sortOrder;
});
setAllPorts(result);
} else {
setAllPorts(null);
}
}, [availablePorts, configuration]);
const isChecked = (value: DialogItem) => {
if (availablePorts === null || configuration === null) {
return false;
}
return configuration.connections.some((port) => port.id === value.id);
};
const setChecked = (value_: DialogItem, checked: boolean) => {
if (availablePorts === null || configuration === null) {
return;
}
let value = new AlsaSequencerPortSelection();
value.id = value_.id;
value.name = value_.name;
value.sortOrder = value_.sortOrder;
let newConnections = configuration.connections.slice();
if (checked) {
newConnections.push(value);
} else {
newConnections = newConnections.filter((port) => port.id !== value.id);
}
let newConfiguration = new AlsaSequencerConfiguration();
newConfiguration.connections = newConnections;
setConfiguration(newConfiguration);
};
let toggleSelect = (value: DialogItem) => {
if (availablePorts === null || configuration === null) {
return;
}
if (!isChecked(value)) {
setChecked(value, true);
} else {
setChecked(value, false);
}
setChanged(true);
};
const handleClose = (): void => {
onClose(null);
onClose();
};
const handleOk = (): void => {
onClose(currentSelection);
if (changed && configuration !== null) {
model.setAlsaSequencerConfiguration(configuration);
}
onClose();
};
return (
<DialogEx tag="midiChannels" onClose={handleClose} aria-labelledby="select-channels-title" open={open}
onEnterKey={handleOk}
<DialogEx tag="midiChannels" onClose={handleClose} aria-labelledby="select-midi-inputs"
open={open && readyToDisplay}
fullWidth maxWidth="xs"
onEnterKey={handleClose}
>
<DialogTitle id="simple-dialog-title">Select MIDI Device</DialogTitle>
<List>
{availableChannels.map((channel) => (
<ListItemButton>
<FormControlLabel
control={
<Checkbox checked={isChecked(currentSelection, channel)} onClick={() => toggleSelect(channel)} key={channel.name} />
}
label={channel.description}
/>
</ListItemButton>
)
<DialogTitle id="select-midi-inputs">Select MIDI Inputs</DialogTitle>
<DialogContent dividers>
<List>
{allPorts !== null && allPorts.length === 0 && (
<Typography variant="body2" style={{ marginLeft: 32, marginRight: 24, marginTop: 8, marginBottom: 16 }}>
No MIDI devices found.
</Typography>)}
{allPorts != null && allPorts.map((port) => (
<ListItemButton key={port.id}>
<FormControlLabel
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 && (
<Typography variant="body2" style={{ marginLeft: 32, marginRight: 24,marginTop:8, marginBottom: 16}}>
No MIDI devices found.</Typography>
)}
</List>
)}
</List>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} variant="dialogSecondary" >
Cancel
+20 -14
View File
@@ -44,9 +44,9 @@ import WifiConfigDialog from './WifiConfigDialog';
import WifiDirectConfigDialog from './WifiDirectConfigDialog';
import DialogEx from './DialogEx'
import GovernorSettings from './GovernorSettings';
import { AlsaMidiDeviceInfo } from './AlsaMidiDeviceInfo';
import SystemMidiBindingsDialog from './SystemMidiBindingsDialog';
import SelectThemeDialog from './SelectThemeDialog';
import {AlsaSequencerConfiguration} from './AlsaSequencer';
import Slide, { SlideProps } from '@mui/material/Slide';
import {createStyles} from './WithStyles';
@@ -73,6 +73,8 @@ interface SettingsDialogState {
jackConfiguration: JackConfiguration;
jackSettings: JackChannelSelection;
jackServerSettings: JackServerSettings;
alsaSequencerConfiguration: AlsaSequencerConfiguration;
jackStatus?: JackHostStatus;
governorSettings: GovernorSettings;
continueDisabled: boolean;
@@ -184,6 +186,7 @@ const SettingsDialog = withStyles(
jackConfiguration: this.model.jackConfiguration.get(),
jackStatus: undefined,
jackSettings: this.model.jackSettings.get(),
alsaSequencerConfiguration: this.model.alsaSequencerConfiguration.get(),
wifiConfigSettings: this.model.wifiConfigSettings.get(),
wifiDirectConfigSettings: this.model.wifiDirectConfigSettings.get(),
governorSettings: this.model.governorSettings.get(),
@@ -207,6 +210,7 @@ const SettingsDialog = withStyles(
hasWifiDevice: this.model.hasWifiDevice.get()
};
this.handleJackConfigurationChanged = this.handleJackConfigurationChanged.bind(this);
this.handleAlsaSequencerConfigurationChanged = this.handleAlsaSequencerConfigurationChanged.bind(this);
this.handleJackSettingsChanged = this.handleJackSettingsChanged.bind(this);
this.handleJackServerSettingsChanged = this.handleJackServerSettingsChanged.bind(this);
this.handleWifiConfigSettingsChanged = this.handleWifiConfigSettingsChanged.bind(this);
@@ -306,6 +310,11 @@ const SettingsDialog = withStyles(
jackConfiguration: this.model.jackConfiguration.get()
});
}
handleAlsaSequencerConfigurationChanged(): void {
this.setState({
alsaSequencerConfiguration: this.model.alsaSequencerConfiguration.get()
});
}
mounted: boolean = false;
active: boolean = false;
@@ -333,6 +342,7 @@ const SettingsDialog = withStyles(
this.model.showStatusMonitor.addOnChangedHandler(this.handleShowStatusMonitorChanged);
this.model.jackSettings.addOnChangedHandler(this.handleJackSettingsChanged);
this.model.jackConfiguration.addOnChangedHandler(this.handleJackConfigurationChanged);
this.model.alsaSequencerConfiguration.addOnChangedHandler(this.handleAlsaSequencerConfigurationChanged);
this.model.jackServerSettings.addOnChangedHandler(this.handleJackServerSettingsChanged);
this.model.wifiConfigSettings.addOnChangedHandler(this.handleWifiConfigSettingsChanged);
this.model.wifiDirectConfigSettings.addOnChangedHandler(this.handleWifiDirectConfigSettingsChanged);
@@ -352,6 +362,7 @@ const SettingsDialog = withStyles(
});
this.handleConnectionStateChanged();
this.handleJackConfigurationChanged();
this.handleAlsaSequencerConfigurationChanged();
this.handleJackSettingsChanged();
this.handleShowStatusMonitorChanged();
this.handleJackServerSettingsChanged();
@@ -368,6 +379,7 @@ const SettingsDialog = withStyles(
this.model.showStatusMonitor.removeOnChangedHandler(this.handleShowStatusMonitorChanged);
this.model.jackConfiguration.removeOnChangedHandler(this.handleJackConfigurationChanged);
this.model.alsaSequencerConfiguration.removeOnChangedHandler(this.handleAlsaSequencerConfigurationChanged);
this.model.jackSettings.removeOnChangedHandler(this.handleJackSettingsChanged);
this.model.jackServerSettings.removeOnChangedHandler(this.handleJackServerSettingsChanged);
this.model.wifiConfigSettings.removeOnChangedHandler(this.handleWifiConfigSettingsChanged);
@@ -437,13 +449,7 @@ const SettingsDialog = withStyles(
});
}
handleSelectMidiDialogResult(channels: AlsaMidiDeviceInfo[] | null): void {
if (channels) {
let newSelection: JackChannelSelection = this.state.jackSettings.clone();
newSelection.inputMidiDevices = channels;
this.model.setJackSettings(newSelection);
}
handleSelectMidiDialogResult(): void {
this.setState({
showMidiSelectDialog: false,
@@ -477,7 +483,7 @@ const SettingsDialog = withStyles(
}
midiSummary(): string {
let ports = this.state.jackSettings.inputMidiDevices;
let ports = this.state.alsaSequencerConfiguration.connections;
if (ports.length === 0) return "Disabled";
let result = "";
@@ -485,7 +491,7 @@ const SettingsDialog = withStyles(
if (result.length !== 0) {
result += ", ";
}
result += port.description;
result += port.name;
}
return result;
}
@@ -689,7 +695,9 @@ const SettingsDialog = withStyles(
<div style={{ width: "100%" }}>
<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>
</ButtonBase>
<ButtonBase className={classes.setting} disabled={!isConfigValid} onClick={() => this.handleMidiMessageSettings()} >
@@ -929,9 +937,7 @@ const SettingsDialog = withStyles(
(this.state.showMidiSelectDialog) &&
(
<SelectMidiChannelsDialog open={this.state.showMidiSelectDialog}
onClose={(selectedChannels: AlsaMidiDeviceInfo[] | null) => this.handleSelectMidiDialogResult(selectedChannels)}
selectedChannels={this.state.jackSettings.inputMidiDevices}
availableChannels={this.state.jackConfiguration.inputMidiDevices}
onClose={() => this.handleSelectMidiDialogResult()}
/>
)
+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 JsonAtom from './JsonAtom';
import { UiFileProperty } from './Lv2Plugin';
import { Divider } from '@mui/material';
import Divider from '@mui/material/Divider';
import useWindowSize from './UseWindowSize';
import { getAlbumArtUri } from './AudioFileMetadata';
import RepeatIcon from '@mui/icons-material/Repeat';
+11 -14
View File
@@ -81,7 +81,7 @@ function ToolTipEx(props: ToolTipExProps) {
if (valueTooltip === undefined) // no timeout if there's a value tooltip
{
if (t > 0) {
console.log("ToolTipEx: starting timeout for ", t);
// console.log("ToolTipEx: starting timeout for ", t);
handle = window.setTimeout(() => {
setOpen(true);
},t);
@@ -89,7 +89,7 @@ function ToolTipEx(props: ToolTipExProps) {
}
return () => {
if (handle !== null) {
console.log("ToolTipEx: clearing timeout for ", t);
// console.log("ToolTipEx: clearing timeout for ", t);
window.clearTimeout(handle);
}
};
@@ -161,6 +161,7 @@ function ToolTipEx(props: ToolTipExProps) {
}
}
}
let effectiveTitle: React.ReactNode | null = null;
let placement: "top-start" | "right" = "top-start";
if (valueTooltip !== undefined && !isLongPress) {
@@ -181,44 +182,40 @@ function ToolTipEx(props: ToolTipExProps) {
return (
<div style={{ display: 'inline-block' }}
onClickCapture={(e) => {
console.log("ToolTipEx: onClickCapture");
// console.log("ToolTipEx: onClickCapture");
handleClickCapture(e);
setTimeout(0);
setOpen(false);
setIsLongPress(false);
setLongPressLeaving(true);
}}
onMouseEnter={(e)=> {
console.log("ToolTipEx: onMouseEnter");
// console.log("ToolTipEx: onMouseEnter");
if (!longPressLeaving) {// Don't handle mouse enter if we're in a long press leaving state
handleMouseEnter(e);
}
}}
onMouseLeave={(e)=> {
console.log("ToolTipEx: onMouseLeave");
// console.log("ToolTipEx: onMouseLeave");
handleMouseLeave(e);
}}
onPointerCancelCapture={(e) => {
console.log("ToolTipEx: onPointerCancelCapture");
// console.log("ToolTipEx: onPointerCancelCapture");
handlePointerCancel(e);
}}
onContextMenuCapture={(e) => {
console.log("ToolTipEx: onContextMenuCapture");
// console.log("ToolTipEx: onContextMenuCapture");
handleConextMenu(e);
return false;
}}
onPointerDownCapture={(e) => {
console.log("ToolTipEx: onPointerDownCapture");
// console.log("ToolTipEx: onPointerDownCapture");
handlePointerDownCapture(e);
}}
onPointerMoveCapture={(e) => {
console.log("ToolTipEx: onPointerMoveCapture");
// console.log("ToolTipEx: onPointerMoveCapture");
handlePointerMoveCapture(e);
}}
onPointerUpCapture={(e) => {
console.log("ToolTipEx: onPointerUpCapture");
// console.log("ToolTipEx: onPointerUpCapture");
handlePointerUpCapture(e);
}}
>
+30 -25
View File
@@ -21,38 +21,43 @@
* SOFTWARE.
*/
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect } from 'react';
const getSize = () => {
return {
width: window.innerWidth,
height: window.innerHeight,
};
return {
width: window.innerWidth,
height: window.innerHeight,
};
};
export interface WindowSize {
width: number;
height: number;
}
export default function useWindowSize() {
const [size, setSize] = useState<WindowSize>(getSize());
const handleResize = useCallback(() => {
let ticking = false;
if (!ticking) {
window.requestAnimationFrame(() => {
setSize(getSize());
ticking = false;
});
ticking = true;
}
}, []);
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return [size];
const [size, setSize] = useState<WindowSize>(getSize());
useEffect(() => {
let mounted = true;
let handleResizeT = () => {
let pendingCallback = false;
if (!pendingCallback) {
window.requestAnimationFrame(() => {
if (mounted) {
setSize(getSize());
pendingCallback = false;
}
});
pendingCallback = true;
}
}
window.addEventListener('resize', handleResizeT);
return () => {
mounted = false;
window.removeEventListener('resize', handleResizeT);
};
}, []);
return [size];
}