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