Compare commits

...

37 Commits

Author SHA1 Message Date
shawn 120279cbd4 Add VU meter data, gate status, and compressor GR to mixer IPC protocol
- MixerEngine.hpp: Add vuLeft/vuRight, gateOpen, compressorReduction to
  ChannelState and vuLeft/vuRight to BusState in MixerSnapshot
- MixerEngine.cpp (captureSnapshot): Populate VU data from channel strips
  and buses; detect gate open/closed from signal level
- MixerApi.cpp (getStateJson): Serialize VU, gate, and compressor fields
  in the JSON state response

This enables the frontend MetersPage to display real-time VU meter bars,
gate open/closed indicators, and compressor gain reduction meters.
2026-06-23 20:09:52 -04:00
shawn 8068f5d168 Mixer engine: output routing, full state restore, MIDI learn API, PipeWire multi-channel support 2026-06-20 16:20:30 -04:00
shawn 01584f50da Initialize MixerEngine on startup (BB-6)
Add MixerEngine creation and wiring after model.Load() in main.cpp.
The mixer engine now gets created with current audio settings (sample rate,
buffer size from JackServerSettings), activated, and attached to the model.
This enables WebSocket mixer commands (mixerGetState, mixerAddChannel,
mixerAddBus, etc.) to work through /pipedal endpoint.

Missing initialization was the root cause of 'Invalid format' errors when
sending mixer commands via WebSocket.
2026-06-20 16:15:12 -04:00
shawn 5fd5946ff6 P4: MIDI control surface mapping — faders/buttons over USB MIDI
C++ backend (fully implemented):
- MidiMapper: CC processing, mapping table, JSON persistence, learn mode
- MidiLearnMode: 3-step learn workflow state machine
- MixerEngine::processMidiEvent() wired into AudioHost MIDI pipeline
- MixerApi + PiPedalSocket: all WS handlers (getMidiMappings,
  setMidiLearnMode, setMidiLearnTarget, commitMidiLearn, etc.)

React frontend (new):
- MidiMappingPanel: dialog with learn mode toggle, CC capture polling,
  commit workflow, current mappings list with delete, manual add
- MixerPage: MIDI button in toolbar, learn mode state management
- ChannelStrip + MasterBus: learn mode callbacks on fader/mute/solo touch
2026-06-20 16:14:12 -04:00
shawn 3d00299051 feat: add PipeWire multi-channel audio driver (full-duplex N channel)
- PipeWireDriver.hpp/cpp: new AudioDriver implementation using pw_filter API
- Full-duplex I/O via pw_filter (capture + playback in one RT callback)
- Dynamic channel count support (tested: 1-8 channels, extensible beyond)
- Uses SPA_AUDIO_FORMAT_F32 for zero-copy-compatible float processing
- Channel position mapping: MONO, stereo, 5.1, 7.1, plus Aux for N>8
- Follows the same buffer management pattern as AlsaDriverImpl
- Channel routing: main/aux input/output mapping with mix ops
- CLI flag: --driver pipewire|alsa (default: alsa)
- AudioHost: conditional driver selection based on driverType_
- PiPedalModel: stores and passes driver type to AudioHost
- CMakeLists.txt: added PipeWireDriver source files
2026-06-20 16:03:16 -04:00
shawn 524f02ec9d fix: resolve TS6133 unused-variable errors in MixerScenePanel files
- Removed unused Theme, DeleteIcon imports; fixed unused 'scenes' destructure
- Removed unused React default import and Restore icon import in subdir version
2026-06-20 16:00:33 -04:00
shawn e4e7cd1ca2 feat: add mixer scene API methods to PiPedalModel 2026-06-20 15:57:40 -04:00
shawn 0316e4b37f feat: add React mixer UI — MixerPage, ChannelStrip, MasterBus, useMixerWS hook
- useMixerWS: WebSocket hook connecting to ws://192.168.0.245:8080/ws
  with PiPedalSocket-compatible protocol (request/reply via replyTo)
  and auto-reconnect with exponential backoff.
- ChannelStrip: per-input channel control with volume fader, pan,
  mute, solo, and channel label/type display.
- MasterBus: master/subgroup/aux bus strip with volume fader and mute.
- MixerPage: full mixer console view with horizontal strip layout,
  real-time state polling (2s), and connection status indicator.
- AppThemed: added 'Mixer Console' drawer item + conditional rendering.

Build verified: tsc clean, vite build passes (1760 modules, 2.63s).
2026-06-20 15:56:17 -04:00
shawn 0a76f5734f Add scene save/load WS messages to MixerApi
- Implement saveScene: captures full mixer state via getStateJson(),
  stores in ~/op-pedal/default_config/scenes.json with auto-incrementing ID
- Implement loadScene: manual JSON walk to restore channel/bus state
  from saved scene JSON, routes skipped for safety
- Implement listScenes: returns JSON array of {id, name}
- Implement deleteScene: remove by ID from scenes file
- Add WS handlers: mixerSaveScene, mixerLoadScene, mixerGetScenes
  registered in PiPedalSocket.cpp message dispatcher
- Uses project's JSON_MAP/raw_json_string for file persistence
- Compiles cleanly against existing build
2026-06-20 15:54:42 -04:00
shawn 959da00d7c docs: add band-in-a-box digital mixer implementation plan 2026-06-20 15:11:14 -04:00
shawn 1854d03c58 feat: add WebsSocket API for mixer engine control
New files:
- MixerApi.hpp/cpp: Model-level API bridging WebSocket messages to
  MixerEngine control (channel volume/pan/mute/solo/hpf, bus control,
  routing, state queries)
- 15 new WebSocket message handlers in PiPedalSocket.cpp for full
  mixer control surface

Integration:
- MixerEngine member added to PiPedalModel with Get/Set accessors
- SetMixerEngine propagates to AudioHost rt processing pipeline
- Socket handler auto-wires MixerEngine to MixerApi on connect

Messages implemented:
  mixerSetChannelVolume, mixerSetChannelPan, mixerSetChannelMute,
  mixerSetChannelSolo, mixerSetChannelLabel, mixerSetChannelHpf,
  mixerGetState, mixerAddChannel, mixerRemoveChannel,
  mixerSetBusVolume, mixerSetBusMute, mixerAddBus, mixerRemoveBus,
  mixerRouteChannelToBus
2026-06-20 14:15:37 -04:00
shawn 0422c91b4e feat: integrate MixerEngine into AudioHost real-time audio pipeline
When SetMixerEngine() is called with a MixerEngine instance, the
audio processing thread routes all device input/output channels
through the mixer (channel strips → buses → master) instead of
the legacy Lv2Pedalboard. Falls back when no mixer engine is set.

- Thread-safe via mutex + realtime raw pointer swap (same pattern
  as existing SetPedalboard)
- Preserves existing pedalboard processing — both modes coexist
- New ProcessLv2Pedalboard() path when realtimeActiveMixerEngine
  is non-null uses device buffers directly for multi-channel I/O
2026-06-20 14:06:46 -04:00
shawn df5a317ceb feat: add MixerEngine core — ChannelStrip, MixerBus, and MixerEngine for band-in-a-box digital mixer
MixerEngine architecture:
- MixerChannelStrip: per-input FX chain (Lv2Pedalboard reuse),
  volume, pan, mute, solo, HPF, aux sends, VU metering
- MixerBus: accumulation bus with volume, mute, VU. Supports
  master, subgroup, aux, and FX-return bus types
- MixerEngine: orchestrator managing channel→bus routing graph,
  bus→bus routing, solo override, and the full real-time audio
  processing cycle

All new code compiles cleanly with the existing C++20 build and
follows the existing PiPedal codebase conventions (namespaces,
error handling, buffer patterns). CPU-efficient real-time thread
processing with atomic control surface interaction.
2026-06-20 13:57:15 -04:00
shawn e1014462b4 fix: Restore missing P in OP LABS — path was truncated, only had O letter
CMake / build (push) Has been cancelled
CodeQL / Analyze (cpp) (push) Has been cancelled
2026-06-20 13:24:59 -04:00
shawn 4854b2fa22 fix: Working OP-Pedal logo SVGs with Arial/sans-serif text + PNG previews
CMake / build (push) Has been cancelled
CodeQL / Analyze (cpp) (push) Has been cancelled
2026-06-20 13:10:03 -04:00
shawn 4047a74dd5 fix: Revise OP-Pedal logos - OP LABS + OP PEDAL subtitle with per-letter spacing
CMake / build (push) Has been cancelled
CodeQL / Analyze (cpp) (push) Has been cancelled
2026-06-20 12:09:17 -04:00
shawn 236e1e2b3c feat: Add OP-Pedal branded logo variants (horizontal + square stack)
CMake / build (push) Has been cancelled
CodeQL / Analyze (cpp) (push) Has been cancelled
- Horizontal lockup: OP Labs icon + 'OP' (silver) + 'PEDAL' (blue)
- Square stack: icon above 'OP PEDAL' text
- Place in artifacts/ and vite/public/img/ for UI use
2026-06-20 12:05:29 -04:00
shawn ae13b3739d feat: Add OP Labs branding — favicon, PWA icons, manifest, UI logo
CMake / build (push) Has been cancelled
CodeQL / Analyze (cpp) (push) Has been cancelled
- OP Labs icon SVG as default app icon + favicon (SVG + ICO fallback)
- 192x192 and 512x512 PWA icons generated from OP Labs logo
- Updated manifest.json with SVG favicon + maskable icon support
- Updated index.html with SVG favicon link
- Replaced old ic_logo.svg with OP Labs icon mark
- Stored OP Labs logo assets (icon, horizontal, square) in artifacts/
- Removed old PiPedal20Thumb.jpg
2026-06-20 12:03:42 -04:00
shawn c028ad8cd6 fix: Rebrand systemd templates, polkit rules, and install scripts
CMake / build (push) Has been cancelled
CodeQL / Analyze (cpp) (push) Has been cancelled
- src/templates/pipedald.service.template — user/group pipedal_d → oppedal_d
- src/templates/pipedaladmind.service.template — WorkingDirectory /var/pipedal → /var/oppedal
- src/templates/pipedal_nm_p2pd.service.template — Description and WorkingDirectory
- src/templates/pipedal_p2pd.service.template — Description and WorkingDirectory
- src/templates/pipedal_p2pd.conf.template — p2p_model_name, manufacturer
- src/templates/jack.service.template — After=pipedald → After=oppedald
- src/templates/*.conf — PiPedal/pipedal-* → OP-Pedal/oppedal-* references
- src/polkit-1/rules/10-pipedal-networkmanager.rules — group pipedal_d → oppedal_d
- install.sh — pipedalconfig/oppedalconfig binary paths
- uninstall.sh — /etc/pipedal, /var/pipedal → /etc/oppedal, /var/oppedal
- run.sh — pipedald → oppedald, /etc/pipedal → /etc/oppedal
2026-06-20 12:02:39 -04:00
shawn 5e93cc1fb9 fix: Update system-level branding in PiPedalCommon (hotspot/ALSA names, service group, device name)
CMake / build (push) Has been cancelled
CodeQL / Analyze (cpp) (push) Has been cancelled
- WifiConfigSettings.hpp: default hotspot 'pipedal' -> 'op-pedal'
- WifiDirectConfigSettings.hpp: default hotspot 'pipedal' -> 'op-pedal'
- WifiDirectConfigSettings.cpp: default device name 'PiPedal' -> 'OP-Pedal'
- ServiceConfiguration.hpp: deviceName 'PiPedal' -> 'OP-Pedal'
- ServiceConfiguration.cpp: group name 'pipedal_d' -> 'oppedal_d'
- AlsaSequencer.cpp: ALSA client/port/queue names 'PiPedal' -> 'OP-Pedal' (5 occurrences)
2026-06-20 12:01:56 -04:00
shawn acfb8d15ab fix: Rebrand project from PiPedal to OP-Pedal
CMake / build (push) Has been cancelled
CodeQL / Analyze (cpp) (push) Has been cancelled
Updated branding across the project:
- CMakeLists.txt: project name, description, homepage, display version
- src/PiPedalVersion.cpp: server name string
- debian/control: package name, source, homepage, description
- vite/package.json: package name
- vite/index.html: title, meta description
- vite/public/manifest.json: name, short_name, description, URLs
- UI files: AboutDialog, AppThemed, MainPage, SettingsDialog,
  UpdateDialog, WifiConfigDialog, WifiConfigSettings,
  WifiDirectConfigSettings, Tone3000Downloader, Tone3000HelpDialog,
  WindowScale

Preserved upstream copyright/attribution and legal references.
2026-06-20 11:04:28 -04:00
shawn d01a7df03a chore: rebrand as OP-Pedal fork of PiPedal v2.0.107
CMake / build (push) Has been cancelled
CodeQL / Analyze (cpp) (push) Has been cancelled
2026-06-20 10:56:19 -04:00
Robin Davies 0db76000d4 Bad link in README.md
Signed-off-by: Robin Davies <rerdavies@gmail.com>
2026-06-18 09:28:15 -04:00
Robin Davies a552fb34f3 Merge pull request #516 from rerdavies/dev
v2.0.107 Dev Merge
2026-06-18 09:23:09 -04:00
Robin E.R. Davies d75a52bdac RE-add esbuild 2026-06-18 08:30:36 -04:00
Robin E.R. Davies b8cec3e1e8 Merge branch 'dev' of https://github.com/rerdavies/pipedal into dev 2026-06-18 08:18:51 -04:00
Robin E.R. Davies e71594000a Referesh vite dependencies in Prod build. 2026-06-18 08:18:48 -04:00
Robin E. R. Davies 5b644fe74c Merge branch 'dev' of https://github.com/rerdavies/pipedal into dev 2026-06-18 08:17:23 -04:00
Robin E. R. Davies 1cd9105f9a ToobAmp v1.3.82 aarch64 2026-06-18 08:16:40 -04:00
Robin E.R. Davies 3539ebcef3 Merge branch 'main' of https://github.com/rerdavies/pipedal into dev 2026-06-18 08:06:07 -04:00
Robin E.R. Davies 11a5c3e174 v2.0.107 2026-06-18 08:04:55 -04:00
Robin E.R. Davies a6a620ad55 Merge branch 'main' of https://github.com/rerdavies/pipedal 2026-06-18 07:58:36 -04:00
Robin E.R. Davies 52e7ea8ad0 ToobAmp v1.3.82 amd64 2026-06-18 07:58:32 -04:00
Robin Davies 7d611709b5 Update ReleaseNotes with contributor information
Added contributor names for minor features.

Signed-off-by: Robin Davies <rerdavies@gmail.com>
2026-06-17 14:18:18 -04:00
Robin E.R. Davies 4522200b3b Merge branch 'dev' of https://github.com/rerdavies/pipedal 2026-06-17 14:11:24 -04:00
Robin Davies a752eac22c Merge pull request #508 from rerdavies/dependabot/npm_and_yarn/vite/npm_and_yarn-30ae9b537b
Bump esbuild from 0.25.2 to removed in /vite in the npm_and_yarn group across 1 directory
2026-06-17 09:37:22 -04:00
dependabot[bot] dd7fb7fb11 Bump esbuild in /vite in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the /vite directory: [esbuild](https://github.com/evanw/esbuild).


Removes `esbuild`

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version:
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-14 03:01:29 +00:00
136 changed files with 9968 additions and 855 deletions
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -1,11 +1,11 @@
cmake_minimum_required(VERSION 3.16.0)
project(pipedal
VERSION 2.0.106
DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi"
HOMEPAGE_URL "https://rerdavies.github.io/pipedal"
project(op-pedal
VERSION 2.0.107
DESCRIPTION "OP-Pedal Guitar Effect Pedal For Raspberry Pi"
HOMEPAGE_URL "https://ourpad.casa/op-pedal"
)
set (DISPLAY_VERSION "PiPedal v2.0.106-Release")
set (DISPLAY_VERSION "OP-Pedal / Ourpad Pedal v2.0.107-Release")
option(PIPEDAL_DISABLE_COPYRIGHT_BUILD "Skip generation of copyright notices (use on non-Debian distros)" OFF)
option(PIPEDAL_EXCLUDE_TESTS "Exclude test targets from default build" OFF)
+5 -5
View File
@@ -275,9 +275,9 @@ namespace pipedal
{
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, "OP-Pedal");
inPort = snd_seq_create_simple_port(seqHandle, "PiPedal:in",
inPort = snd_seq_create_simple_port(seqHandle, "OP-Pedal:in",
SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE,
SND_SEQ_PORT_TYPE_MIDI_GENERIC |
SND_SEQ_PORT_TYPE_MIDI_GM | SND_SEQ_PORT_TYPE_APPLICATION);
@@ -651,7 +651,7 @@ namespace pipedal
// Create a new queue if we don't have one yet
if (queueId < 0)
{
queueId = snd_seq_alloc_named_queue(seqHandle, "PiPedal Realtime Queue");
queueId = snd_seq_alloc_named_queue(seqHandle, "OP-Pedal Realtime Queue");
if (queueId < 0)
{
throw std::runtime_error(SS("Failed to create ALSA queue: " << snd_strerror(queueId)));
@@ -862,7 +862,7 @@ namespace pipedal
snd_seq_set_client_name(seqHandle, "Device Monitor");
int inPort = snd_seq_create_simple_port(
seqHandle, "PiPedal:portMonitor",
seqHandle, "OP-Pedal:portMonitor",
SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE,
SND_SEQ_PORT_TYPE_APPLICATION);
if (inPort < 0)
@@ -973,7 +973,7 @@ namespace pipedal
// 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");
queueId = snd_seq_alloc_named_queue(seqHandle, "OP-Pedal Device Monitor Queue");
if (queueId < 0)
{
throw std::runtime_error(SS("Failed to create ALSA queue: " << snd_strerror(queueId)));
+2 -2
View File
@@ -71,10 +71,10 @@ void ServiceConfiguration::Save()
t.close();
struct group *group_;
group_ = getgrnam("pipedal_d");
group_ = getgrnam("oppedal_d");
if (group_ == nullptr)
{
throw logic_error("Group not found: pipedal_d");
throw logic_error("Group not found: oppedal_d");
}
std::ignore = chown(DEVICEID_FILE_NAME, -1, group_->gr_gid);
std::ignore = chmod(DEVICEID_FILE_NAME, S_IWUSR | S_IRUSR | S_IRGRP | S_IWGRP | S_IROTH);
@@ -174,7 +174,7 @@ void WifiDirectConfigSettings::Load()
if (!ConfigUtil::GetConfigLine("/var/pipedal/config/pipedal_p2pd.conf","p2p_device_name",&this->hotspotName_))
{
this->hotspotName_ = "PiPedal";
this->hotspotName_ = "OP-Pedal";
}
if (!ConfigUtil::GetConfigLine("/var/pipedal/config/pipedal_p2pd.conf","country_code",&this->countryCode_))
@@ -46,7 +46,7 @@ namespace pipedal {
std::string uuid;
std::string deviceName = "PiPedal";
std::string deviceName = "OP-Pedal";
uint32_t server_port = 80;
private:
std::filesystem::path filename;
@@ -71,11 +71,11 @@ namespace pipedal {
std::string homeNetwork_;
std::string countryCode_ = "US"; // iso 3661
std::string hotspotName_ = "pipedal";
std::string hotspotName_ = "op-pedal";
bool hasPassword_ = false;
std::string password_;
std::string channel_ = "";
std::string mdnsName_ = "pipedal";
std::string mdnsName_ = "op-pedal";
bool wifiWarningGiven_ = false; // Do not use. Present only for backward compatibility.
bool valid_ = false; // Do not use. Present only for backward compatibility.
@@ -34,7 +34,7 @@ namespace pipedal {
bool rebootRequired_ = false;
bool enable_ = false;
std::string countryCode_ = ""; // iso 3661
std::string hotspotName_ = "pipedal";
std::string hotspotName_ = "op-pedal";
bool pinChanged_ = false;
std::string pin_;
std::string channel_ = "1"; // "0" -> select automatically.
+16 -16
View File
@@ -1,28 +1,28 @@
# 🎸 OP-Pedal
**Fork of [PiPedal](https://github.com/rerdavies/pipedal) v2.0.107** — Ourpad's custom guitar multi-FX pedal for Raspberry Pi.
<img src='docs/GithubBanner.png' width="100%" /><br/>
<a href="https://rerdavies.github.io/pipedal/ReleaseNotes"><img src="https://img.shields.io/github/v/release/rerdavies/pipedal?color=%23808080"/></a>
<a href="https://rerdavies.github.io/pipedal/download"><img src="https://img.shields.io/badge/Download-008060" /></a>
<a href="https://rerdavies.github.io/pipedal/Documentation"><img src="https://img.shields.io/badge/Docmentation-0060d0"/></a>
<a href="https://rerdavies.github.io/pipedal/LicensePiPedal.html"><img src="https://img.shields.io/badge/MIT-MIT?label=license&color=%23808080"/></a>
<a href="https://github.com/rerdavies/pipedal/actions"><img src="https://img.shields.io/github/actions/workflow/status/rerdavies/pipedal/cmake.yml?branch=main"/></a>
<img src="https://img.shields.io/github/downloads/rerdavies/pipedal/total?color=%23808080&link=https%3A%2F%2Frerdavies.github.io%2Fpipedal%2Fdownload.html"/>
| Badge | Link |
|-------|------|
| ![GitHub release](https://img.shields.io/github/v/release/rerdavies/pipedal?color=%23808080) | [Upstream Release Notes](https://rerdavies.github.io/pipedal/ReleaseNotes) |
| ![Download](https://img.shields.io/badge/Download-008060) | [Download PiPedal](https://rerdavies.github.io/pipedal/download) |
| ![Docs](https://img.shields.io/badge/Documentation-0060d0) | [Upstream Docs](https://rerdavies.github.io/pipedal/Documentation) |
| ![License](https://img.shields.io/badge/MIT-MIT?label=license&color=%23808080) | [MIT License](LICENSE.md) |
Download:&nbsp;<a href='https://rerdavies.github.io/pipedal/download.html'>v2.0.106</a>
Website:&nbsp;[https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal).
Documentation:&nbsp;[https://rerdavies.github.io/pipedal/Documentation.html](https://rerdavies.github.io/pipedal/Documentation.html).
#### Announcing PiPedal 2.0 (2.0.106)&mdash;a major update to PiPedal, including exciting new features. See the Pipedal website [documentation](https://rerdavies.github.io/pipedal/PiPedal.html) for more information.
**Gitea:** https://gitea.ourpad.casa/shawn/op-pedal
**Upstream:** https://github.com/rerdavies/pipedal
&nbsp;
Use your Raspberry Pi, or Ubuntu amd/x86-64 computer as a guitar effects pedal. Configure and control PiPedal remotedly, with your phone or tablet, or via a web browser.
Use your Raspberry Pi, or Ubuntu amd/x86-64 computer as a guitar effects pedal. Configure and control OP-Pedal remotedly, with your phone or tablet, or via a web browser.
PiPedal running on a Raspberry Pi 4 or Pi 5 provides stable super-low-latency audio via external USB audio devices, or internal Raspberry Pi audio hats.
OP-Pedal running on a Raspberry Pi 4 or Pi 5 provides stable super-low-latency audio via external USB audio devices, or internal Raspberry Pi audio hats.
PiPedal runs on Raspbery Pi OS (Bookworm or Trixie), or Ubuntu 24.x or later (amd64/x86-64 and aarch64). Make sure you follow the [Ubuntu post-install
instructions](https://rerdavies.github.io/pipedal/Configuring.html) to make sure your Ubuntu OS is using a realtime-capable kernel.
OP-Pedal runs on Raspbery Pi OS (Bookworm or Trixie), or Ubuntu 24.x or later (amd64/x86-64 and aarch64). Make sure you follow the [Ubuntu post-install
instructions](https://ourpad.casa/op-pedal/Configuring.html) to make sure your Ubuntu OS is using a realtime-capable kernel.
<img src="docs/gallery/dark-sshot1.png"></img>
+6
View File
@@ -0,0 +1,6 @@
<svg width="1626" height="536" viewBox="0 0 1626 536" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M594.746 359.92C584.669 359.92 578.111 357.745 575.073 353.396C572.034 349.047 572.336 342.17 575.98 332.766L625.756 202.482C629.407 193.081 634.402 186.205 640.743 181.852C647.084 177.499 655.296 175.325 665.381 175.328H753.975C764.048 175.328 770.52 177.505 773.392 181.857C776.264 186.21 775.878 193.085 772.234 202.482L722.442 332.766C718.791 342.166 713.882 349.043 707.716 353.396C701.55 357.749 693.428 359.923 683.351 359.92H594.746ZM644.277 304.045C642.188 309.442 641.71 313.098 642.841 315.012C643.973 316.927 646.709 317.884 651.05 317.884H658.869C663.21 317.884 666.642 316.927 669.165 315.012C671.688 313.098 673.99 309.442 676.072 304.045L703.954 231.203C706.043 225.813 706.564 222.157 705.518 220.235C704.471 218.314 701.78 217.357 697.442 217.364H689.606C685.258 217.364 681.783 218.321 679.183 220.235C676.582 222.15 674.235 225.806 672.143 231.203L644.277 304.045ZM941.736 181.857C944.604 186.207 944.213 193.083 940.562 202.488L914.505 270.886C910.855 280.287 905.946 287.163 899.78 291.516C893.613 295.869 885.492 298.044 875.415 298.04H815.994L792.269 359.92H734.947L789.676 217.364H778.467L805.069 175.328H922.325C932.398 175.328 938.868 177.505 941.736 181.857ZM850.916 256.015C855.257 256.015 858.689 255.058 861.212 253.144C863.735 251.229 866.037 247.575 868.119 242.182L872.287 231.214C874.368 225.824 874.89 222.168 873.851 220.247C872.812 218.325 870.118 217.368 865.77 217.375H847.01L832.156 256.015H850.916Z" fill="#D1D3D4"/>
<path d="M1007.47 317.884H1069.49L1042.65 359.92H933.978L1004.6 175.328H1061.92L1007.47 317.884ZM1254.78 175.328L1234.45 359.92H1178.16L1193.53 234.855L1142.72 317.884H1173.99L1146.89 359.92H1061.15L1174.77 175.328H1254.78ZM1245.65 359.92L1300.38 217.364H1289.18L1315.76 175.328H1433.03C1443.1 175.328 1449.57 177.505 1452.44 181.857C1455.31 186.21 1454.92 193.087 1451.27 202.488L1434.07 247.395C1433.31 249.36 1432.21 251.173 1430.81 252.748C1429.16 254.751 1427.34 256.753 1425.34 258.753C1423.4 260.697 1421.36 262.527 1419.22 264.234C1417.13 265.888 1415.39 267.237 1414 268.282C1414.72 269.547 1415.38 270.85 1415.96 272.185C1416.75 273.959 1417.4 275.791 1417.91 277.666C1418.43 279.59 1418.78 281.557 1418.95 283.543C1419.15 285.364 1418.89 287.207 1418.18 288.896L1401.48 332.755C1397.83 342.155 1392.92 349.032 1386.75 353.385C1380.59 357.738 1372.47 359.912 1362.39 359.909L1245.65 359.92ZM1319.15 317.884H1337.91C1342.25 317.884 1345.68 316.927 1348.2 315.012C1350.73 313.098 1353.03 309.442 1355.11 304.045L1355.63 302.478C1357.72 297.088 1358.24 293.434 1357.19 291.516C1356.14 289.598 1353.45 288.641 1349.12 288.645H1330.35L1319.15 317.884ZM1375.31 244.284C1377.82 242.369 1380.12 238.715 1382.21 233.322L1382.99 231.231C1385.08 225.841 1385.6 222.185 1384.56 220.263C1383.51 218.342 1380.82 217.384 1376.47 217.392H1357.7L1346.23 247.155H1365C1369.35 247.137 1372.78 246.17 1375.31 244.256V244.284ZM1530.75 217.392C1526.75 217.392 1523.54 218.566 1521.11 220.916C1518.68 223.265 1516.68 226.61 1515.12 230.952L1514.33 233.043C1512.94 237.221 1512.25 239.918 1512.25 241.133C1512.25 243.224 1512.99 244.618 1514.48 245.315C1515.97 246.012 1518.17 246.358 1521.12 246.358H1563.06C1573.14 246.358 1579.69 248.533 1582.74 252.882C1585.78 257.231 1585.47 264.108 1581.82 273.512L1559.17 332.766C1555.52 342.166 1550.52 349.043 1544.18 353.396C1537.84 357.749 1529.63 359.923 1519.55 359.92H1417.15L1422.88 317.884H1495.59C1499.94 317.884 1503.37 316.927 1505.89 315.012C1508.41 313.098 1510.71 309.442 1512.79 304.045L1513.58 302.222C1515.31 297.694 1516.01 294.386 1515.66 292.297C1515.31 290.208 1513.66 288.907 1510.71 288.394H1464.84C1454.77 288.394 1448.21 286.217 1445.17 281.865C1442.12 277.512 1442.43 270.635 1446.08 261.234L1468.73 202.22C1474.81 186.207 1485.58 177.243 1501.04 175.328H1619.36L1592.25 217.364L1530.75 217.392Z" fill="#1C75BC"/>
<path d="M266.908 483.889C381.732 483.889 474.816 387.384 474.816 268.338C474.816 149.293 381.732 52.7879 266.908 52.7879C152.084 52.7879 59 149.293 59 268.338C59 387.384 152.084 483.889 266.908 483.889Z" fill="#1C75BC"/>
<path d="M515.644 2.12828L151.951 231.672C149.848 232.999 148.143 234.906 147.025 237.181C145.908 239.456 145.424 242.008 145.626 244.552C145.828 247.096 146.708 249.532 148.169 251.587C149.63 253.642 151.613 255.234 153.897 256.186L179.804 267.026C181.745 267.838 183.474 269.116 184.848 270.754C186.222 272.393 187.202 274.345 187.709 276.451C188.216 278.558 188.235 280.759 187.764 282.875C187.293 284.99 186.346 286.96 185.001 288.624L3.22682 513.262C-6.50516 525.303 7.68406 542.137 20.4913 533.74L382.112 296.344C384.242 294.945 385.943 292.946 387.018 290.581C388.092 288.215 388.494 285.581 388.178 282.987C387.861 280.393 386.839 277.947 385.23 275.933C383.622 273.92 381.494 272.423 379.095 271.618L360.264 265.32C358.195 264.628 356.322 263.42 354.809 261.801C353.297 260.182 352.191 258.202 351.589 256.036C350.987 253.87 350.908 251.584 351.358 249.379C351.809 247.173 352.775 245.116 354.171 243.389L532.705 22.7271C542.359 10.7977 528.442 -5.96589 515.644 2.12828Z" fill="#D1D3D4"/>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="700" height="700" viewBox="0 0 700 700" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M349.203 572.671C468.026 572.671 564.351 472.819 564.351 349.645C564.351 226.471 468.026 126.619 349.203 126.619C230.38 126.619 134.055 226.471 134.055 349.645C134.055 472.819 230.38 572.671 349.203 572.671Z" fill="#1C75BC"/>
<path d="M606.601 74.2021L230.242 311.707C228.066 313.079 226.301 315.053 225.146 317.407C223.99 319.761 223.488 322.401 223.697 325.034C223.906 327.666 224.817 330.186 226.329 332.312C227.841 334.438 229.893 336.086 232.257 337.071L259.065 348.287C261.074 349.128 262.863 350.45 264.285 352.145C265.707 353.84 266.721 355.86 267.246 358.039C267.77 360.219 267.79 362.496 267.303 364.685C266.816 366.874 265.836 368.913 264.443 370.634L76.3392 603.063C66.2683 615.521 80.9517 632.939 94.2049 624.251L468.419 378.622C470.623 377.174 472.384 375.106 473.495 372.658C474.607 370.211 475.023 367.486 474.696 364.802C474.368 362.118 473.31 359.586 471.646 357.503C469.981 355.42 467.779 353.871 465.297 353.038L445.81 346.522C443.669 345.806 441.731 344.555 440.165 342.88C438.6 341.205 437.456 339.157 436.833 336.916C436.21 334.675 436.128 332.309 436.594 330.027C437.06 327.746 438.06 325.617 439.505 323.83L624.255 95.5153C634.246 83.1722 619.844 65.8272 606.601 74.2021Z" fill="#D1D3D4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

+30
View File
@@ -0,0 +1,30 @@
<svg width="1626" height="750" viewBox="0 0 1626 750" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- ===== OP LABS + OP PEDAL horizontal lockup ===== -->
<!-- Blue circle icon -->
<path d="M266.908 483.889C381.732 483.889 474.816 387.384 474.816 268.338C474.816 149.293 381.732 52.7879 266.908 52.7879C152.084 52.7879 59 149.293 59 268.338C59 387.384 152.084 483.889 266.908 483.889Z" fill="#1C75BC"/>
<!-- Silver swoosh -->
<path d="M515.644 2.12828L151.951 231.672C149.848 232.999 148.143 234.906 147.025 237.181C145.908 239.456 145.424 242.008 145.626 244.552C145.828 247.096 146.708 249.532 148.169 251.587C149.63 253.642 151.613 255.234 153.897 256.186L179.804 267.026C181.745 267.838 183.474 269.116 184.848 270.754C186.222 272.393 187.202 274.345 187.709 276.451C188.216 278.558 188.235 280.759 187.764 282.875C187.293 284.99 186.346 286.96 185.001 288.624L3.22682 513.262C-6.50516 525.303 7.68406 542.137 20.4913 533.74L382.112 296.344C384.242 294.945 385.943 292.946 387.018 290.581C388.092 288.215 388.494 285.581 388.178 282.987C387.861 280.393 386.839 277.947 385.23 275.933C383.622 273.92 381.494 272.423 379.095 271.618L360.264 265.32C358.195 264.628 356.322 263.42 354.809 261.801C353.297 260.182 352.191 258.202 351.589 256.036C350.987 253.87 350.908 251.584 351.358 249.379C351.809 247.173 352.775 245.116 354.171 243.389L532.705 22.7271C542.359 10.7977 528.442 -5.96589 515.644 2.12828Z" fill="#D1D3D4"/>
<!-- OP LABS (original brand path data) -->
<g transform="translate(0, -30)">
<!-- OP in silver -->
<path d="M594.746 359.92C584.669 359.92 578.111 357.745 575.073 353.396C572.034 349.047 572.336 342.17 575.98 332.766L625.756 202.482C629.407 193.081 634.402 186.205 640.743 181.852C647.084 177.499 655.296 175.325 665.381 175.328H753.975C764.048 175.328 770.52 177.505 773.392 181.857C776.264 186.21 775.878 193.085 772.234 202.482L722.442 332.766C718.791 342.166 713.882 349.043 707.716 353.396C701.55 357.749 693.428 359.923 683.351 359.92H594.746ZM644.277 304.045C642.188 309.442 641.71 313.098 642.841 315.012C643.973 316.927 646.709 317.884 651.05 317.884H658.869C663.21 317.884 666.642 316.927 669.165 315.012C671.688 313.098 673.99 309.442 676.072 304.045L703.954 231.203C706.043 225.813 706.564 222.157 705.518 220.235C704.471 218.314 701.78 217.357 697.442 217.364H689.606C685.258 217.364 681.783 218.321 679.183 220.235C676.582 222.15 674.235 225.806 672.143 231.203L644.277 304.045ZM941.736 181.857C944.604 186.207 944.213 193.083 940.562 202.488L914.505 270.886C910.855 280.287 905.946 287.163 899.78 291.516C893.613 295.869 885.492 298.044 875.415 298.04H815.994L792.269 359.92H734.947L789.676 217.364H778.467L805.069 175.328H922.325C932.398 175.328 938.868 177.505 941.736 181.857ZM850.916 256.015C855.257 256.015 858.689 255.058 861.212 253.144C863.735 251.229 866.037 247.575 868.119 242.182L872.287 231.214C874.368 225.824 874.89 222.168 873.851 220.247C872.812 218.325 870.118 217.368 865.77 217.375H847.01L832.156 256.015H850.916Z" fill="#D1D3D4"/>
<!-- LABS in blue -->
<path d="M1007.47 317.884H1069.49L1042.65 359.92H933.978L1004.6 175.328H1061.92L1007.47 317.884ZM1254.78 175.328L1234.45 359.92H1178.16L1193.53 234.855L1142.72 317.884H1173.99L1146.89 359.92H1061.15L1174.77 175.328H1254.78ZM1245.65 359.92L1300.38 217.364H1289.18L1315.76 175.328H1433.03C1443.1 175.328 1449.57 177.505 1452.44 181.857C1455.31 186.21 1454.92 193.087 1451.27 202.488L1434.07 247.395C1433.31 249.36 1432.21 251.173 1430.81 252.748C1429.16 254.751 1427.34 256.753 1425.34 258.753C1423.4 260.697 1421.36 262.527 1419.22 264.234C1417.13 265.888 1415.39 267.237 1414 268.282C1414.72 269.547 1415.38 270.85 1415.96 272.185C1416.75 273.959 1417.4 275.791 1417.91 277.666C1418.43 279.59 1418.78 281.557 1418.95 283.543C1419.15 285.364 1418.89 287.207 1418.18 288.896L1401.48 332.755C1397.83 342.155 1392.92 349.032 1386.75 353.385C1380.59 357.738 1372.47 359.912 1362.39 359.909L1245.65 359.92ZM1319.15 317.884H1337.91C1342.25 317.884 1345.68 316.927 1348.2 315.012C1350.73 313.098 1353.03 309.442 1355.11 304.045L1355.63 302.478C1357.72 297.088 1358.24 293.434 1357.19 291.516C1356.14 289.598 1353.45 288.641 1349.12 288.645H1330.35L1319.15 317.884ZM1375.31 244.284C1377.82 242.369 1380.12 238.715 1382.21 233.322L1382.99 231.231C1385.08 225.841 1385.6 222.185 1384.56 220.263C1383.51 218.342 1380.82 217.384 1376.47 217.392H1357.7L1346.23 247.155H1365C1369.35 247.137 1372.78 246.17 1375.31 244.256V244.284ZM1530.75 217.392C1526.75 217.392 1523.54 218.566 1521.11 220.916C1518.68 223.265 1516.68 226.61 1515.12 230.952L1514.33 233.043C1512.94 237.221 1512.25 239.918 1512.25 241.133C1512.25 243.224 1512.99 244.618 1514.48 245.315C1515.97 246.012 1518.17 246.358 1521.12 246.358H1563.06C1573.14 246.358 1579.69 248.533 1582.74 252.882C1585.78 257.231 1585.47 264.108 1581.82 273.512L1559.17 332.766C1555.52 342.166 1550.52 349.043 1544.18 353.396C1537.84 357.749 1529.63 359.923 1519.55 359.92H1417.15L1422.88 317.884H1495.59C1499.94 317.884 1503.37 316.927 1505.89 315.012C1508.41 313.098 1510.71 309.442 1512.79 304.045L1513.58 302.222C1515.31 297.694 1516.01 294.386 1515.66 292.297C1515.31 290.208 1513.66 288.907 1510.71 288.394H1464.84C1454.77 288.394 1448.21 286.217 1445.17 281.865C1442.12 277.512 1442.43 270.635 1446.08 261.234L1468.73 202.22C1474.81 186.207 1485.58 177.243 1501.04 175.328H1619.36L1592.25 217.364L1530.75 217.392Z" fill="#1C75BC"/>
</g>
<!-- ===== OP PEDAL subtitle ===== -->
<!-- Uses sans-serif — renders in ALL browsers, including Gitea's SVG viewer -->
<g font-family="Arial, Helvetica, sans-serif" font-size="72" font-weight="bold">
<!-- OP in silver -->
<text x="595" y="510" fill="#D1D3D4">O</text>
<text x="670" y="510" fill="#D1D3D4">P</text>
<!-- PEDAL in blue -->
<text x="890" y="510" fill="#1C75BC">P</text>
<text x="965" y="510" fill="#1C75BC">E</text>
<text x="1040" y="510" fill="#1C75BC">D</text>
<text x="1115" y="510" fill="#1C75BC">A</text>
<text x="1190" y="510" fill="#1C75BC">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

+32
View File
@@ -0,0 +1,32 @@
<svg width="700" height="900" viewBox="0 0 700 900" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- OP PEDAL Square Stacked Logo -->
<!-- Blue circle icon (from OP Labs) -->
<path d="M350 460C440.5 460 514 386.5 514 296C514 205.5 440.5 132 350 132C259.5 132 186 205.5 186 296C186 386.5 259.5 460 350 460Z" fill="#1C75BC"/>
<!-- Silver swoosh -->
<path d="M555 53L266 233.5C264.3 234.6 262.9 236.1 262 237.9C261.1 239.7 260.7 241.7 260.9 243.7C261.1 245.7 261.8 247.6 262.9 249.2C264 250.8 265.5 252 267.3 252.7L287 261.5C288.4 262.1 289.6 263.1 290.6 264.3C291.6 265.5 292.3 266.9 292.7 268.4C293.1 269.9 293.1 271.5 292.7 273C292.3 274.5 291.6 275.9 290.5 277L148 458C141 467 151 478 159 472L413 289.5C414.7 288.4 416.1 286.9 417 285.1C417.9 283.3 418.3 281.3 418.1 279.3C417.9 277.3 417.2 275.5 416.1 273.9C415 272.3 413.5 271.1 411.8 270.5L397 265.5C395.4 264.9 394 264 392.9 262.8C391.8 261.6 391 260.1 390.5 258.5C390 256.9 390 255.2 390.4 253.6C390.8 252 391.5 250.5 392.5 249.3L527 63C534.5 55 524 46 516 53Z" fill="#D1D3D4"/>
<!-- OP LABS - first line -->
<g font-family="sans-serif" font-size="90" font-weight="bold">
<text x="350" y="600" fill="#D1D3D4" text-anchor="middle">OP LABS</text>
</g>
<!-- OP PEDAL - second line, each letter individually positioned -->
<g font-family="sans-serif" font-size="72" font-weight="bold">
<!-- O -->
<text x="185" y="700" fill="#D1D3D4">O</text>
<!-- P -->
<text x="250" y="700" fill="#D1D3D4">P</text>
<!-- P -->
<text x="353" y="700" fill="#1C75BC">P</text>
<!-- E -->
<text x="418" y="700" fill="#1C75BC">E</text>
<!-- D -->
<text x="483" y="700" fill="#1C75BC">D</text>
<!-- A -->
<text x="548" y="700" fill="#1C75BC">A</text>
<!-- L -->
<text x="613" y="700" fill="#1C75BC">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+6
View File
@@ -3,8 +3,14 @@
set -e
# Make sure vite dependencies are up to date.
cd vite
npm install
cd ..
# clean build
if [ "$1" != "--continue" ]; then
rm -rf build
fi
+3 -3
View File
@@ -1,10 +1,10 @@
Depends: dnsmasq(>= 2.85),lv2-dev(>=1.14), iw
Description: IoT guitar effect for Raspberry Pi
IoT guitar effect pedal for Raspberry Pi, with phone-friendly web interface.
Homepage: https://github.com/rerdavies/pipedal
Homepage: https://ourpad.casa/op-pedal
Maintainer: Robin E. R. Davies<rerdavies@gmail.com>
Source: pipedal
Package: pipedal
Source: oppedal
Package: oppedal
Priority: optional
Section: sound
Version: 1.2.32
+7 -7
View File
@@ -13,18 +13,18 @@ page_icon: img/Install4.jpg
Download the most recent Debian (.deb) package for your platform:
- [Raspberry Pi OS bookworm (aarch64) v2.0.106](https://github.com/rerdavies/pipedal/releases/download/v2.0.106/pipedal_2.0.106_arm64.deb)
- [Ubuntu 24.04 through 25.04 (aarch64) v2.0.106](https://github.com/rerdavies/pipedal/releases/download/v2.0.106/pipedal_2.0.106_arm64.deb)
- [Ubuntu 24.04 through 25.04 (amd64) v2.0.106](https://github.com/rerdavies/pipedal/releases/download/v2.0.106/pipedal_2.0.106_amd64.deb)
- [Raspberry Pi OS bookworm (aarch64) v2.0.107](https://github.com/rerdavies/pipedal/releases/download/v2.0.107/pipedal_2.0.107_arm64.deb)
- [Ubuntu 24.04 through 25.04 (aarch64) v2.0.107](https://github.com/rerdavies/pipedal/releases/download/v2.0.107/pipedal_2.0.107_arm64.deb)
- [Ubuntu 24.04 through 25.04 (amd64) v2.0.107](https://github.com/rerdavies/pipedal/releases/download/v2.0.107/pipedal_2.0.107_amd64.deb)
Version 2.0.106 has been tested on Raspberry Pi OS bookworm, Ubuntu 24.04 (amd64), Ubuntu 24.10 (aarch64), and Ubuntu 25.04 (aarch64). Download the appropriate package for your platform, and install using the following procedure:
Version 2.0.107 has been tested on Raspberry Pi OS bookworm, Ubuntu 24.04 (amd64), Ubuntu 24.10 (aarch64), and Ubuntu 25.04 (aarch64). Download the appropriate package for your platform, and install using the following procedure:
```
sudo apt update
sudo apt upgrade
cd ~/Downloads
sudo apt-get install ./pipedal_2.0.106_arm64.deb
sudo apt-get install ./pipedal_2.0.107_arm64.deb
```
You MUST use `apt-get`. `apt` will not install downloaded packages; and `dpkg -i` will not install dependencies.
@@ -62,7 +62,7 @@ Use the following command to copy the downloaded package to the server from Linu
Adjust `username`, `server_address` and the actual name of the Debian package you downloaded as needed:
```
scp Downloads/pipedal_2.0.106_arm64.deb username@server_address:/home/username/
scp Downloads/pipedal_2.0.107_arm64.deb username@server_address:/home/username/
```
This will copy the downloaded package on your desktop computer to your home directory on the PiPedal server computer.
@@ -70,7 +70,7 @@ Once you have copied the package to the server, you can SSH into the server and
```
ssh username@server_address
sudo apt-get install ./pipedal_2.0.106_arm64.deb
sudo apt-get install ./pipedal_2.0.107_arm64.deb
```
The PiPedal package installer will print out the port number that the PiPedal web server is listening on. It defaults to port 80, but if you have another web server already running on port 80, it will select the next available port. Ubuntu default installs have Apache Server running on port 80, so the PiPedal web server will default to port 81 on Ubuntu.
+1 -1
View File
@@ -11,7 +11,7 @@
<a href="https://rerdavies.github.io/pipedal/download2"><img src="https://img.shields.io/badge/Download-008060" /></a>
<a href="https://rerdavies.github.io/pipedal/Documentation"><img src="https://img.shields.io/badge/Documentation-0060d0"/></a>
_To download PiPedal v2.0.106-alpha, click [*here*](download.md).
_To download PiPedal v2.0.107-alpha, click [*here*](download.md).
To view PiPedal documentation, click [*here*](Documentation.md)._
Use your Raspberry Pi as a guitar effects pedal. Configure and control PiPedal with your phone or tablet.
+5 -3
View File
@@ -1,6 +1,6 @@
# Release Notes
## PiPedal 2.0.106-Release
## PiPedal 2.0.107-Release
New Features
- New TooB Multi-Tap Delay plugin.
@@ -9,14 +9,16 @@ New Features
- New system MIDI bindings for Next/Previous Snapshot. Provided by Fulgenzio Ruiz Rubio.
Minor Features:
- Alpha release of PiPedal build procedure for ARCH Linux.
- Fix compilation warnings/errors when compiling with GCC 16.
- Alpha release of PiPedal build procedure for ARCH Linux. Provided by Fulgenzio Ruiz Rubio.
- Fix compilation warnings/errors when compiling with GCC 16. Provided by Fulgenzio Ruiz Rubio.
- Better display names for ALSA USB audio devices in the UI.
Bug fixes:
- Incorrect rendering when dragging plugins.
- MIDI hangups when using MIDI footpedals (or other high-rate CC messages). Provided by onirob.
- TooB Multi-Tap Delay: Mono plugin crash; Bypass not working.
## PiPedal 2.0.105-Release
+5 -5
View File
@@ -1,13 +1,13 @@
## Download
# Download PiPedal 2.0.106-alpha
# Download PiPedal 2.0.107-alpha
Download the most recent Debian (.deb) package for your platform:
- [Raspberry Pi OS bookworm (aarch64) v2.0.106 Alpha](https://github.com/rerdavies/pipedal/releases/download/v2.0.106/pipedal_2.0.106_arm64.deb)
- [Ubuntu 24.x, 25.04 (aarch64) v2.0.106 Alpha](https://github.com/rerdavies/pipedal/releases/download/v2.0.106/pipedal_2.0.106_arm64.deb)
- [Ubuntu 24.x, 25.04 (amd64/x86_64) v2.0.106 Alpha](https://github.com/rerdavies/pipedal/releases/download/v2.0.106/pipedal_2.0.106_amd64.deb)
- [Raspberry Pi OS bookworm (aarch64) v2.0.107 Alpha](https://github.com/rerdavies/pipedal/releases/download/v2.0.107/pipedal_2.0.107_arm64.deb)
- [Ubuntu 24.x, 25.04 (aarch64) v2.0.107 Alpha](https://github.com/rerdavies/pipedal/releases/download/v2.0.107/pipedal_2.0.107_arm64.deb)
- [Ubuntu 24.x, 25.04 (amd64/x86_64) v2.0.107 Alpha](https://github.com/rerdavies/pipedal/releases/download/v2.0.107/pipedal_2.0.107_amd64.deb)
Install the package by running
@@ -15,7 +15,7 @@ Install the package by running
```
sudo apt update
cd ~/Downloads
sudo apt install ./pipedal_2.0.106_arm64.deb
sudo apt install ./pipedal_2.0.107_arm64.deb
# or ... _amd64.deb as appropriate for your platform
```
The message about missing permissions given by `apt` is
+1 -1
View File
@@ -9,7 +9,7 @@
<a href="https://rerdavies.github.io/pipedal/download"><img src="https://img.shields.io/badge/Download-008060" /></a>
<a href="https://rerdavies.github.io/pipedal/Documentation"><img src="https://img.shields.io/badge/Documentation-0060d0"/></a>
_To download PiPedal v2.0.106 Release, click [*here*](download.md).
_To download PiPedal v2.0.107 Release, click [*here*](download.md).
To view PiPedal documentation, click [*here*](Documentation.md)._
Use your Raspberry Pi, or Ubuntu amd/x86-64 computer as a guitar effects pedal. Configure and control PiPedal remotedly, with your phone or tablet, or via a web browser.
+4 -4
View File
@@ -2,7 +2,7 @@
# copy files to installation directories
cmake --install build --prefix /usr --config Release -v
# Done as an install action by the debian package.
sudo /usr/bin/pipedalconfig --install
# copy pipedalPluginProfile as well.
sudo cp build/src/pipedalProfilePlugin /usr/bin/pipedalProfilePlugin
chmod +X /usr/bin/pipedalProfilePlugin
sudo /usr/bin/oppedalconfig --install
# copy oppedalProfilePlugin as well.
sudo cp build/src/oppedalProfilePlugin /usr/bin/oppedalProfilePlugin
chmod +X /usr/bin/oppedalProfilePlugin
+1 -1
View File
@@ -93,7 +93,7 @@ cabir:impulseFile3
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
TooB Cab IR is a convolution-based guitar cabinet impulse response simulator.
+1 -1
View File
@@ -50,7 +50,7 @@ toob:frequencyResponseVector
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
mod:brand "TooB";
mod:label "Cab Simulator";
@@ -53,7 +53,7 @@ toobimpulse:impulseFile
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
Toob Convolution Reverb Stereo uses convolution reverb impulse/response files in order to produce highly
@@ -51,7 +51,7 @@ toobimpulse:impulseFile
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
+1 -1
View File
@@ -66,7 +66,7 @@ inputStage:filterGroup
doap:license <https://two-play.com/TooB/licenses/isc> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
mod:brand "TooB";
mod:label "Input";
+1 -1
View File
@@ -68,7 +68,7 @@ pstage:stage3
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
mod:brand "TooB";
mod:label "Power Stage";
+1 -1
View File
@@ -57,7 +57,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment "TooB spectrum analyzer" ;
mod:brand "TooB";
+1 -1
View File
@@ -57,7 +57,7 @@ tonestack:eqGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
uiext:ui <http://two-play.com/plugins/toob-tone-stack-ui>;
+1 -1
View File
@@ -52,7 +52,7 @@ toob:frequencyResponseVector
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
uiext:ui <http://two-play.com/plugins/toob-three-band-eq-ui>;
@@ -52,7 +52,7 @@ toob:frequencyResponseVector
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
uiext:ui <http://two-play.com/plugins/toob-three-band-eq-stereo-ui>;
Binary file not shown.
+1 -1
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
Emulation of a Boss CE-2 Chorus.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
A straightforward no-frills digital delay.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
Emulation of a Boss BF-2 Flanger, based on circuit analysis and simulation of the original.
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
Digital emulation of a Boss BF-2 Flanger.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
Toob Freeverb is an Lv2 implementation of the famous Freeverb reverb effect. FreeVerb delivers a well-balanced reverb with very little tonal coloration.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
A 7 octave graphic equalizer, with frequencies chosen to be useful for EQ-ing guitar signals.
""" ;
+1 -1
View File
@@ -92,7 +92,7 @@ myprefix:output_group
doap:license <https://opensource.org/license/mit/> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 3 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
ui:ui <http://two-play.com/plugins/toob-looper-four-ui>;
+1 -1
View File
@@ -78,7 +78,7 @@ myprefix:output_group
doap:license <https://opensource.org/license/mit/> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 3 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
ui:ui <http://two-play.com/plugins/toob-looper-one-ui>;
+1 -1
View File
@@ -67,7 +67,7 @@ toobml:sagGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
The TooB ML Amplifier plugin provides emulation of a variety of amplifiers and overdrive pedals that are implemented
using neural-network-based machine learning models of real amplifiers.
+1 -1
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
Remix a stereo input signal.
+1 -1
View File
@@ -64,7 +64,7 @@ toobMultiEcho:tap4
doap:license <https:##rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
A 4-tap Echo/Delay effect.
@@ -64,7 +64,7 @@ toobMultiEcho:tap4
doap:license <https:##rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
A 4-tap Echo/Delay effect.
@@ -74,7 +74,7 @@ toobNam:calibrationGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
TooB Neural Amp Modeler is a neural network based Amp Simulator. It uses .nam model files that are generated by training nueral networks
on audio recordings of actual guitar amps, effects pedals and other equipment. .nam files contain data from the trained models, which can be loaded into
+1 -1
View File
@@ -54,7 +54,7 @@ noisegate:envelope_group
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
A noise gate is an audio processing tool that controls the volume of an audio signal
by allowing it to pass through only when it exceeds a set threshold.
+1 -1
View File
@@ -87,7 +87,7 @@ parametric_eq:hfGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
uiext:ui <http://two-play.com/plugins/toob-parametric-eq-ui>;
@@ -87,7 +87,7 @@ toob-parametric-eq-stereo:hfGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
uiext:ui <http://two-play.com/plugins/toob-parametric-eq-stereo-ui>;
+1 -1
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
A loose emulation of an MXR® Phase 90 Phaser.
+1 -1
View File
@@ -60,7 +60,7 @@ toobPlayer:seek
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
Play audio files. Audio files can optionally be loooped by pressing the "Set loop" button.
+1 -1
View File
@@ -51,7 +51,7 @@ recordPrefix:audioFile
doap:license <https://opensource.org/license/mit/> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 3 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
ui:ui <http://two-play.com/plugins/toob-record-mono-ui>;
+1 -1
View File
@@ -88,7 +88,7 @@ myprefix:loop3_group
doap:license <https://opensource.org/license/mit/> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 3 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
ui:ui <http://two-play.com/plugins/toob-record-stereo-ui>;
+1 -1
View File
@@ -43,7 +43,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
TooB Tone is a simple tone control. For more flexible EQ plugins, see TooB 3 Band Eq, or TooB Parameteric EQ.
+1 -1
View File
@@ -43,7 +43,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
TooB Tone is a simple tone control. For more flexible EQ plugins, see TooB 3 Band Eq, or TooB Parameteric EQ.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
A tremolo effect, equivalent to "vibrato" on vintage Fender tube amps.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
A tremolo effect, equivalent to "vibrato" on vintage Fender tube amps.
+1 -1
View File
@@ -44,7 +44,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
TooB Tuner is a chromatic guitar tuner.
""" ;
+1 -1
View File
@@ -39,7 +39,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ;
lv2:microVersion 81 ;
lv2:microVersion 82 ;
rdfs:comment """
Volume control.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,2 +1,2 @@
#!/bin/bash
build/src/pipedald "/etc/pipedal/config" "/etc/pipedal/react" "-port" "0.0.0.0:8080"
build/src/oppedald "/etc/oppedal/config" "/etc/oppedal/react" "-port" "0.0.0.0:8080"
+10
View File
@@ -411,6 +411,16 @@ namespace pipedal
return this->sampleRate;
}
virtual uint32_t GetDeviceCaptureChannels() const override
{
return (uint32_t)this->captureChannels;
}
virtual uint32_t GetDevicePlaybackChannels() const override
{
return (uint32_t)this->playbackChannels;
}
JackServerSettings jackServerSettings;
std::string alsa_device_name;
+6
View File
@@ -57,6 +57,12 @@ namespace pipedal {
virtual uint32_t GetSampleRate() = 0;
/// Get the number of capture (input) channels the device actually provides.
virtual uint32_t GetDeviceCaptureChannels() const = 0;
/// Get the number of playback (output) channels the device actually provides.
virtual uint32_t GetDevicePlaybackChannels() const = 0;
virtual size_t GetMidiInputEventCount() = 0;
virtual MidiEvent*GetMidiEvents() = 0;
+80 -5
View File
@@ -24,12 +24,14 @@
#include <lv2/atom/atom.h>
#include "SchedulerPriority.hpp"
#include "AlsaSequencer.hpp"
#include "MixerEngine.hpp"
#include "Lv2Log.hpp"
#include "SchedulerPriority.hpp"
#include "JackDriver.hpp"
#include "AlsaDriver.hpp"
#include "PipeWireDriver.hpp"
#include "DummyAudioDriver.hpp"
#include "AtomConverter.hpp"
#include <unordered_map>
@@ -481,6 +483,7 @@ private:
Uris uris;
std::unique_ptr<AudioDriver> audioDriver;
std::string driverType_;
std::recursive_mutex mutex;
int64_t overrunGracePeriodSamples = 0;
@@ -535,6 +538,10 @@ private:
std::vector<std::shared_ptr<Lv2Pedalboard>> activePedalboards; // pedalboards that have been sent to the audio queue.
Lv2Pedalboard *realtimeActivePedalboard = nullptr;
// Band-in-a-Box Mixer Engine
std::shared_ptr<MixerEngine> currentMixerEngine;
MixerEngine *realtimeActiveMixerEngine = nullptr;
uint32_t sampleRate = 0;
uint64_t currentSample = 0;
@@ -1083,6 +1090,11 @@ private:
{
OnSnapshotTriggered(5);
}
else if (this->realtimeActiveMixerEngine != nullptr && (event.buffer[0] & 0xF0) == 0xB0)
{
// Route MIDI CC to mixer engine's control surface mapper
this->realtimeActiveMixerEngine->processMidiEvent(event);
}
else
{
ProcessMidiMonitor(eventBufferWriter, iterator, event);
@@ -1197,6 +1209,24 @@ private:
PIPEDAL_NON_INLINE void ProcessLv2Pedalboard(size_t nframes)
{
// Band-in-a-Box Mixer Engine takes priority over legacy pedalboard
MixerEngine *mixerEngine = this->realtimeActiveMixerEngine;
if (mixerEngine != nullptr)
{
// Route through mixer engine using device input/output buffers
auto &driver = this->audioDriver;
if (driver) {
mixerEngine->process(
driver->DeviceInputBuffers().data(),
(uint32_t)driver->DeviceInputBufferCount(),
driver->DeviceOutputBuffers().data(),
(uint32_t)driver->DeviceOutputBufferCount(),
(uint32_t)nframes
);
}
return;
}
Lv2Pedalboard *pedalboard = nullptr;
std::vector<float *> *pInputBuffers;
@@ -1395,7 +1425,7 @@ private:
}
bool processed = false;
if (pedalboard != nullptr)
if (pedalboard != nullptr || this->realtimeActiveMixerEngine != nullptr)
{
ProcessGlobalMidiInput();
}
@@ -1421,7 +1451,7 @@ private:
}
public:
AudioHostImpl(IHost *pHost)
AudioHostImpl(IHost *pHost, const std::string &driverType)
: inputRingBuffer(RING_BUFFER_SIZE),
outputRingBuffer(RING_BUFFER_SIZE),
realtimeReader(&this->inputRingBuffer),
@@ -1431,7 +1461,8 @@ public:
eventBufferUrids(pHost),
pHost(pHost),
uris(pHost),
atomConverter(pHost->GetMapFeature())
atomConverter(pHost->GetMapFeature()),
driverType_(driverType)
{
realtimeAtomBuffer.resize(32 * 1024);
lv2_atom_forge_init(&inputWriterForge, pHost->GetMapFeature().GetMap());
@@ -1470,6 +1501,25 @@ public:
{
return this->sampleRate;
}
virtual uint32_t GetDeviceCaptureChannels() override
{
if (this->audioDriver)
{
return this->audioDriver->GetDeviceCaptureChannels();
}
return 2;
}
virtual uint32_t GetDevicePlaybackChannels() override
{
if (this->audioDriver)
{
return this->audioDriver->GetDevicePlaybackChannels();
}
return 2;
}
void HandleAlsaSequencerDevicesChanged(
AlsaSequencerDeviceMonitor::MonitorAction action, int client, const std::string &clientName)
{
@@ -1858,7 +1908,16 @@ public:
isOpen = true;
this->isDummyAudioDriver = jackServerSettings.IsDummyAudioDevice();
if (driverType_ == "pipewire")
{
this->audioDriver = std::unique_ptr<AudioDriver>(CreatePipeWireDriver(this));
Lv2Log::info("Using PipeWire audio driver.");
}
else
{
this->audioDriver = std::unique_ptr<AudioDriver>(CreateAlsaDriver(this));
}
this->currentSample = 0;
this->underruns = 0;
@@ -1944,6 +2003,22 @@ public:
}
}
virtual void SetMixerEngine(const std::shared_ptr<MixerEngine> &mixerEngine)
{
std::lock_guard guard(mutex);
this->currentMixerEngine = mixerEngine;
if (active && mixerEngine)
{
// Activate the mixer engine and set it as the realtime processing target.
// The mixer engine takes over from the legacy pedalboard.
this->realtimeActiveMixerEngine = mixerEngine.get();
}
else if (!mixerEngine)
{
this->realtimeActiveMixerEngine = nullptr;
}
}
virtual void SetBypass(uint64_t instanceId, bool enabled)
{
std::lock_guard guard(mutex);
@@ -2458,9 +2533,9 @@ void AudioHostImpl::SetSystemMidiBindings(const std::vector<MidiBinding> &bindin
}
}
AudioHost *AudioHost::CreateInstance(IHost *pHost)
AudioHost *AudioHost::CreateInstance(IHost *pHost, const std::string &driverType)
{
return new AudioHostImpl(pHost);
return new AudioHostImpl(pHost, driverType);
}
// Removed because any updates to state have to be sent to clients as well,
+13 -1
View File
@@ -37,6 +37,8 @@
namespace pipedal
{
class MixerEngine; // forward declaration for band-in-a-box mode
struct RealtimeMidiProgramRequest;
struct RealtimeNextMidiProgramRequest;
class PluginHost;
@@ -213,7 +215,7 @@ namespace pipedal
AudioHost() {}
public:
static AudioHost *CreateInstance(IHost *pHost);
static AudioHost *CreateInstance(IHost *pHost, const std::string &driverType = "alsa");
virtual ~AudioHost() {};
virtual void UpdateServerConfiguration(const JackServerSettings &jackServerSettings,
@@ -235,10 +237,20 @@ namespace pipedal
virtual void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0;
virtual uint32_t GetSampleRate() = 0;
/// Get the number of capture (input) channels the audio device provides.
virtual uint32_t GetDeviceCaptureChannels() = 0;
/// Get the number of playback (output) channels the audio device provides.
virtual uint32_t GetDevicePlaybackChannels() = 0;
virtual JackConfiguration GetServerConfiguration() = 0;
virtual void SetPedalboard(const std::shared_ptr<Lv2Pedalboard> &pedalboard) = 0;
/// Set the mixer engine for band-in-a-box mode.
/// When set, overrides the legacy pedalboard processing.
virtual void SetMixerEngine(const std::shared_ptr<MixerEngine> &mixerEngine) = 0;
virtual void SetControlValue(uint64_t instanceId, const std::string &symbol, float value) = 0;
+9
View File
@@ -366,9 +366,18 @@ set (PIPEDAL_SOURCES
JackDriver.cpp JackDriver.hpp
AlsaDriver.cpp AlsaDriver.hpp
DummyAudioDriver.cpp DummyAudioDriver.hpp
PipeWireDriver.cpp PipeWireDriver.hpp
AudioDriver.hpp
AudioConfig.hpp
# Mixer Engine (Band-in-a-Box)
MixerChannelStrip.cpp MixerChannelStrip.hpp
MixerBus.cpp MixerBus.hpp
MixerEngine.cpp MixerEngine.hpp
MixerApi.cpp MixerApi.hpp
MidiMapper.cpp MidiMapper.hpp
MidiLearnMode.cpp MidiLearnMode.hpp
${VST3_SOURCES}
)
+10
View File
@@ -136,6 +136,16 @@ namespace pipedal
return this->sampleRate;
}
virtual uint32_t GetDeviceCaptureChannels() const override
{
return (uint32_t)deviceCaptureBuffers.size();
}
virtual uint32_t GetDevicePlaybackChannels() const override
{
return (uint32_t)devicePlaybackBuffers.size();
}
JackServerSettings jackServerSettings;
AlsaSequencer::ptr alsaSequencer;
+114
View File
@@ -0,0 +1,114 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#include "pch.h"
#include "MidiLearnMode.hpp"
using namespace pipedal;
MidiLearnMode::MidiLearnMode()
{
}
MidiLearnMode::~MidiLearnMode()
{
}
void MidiLearnMode::setEnabled(bool enabled)
{
std::lock_guard<std::mutex> lock(mutex_);
enabled_ = enabled;
if (!enabled) {
capturedMidiChannel_ = -1;
capturedCcNumber_ = -1;
}
}
void MidiLearnMode::setPendingTarget(MidiTargetType type, int64_t id)
{
std::lock_guard<std::mutex> lock(mutex_);
pendingTargetType_ = type;
pendingTargetId_ = id;
}
bool MidiLearnMode::getPendingTarget(MidiTargetType& outType, int64_t& outId) const
{
std::lock_guard<std::mutex> lock(mutex_);
outType = pendingTargetType_;
outId = pendingTargetId_;
return true;
}
void MidiLearnMode::captureEvent(int midiChannel, int ccNumber)
{
std::lock_guard<std::mutex> lock(mutex_);
if (!enabled_) return;
capturedMidiChannel_ = midiChannel;
capturedCcNumber_ = ccNumber;
}
bool MidiLearnMode::hasCapturedEvent() const
{
std::lock_guard<std::mutex> lock(mutex_);
return capturedMidiChannel_ >= 0 && capturedCcNumber_ >= 0;
}
bool MidiLearnMode::getCapturedEvent(int& outMidiChannel, int& outCcNumber) const
{
std::lock_guard<std::mutex> lock(mutex_);
if (capturedMidiChannel_ < 0 || capturedCcNumber_ < 0) return false;
outMidiChannel = capturedMidiChannel_;
outCcNumber = capturedCcNumber_;
return true;
}
MidiMappingEntry MidiLearnMode::buildMapping() const
{
MidiMappingEntry entry;
std::lock_guard<std::mutex> lock(mutex_);
entry.midiChannel = capturedMidiChannel_;
entry.ccNumber = capturedCcNumber_;
entry.targetType = pendingTargetType_;
entry.targetId = pendingTargetId_;
// Sensible default ranges based on target type
switch (entry.targetType) {
case MidiTargetType::ChannelVolume:
case MidiTargetType::BusVolume:
case MidiTargetType::MasterVolume:
entry.minValue = -96.0f;
entry.maxValue = 12.0f;
break;
case MidiTargetType::ChannelPan:
entry.minValue = -1.0f;
entry.maxValue = 1.0f;
break;
case MidiTargetType::ChannelMute:
case MidiTargetType::ChannelSolo:
case MidiTargetType::BusMute:
case MidiTargetType::MasterMute:
entry.minValue = 0.0f;
entry.maxValue = 1.0f;
break;
}
return entry;
}
void MidiLearnMode::clearCapturedEvent()
{
std::lock_guard<std::mutex> lock(mutex_);
capturedMidiChannel_ = -1;
capturedCcNumber_ = -1;
}
void MidiLearnMode::reset()
{
std::lock_guard<std::mutex> lock(mutex_);
enabled_ = false;
pendingTargetType_ = MidiTargetType::ChannelVolume;
pendingTargetId_ = 0;
capturedMidiChannel_ = -1;
capturedCcNumber_ = -1;
}
+66
View File
@@ -0,0 +1,66 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#pragma once
#include <cstdint>
#include <mutex>
#include "MidiMapper.hpp"
namespace pipedal {
/// MIDI Learn mode state machine.
///
/// Tracks the three-step learn workflow:
/// 1. User enables learn mode and touches a UI control (setPendingTarget)
/// 2. User moves a hardware fader — the CC event is captured (captureEvent)
/// 3. User confirms — a new MidiMappingEntry is created (commitMapping)
///
class MidiLearnMode {
public:
MidiLearnMode();
~MidiLearnMode();
/// Enable or disable learn mode.
void setEnabled(bool enabled);
bool isEnabled() const { return enabled_; }
/// Set the mixer parameter that should receive the next learned mapping.
/// Call this when the user touches a UI control while in learn mode.
void setPendingTarget(MidiTargetType type, int64_t id);
/// Get the current pending target.
bool getPendingTarget(MidiTargetType& outType, int64_t& outId) const;
/// Capture a MIDI CC event while in learn mode.
/// Call this from the RT audio thread when processEvent sees a CC.
void captureEvent(int midiChannel, int ccNumber);
/// Check if a CC event has been captured since learn mode was entered
/// or since the last clear().
bool hasCapturedEvent() const;
/// Get the last captured CC event info.
/// Returns true if an event was captured.
bool getCapturedEvent(int& outMidiChannel, int& outCcNumber) const;
/// Build a MidiMappingEntry from pending target + captured event.
/// Clears the captured event after building (avoids stale recomit).
MidiMappingEntry buildMapping() const;
/// Clear captured event without committing.
void clearCapturedEvent();
/// Reset all learn state.
void reset();
private:
bool enabled_ = false;
MidiTargetType pendingTargetType_ = MidiTargetType::ChannelVolume;
int64_t pendingTargetId_ = 0;
int capturedMidiChannel_ = -1;
int capturedCcNumber_ = -1;
mutable std::mutex mutex_;
};
} // namespace pipedal
+415
View File
@@ -0,0 +1,415 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#include "pch.h"
#include "MidiMapper.hpp"
#include "MixerEngine.hpp"
#include "MixerChannelStrip.hpp"
#include "MixerBus.hpp"
#include "MidiEvent.hpp"
#include "json.hpp"
#include <cmath>
#include <filesystem>
#include <fstream>
#include <sstream>
using namespace pipedal;
// ─── Target type string conversion ──────────────────────────────────────────
MidiTargetType MidiMappingEntry::targetTypeFromString(const std::string& str)
{
if (str == "channelVolume") return MidiTargetType::ChannelVolume;
if (str == "channelPan") return MidiTargetType::ChannelPan;
if (str == "channelMute") return MidiTargetType::ChannelMute;
if (str == "channelSolo") return MidiTargetType::ChannelSolo;
if (str == "busVolume") return MidiTargetType::BusVolume;
if (str == "busMute") return MidiTargetType::BusMute;
if (str == "masterVolume") return MidiTargetType::MasterVolume;
if (str == "masterMute") return MidiTargetType::MasterMute;
return MidiTargetType::ChannelVolume;
}
const char* MidiMappingEntry::targetTypeToString(MidiTargetType type)
{
switch (type) {
case MidiTargetType::ChannelVolume: return "channelVolume";
case MidiTargetType::ChannelPan: return "channelPan";
case MidiTargetType::ChannelMute: return "channelMute";
case MidiTargetType::ChannelSolo: return "channelSolo";
case MidiTargetType::BusVolume: return "busVolume";
case MidiTargetType::BusMute: return "busMute";
case MidiTargetType::MasterVolume: return "masterVolume";
case MidiTargetType::MasterMute: return "masterMute";
}
return "channelVolume";
}
// ─── MidiMapper ─────────────────────────────────────────────────────────────
MidiMapper::MidiMapper()
{
}
MidiMapper::~MidiMapper()
{
}
bool MidiMapper::processEvent(const MidiEvent& event)
{
if (!mixerEngine_) return false;
// Only process MIDI CC messages (0xB0)
if (event.size < 3) return false;
uint8_t command = event.buffer[0] & 0xF0;
if (command != 0xB0) return false;
int midiChannel = static_cast<int>(event.buffer[0] & 0x0F);
int ccNumber = static_cast<int>(event.buffer[1]);
uint8_t ccValue = event.buffer[2];
// ── Learn mode: capture the CC event ──
if (learnMode_) {
std::lock_guard<std::mutex> lock(learnMutex_);
lastLearnedMidiChannel_ = midiChannel;
lastLearnedCcNumber_ = ccNumber;
}
// ── Snapshot the current mapping table ──
std::vector<MidiMappingEntry> mappingsSnapshot;
{
std::lock_guard<std::mutex> lock(mappingsMutex_);
mappingsSnapshot = mappings_;
}
// ── Apply matching mappings ──
bool consumed = false;
for (const auto& entry : mappingsSnapshot) {
// Match MIDI channel (-1 = omni)
if (entry.midiChannel >= 0 && entry.midiChannel != midiChannel) {
continue;
}
if (entry.ccNumber != ccNumber) {
continue;
}
applyValue(entry, ccValue);
consumed = true;
}
return consumed;
}
void MidiMapper::applyValue(const MidiMappingEntry& entry, uint8_t ccValue)
{
if (!mixerEngine_) return;
// Map CC 0-127 to parameter range
float range = entry.maxValue - entry.minValue;
float normalized = static_cast<float>(ccValue) / 127.0f;
float mappedValue = entry.minValue + normalized * range;
switch (entry.targetType) {
case MidiTargetType::ChannelVolume: {
auto* ch = mixerEngine_->getChannel(static_cast<int>(entry.targetId));
if (ch) ch->setVolume(mappedValue);
break;
}
case MidiTargetType::ChannelPan: {
auto* ch = mixerEngine_->getChannel(static_cast<int>(entry.targetId));
if (ch) ch->setPan(mappedValue);
break;
}
case MidiTargetType::ChannelMute: {
auto* ch = mixerEngine_->getChannel(static_cast<int>(entry.targetId));
if (ch) ch->setMute(mappedValue >= 0.5f);
break;
}
case MidiTargetType::ChannelSolo: {
auto* ch = mixerEngine_->getChannel(static_cast<int>(entry.targetId));
if (ch) ch->setSolo(mappedValue >= 0.5f);
break;
}
case MidiTargetType::BusVolume: {
auto* bus = mixerEngine_->getBus(entry.targetId);
if (bus) bus->setVolume(mappedValue);
break;
}
case MidiTargetType::BusMute: {
auto* bus = mixerEngine_->getBus(entry.targetId);
if (bus) bus->setMute(mappedValue >= 0.5f);
break;
}
case MidiTargetType::MasterVolume: {
auto* bus = mixerEngine_->masterBus();
if (bus) bus->setVolume(mappedValue);
break;
}
case MidiTargetType::MasterMute: {
auto* bus = mixerEngine_->masterBus();
if (bus) bus->setMute(mappedValue >= 0.5f);
break;
}
}
}
// ─── Mapping table management ───────────────────────────────────────────────
void MidiMapper::setMappings(const std::vector<MidiMappingEntry>& mappings)
{
std::lock_guard<std::mutex> lock(mappingsMutex_);
mappings_ = mappings;
}
void MidiMapper::addMapping(const MidiMappingEntry& entry)
{
std::lock_guard<std::mutex> lock(mappingsMutex_);
mappings_.push_back(entry);
}
bool MidiMapper::removeMapping(int midiChannel, int ccNumber)
{
std::lock_guard<std::mutex> lock(mappingsMutex_);
auto it = std::remove_if(mappings_.begin(), mappings_.end(),
[midiChannel, ccNumber](const MidiMappingEntry& e) {
return e.midiChannel == midiChannel && e.ccNumber == ccNumber;
});
bool removed = (it != mappings_.end());
mappings_.erase(it, mappings_.end());
return removed;
}
bool MidiMapper::removeMappingByIndex(size_t index)
{
std::lock_guard<std::mutex> lock(mappingsMutex_);
if (index >= mappings_.size()) return false;
mappings_.erase(mappings_.begin() + static_cast<ptrdiff_t>(index));
return true;
}
void MidiMapper::clearMappings()
{
std::lock_guard<std::mutex> lock(mappingsMutex_);
mappings_.clear();
}
std::vector<MidiMappingEntry> MidiMapper::getMappings() const
{
std::lock_guard<std::mutex> lock(mappingsMutex_);
return mappings_;
}
// ─── JSON serialization ─────────────────────────────────────────────────────
std::string MidiMapper::getMappingsJson() const
{
auto mappings = getMappings();
std::stringstream ss;
json_writer writer(ss, false);
writer.start_array();
for (const auto& entry : mappings) {
writer.start_object();
writer.write_member("midiChannel", (int64_t)entry.midiChannel);
writer.write_member("ccNumber", (int64_t)entry.ccNumber);
writer.write_member("targetType", MidiMappingEntry::targetTypeToString(entry.targetType));
writer.write_member("targetId", entry.targetId);
writer.write_member("minValue", (double)entry.minValue);
writer.write_member("maxValue", (double)entry.maxValue);
writer.end_object();
}
writer.end_array();
return ss.str();
}
void MidiMapper::setMappingsFromJson(const std::string& json)
{
std::vector<MidiMappingEntry> entries;
std::stringstream ss(json);
json_reader reader(ss);
// Parse array: [ ... ]
reader.consume('[');
while (reader.peek() != ']') {
MidiMappingEntry entry;
std::string targetTypeStr;
// Parse object: { "key": value, ... }
reader.consume('{');
while (reader.peek() != '}') {
std::string key;
reader.read(&key);
reader.consume(':');
if (key == "midiChannel") {
int64_t v; reader.read(&v); entry.midiChannel = (int)v;
} else if (key == "ccNumber") {
int64_t v; reader.read(&v); entry.ccNumber = (int)v;
} else if (key == "targetType") {
reader.read(&targetTypeStr);
} else if (key == "targetId") {
reader.read(&entry.targetId);
} else if (key == "minValue") {
double v; reader.read(&v); entry.minValue = (float)v;
} else if (key == "maxValue") {
double v; reader.read(&v); entry.maxValue = (float)v;
} else {
reader.skip_property();
}
// Consume comma separator
if (reader.peek() == ',') {
reader.consume(',');
}
}
reader.consume('}'); // end object
if (!targetTypeStr.empty()) {
entry.targetType = MidiMappingEntry::targetTypeFromString(targetTypeStr);
}
entries.push_back(entry);
// Consume comma separator between array elements
if (reader.peek() == ',') {
reader.consume(',');
}
}
reader.consume(']'); // end array
setMappings(entries);
}
std::string MidiMapper::defaultConfigPath()
{
// Store alongside other pipedal config
const char* home = std::getenv("HOME");
if (home) {
return std::string(home) + "/.config/pipedal/midi_map.json";
}
return "/etc/pipedal/config/midi_map.json";
}
void MidiMapper::loadFromFile()
{
std::string path = defaultConfigPath();
std::ifstream file(path);
if (!file.is_open()) return;
std::stringstream ss;
ss << file.rdbuf();
std::string content = ss.str();
if (!content.empty()) {
setMappingsFromJson(content);
}
}
void MidiMapper::saveToFile() const
{
std::string path = defaultConfigPath();
// Ensure directory exists
std::filesystem::path dir = std::filesystem::path(path).parent_path();
std::error_code ec;
std::filesystem::create_directories(dir, ec);
std::string json = getMappingsJson();
std::ofstream file(path);
if (file.is_open()) {
file << json;
}
}
// ─── Learn mode ─────────────────────────────────────────────────────────────
void MidiMapper::setLearnMode(bool enabled)
{
{
std::lock_guard<std::mutex> lock(learnMutex_);
learnMode_ = enabled;
if (!enabled) {
// Clear last learned on exit
lastLearnedMidiChannel_ = -1;
lastLearnedCcNumber_ = -1;
}
}
}
void MidiMapper::setPendingLearnTarget(MidiTargetType type, int64_t id)
{
std::lock_guard<std::mutex> lock(learnMutex_);
pendingTargetType_ = type;
pendingTargetId_ = id;
}
bool MidiMapper::getLastLearnedEvent(int& outMidiChannel, int& outCcNumber) const
{
std::lock_guard<std::mutex> lock(learnMutex_);
if (lastLearnedMidiChannel_ < 0 || lastLearnedCcNumber_ < 0) return false;
outMidiChannel = lastLearnedMidiChannel_;
outCcNumber = lastLearnedCcNumber_;
return true;
}
void MidiMapper::clearLastLearnedEvent()
{
std::lock_guard<std::mutex> lock(learnMutex_);
lastLearnedMidiChannel_ = -1;
lastLearnedCcNumber_ = -1;
}
bool MidiMapper::commitLearnMapping()
{
std::lock_guard<std::mutex> lock(learnMutex_);
if (lastLearnedMidiChannel_ < 0 || lastLearnedCcNumber_ < 0) {
return false; // No CC event captured yet
}
MidiMappingEntry entry;
entry.midiChannel = lastLearnedMidiChannel_;
entry.ccNumber = lastLearnedCcNumber_;
entry.targetType = pendingTargetType_;
entry.targetId = pendingTargetId_;
// Set sensible defaults based on target type
switch (entry.targetType) {
case MidiTargetType::ChannelVolume:
case MidiTargetType::BusVolume:
case MidiTargetType::MasterVolume:
entry.minValue = -96.0f; // -inf dB
entry.maxValue = 12.0f; // +12 dB max
break;
case MidiTargetType::ChannelPan:
entry.minValue = -1.0f; // full left
entry.maxValue = 1.0f; // full right
break;
case MidiTargetType::ChannelMute:
case MidiTargetType::ChannelSolo:
case MidiTargetType::BusMute:
case MidiTargetType::MasterMute:
entry.minValue = 0.0f; // off
entry.maxValue = 1.0f; // on (threshold 0.5)
break;
}
// Reset learned event so we don't recomit the same one
lastLearnedMidiChannel_ = -1;
lastLearnedCcNumber_ = -1;
// Add to mapping table
{
std::lock_guard<std::mutex> lockMap(mappingsMutex_);
mappings_.push_back(entry);
}
saveToFile();
return true;
}
bool MidiMapper::getPendingLearnTarget(MidiTargetType& outType, int64_t& outId) const
{
std::lock_guard<std::mutex> lock(learnMutex_);
outType = pendingTargetType_;
outId = pendingTargetId_;
return true;
}
+149
View File
@@ -0,0 +1,149 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include <mutex>
#include <memory>
namespace pipedal {
class MixerEngine;
struct MidiEvent;
/// Types of mixer parameters that can be mapped from MIDI CC.
enum class MidiTargetType {
ChannelVolume, ///< Channel fader (-inf .. +12 dB)
ChannelPan, ///< Channel pan (-1 .. +1)
ChannelMute, ///< Channel mute toggle
ChannelSolo, ///< Channel solo toggle
BusVolume, ///< Bus fader (-inf .. +12 dB)
BusMute, ///< Bus mute toggle
MasterVolume, ///< Master bus volume
MasterMute, ///< Master bus mute
};
/// A single mapping entry: MIDI CC# + channel → mixer parameter.
struct MidiMappingEntry {
int midiChannel = -1; ///< MIDI channel (-1 = omni / any)
int ccNumber = 0; ///< MIDI CC number (0-127)
MidiTargetType targetType = MidiTargetType::ChannelVolume;
int64_t targetId = 0; ///< channel index for Channel*, bus ID for Bus*
/// Output range: CC=0 maps to minValue, CC=127 maps to maxValue.
float minValue = 0.0f;
float maxValue = 1.0f;
/// Convert between enum and string (for JSON serialization).
static MidiTargetType targetTypeFromString(const std::string& str);
static const char* targetTypeToString(MidiTargetType type);
};
/// MIDI CC → mixer parameter mapper.
///
/// Receives MIDI CC events (0xB0) from the real-time audio thread and applies
/// them to the MixerEngine via atomic parameter setters.
///
/// The mapping table is configured from the non-real-time thread; a mutex
/// protects the table while the RT path snapshots the current mapping set.
class MidiMapper {
public:
MidiMapper();
~MidiMapper();
/// Set the mixer engine to control. Must be set before processing events.
void setMixerEngine(MixerEngine* engine) { mixerEngine_ = engine; }
/// Process a MIDI event. Returns true if a mapping consumed the event.
/// RT-safe: uses atomic mixer setters directly.
bool processEvent(const MidiEvent& event);
// --- Mapping table management (non-RT thread) ---
/// Replace the entire mapping table.
void setMappings(const std::vector<MidiMappingEntry>& mappings);
/// Add a single mapping entry.
void addMapping(const MidiMappingEntry& entry);
/// Remove all mappings matching the given MIDI channel and CC number.
bool removeMapping(int midiChannel, int ccNumber);
/// Remove a specific mapping entry by index.
bool removeMappingByIndex(size_t index);
/// Clear all mappings.
void clearMappings();
/// Get a copy of the current mapping table.
std::vector<MidiMappingEntry> getMappings() const;
// --- Persistence ---
/// Serialize mappings to JSON string.
std::string getMappingsJson() const;
/// Deserialize mappings from JSON string.
void setMappingsFromJson(const std::string& json);
/// Default config file path.
static std::string defaultConfigPath();
/// Load mappings from default config file.
void loadFromFile();
/// Save mappings to default config file.
void saveToFile() const;
// --- Learn mode ---
/// Enable/disable MIDI learn mode.
/// When enabled, each incoming CC event is captured and cached
/// so the next call to commitLearnMapping() will create a mapping.
void setLearnMode(bool enabled);
/// True if learn mode is active.
bool learnMode() const { return learnMode_; }
/// Set the target parameter for the next learned mapping.
/// Call this when the user touches a UI control.
void setPendingLearnTarget(MidiTargetType type, int64_t id);
/// Get the last-learned MIDI event info.
/// Returns true if a CC event was captured since learn mode was enabled
/// or since the last clearLastLearnedEvent().
bool getLastLearnedEvent(int& outMidiChannel, int& outCcNumber) const;
/// Commit the current pending learn target + last CC into a mapping entry.
/// Returns the new entry, or nullopt if no CC was captured.
bool commitLearnMapping();
/// Clear cached last-learned CC event.
void clearLastLearnedEvent();
/// Get pending learn target info.
bool getPendingLearnTarget(MidiTargetType& outType, int64_t& outId) const;
private:
MixerEngine* mixerEngine_ = nullptr;
// Mapping table — mutable for snapshot-copy in RT path
std::vector<MidiMappingEntry> mappings_;
mutable std::mutex mappingsMutex_;
// Learn mode state
bool learnMode_ = false;
MidiTargetType pendingTargetType_ = MidiTargetType::ChannelVolume;
int64_t pendingTargetId_ = 0;
int lastLearnedMidiChannel_ = -1;
int lastLearnedCcNumber_ = -1;
mutable std::mutex learnMutex_;
// Apply a single mapping entry value to the mixer (RT-safe).
void applyValue(const MidiMappingEntry& entry, uint8_t ccValue);
};
} // namespace pipedal
+754
View File
@@ -0,0 +1,754 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#include "pch.h"
#include "MixerApi.hpp"
#include "MixerEngine.hpp"
#include "MixerChannelStrip.hpp"
#include "MixerBus.hpp"
#include "json.hpp"
#include "json_variant.hpp"
#include "Lv2Log.hpp"
#include <sstream>
#include <filesystem>
#include <fstream>
#include <algorithm>
#include <cstdlib>
using namespace pipedal;
// ---------------------------------------------------------------------------
// Scene storage types (JSON-serializable, mapped via JSON_MAP macros)
// ---------------------------------------------------------------------------
namespace pipedal {
/// A single saved scene.
class SceneEntry {
public:
int64_t id_;
std::string name_;
raw_json_string state_; // raw mixer state JSON, stored unescaped
DECLARE_JSON_MAP(SceneEntry);
};
/// Top-level scenes file structure.
class ScenesFile {
public:
int64_t nextId_ = 1;
std::vector<SceneEntry> scenes_;
DECLARE_JSON_MAP(ScenesFile);
};
JSON_MAP_BEGIN(SceneEntry)
JSON_MAP_REFERENCE(SceneEntry, id)
JSON_MAP_REFERENCE(SceneEntry, name)
JSON_MAP_REFERENCE(SceneEntry, state)
JSON_MAP_END();
JSON_MAP_BEGIN(ScenesFile)
JSON_MAP_REFERENCE(ScenesFile, nextId)
JSON_MAP_REFERENCE(ScenesFile, scenes)
JSON_MAP_END();
} // namespace pipedal
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/// Resolve ~/op-pedal/default_config/ to an absolute path.
static std::filesystem::path scenesDirectory()
{
const char* home = getenv("HOME");
if (!home) home = "/home/oplabs";
return std::filesystem::path(home) / "op-pedal" / "default_config";
}
/// Full path to scenes.json.
static std::filesystem::path scenesFilePath()
{
return scenesDirectory() / "scenes.json";
}
/// Load scenes from disk. Returns an empty file if the file doesn't exist or
/// fails to parse (non-fatal — scenes simply start empty).
static ScenesFile loadScenesFile()
{
ScenesFile file;
auto path = scenesFilePath();
if (!std::filesystem::exists(path))
return file;
try {
std::ifstream s(path);
if (!s.is_open())
return file;
json_reader reader(s);
reader.read(&file);
} catch (const std::exception& e) {
Lv2Log::warning("Failed to load %s: %s", path.c_str(), e.what());
// Return empty file on parse failure
}
return file;
}
/// Save scenes to disk.
static void saveScenesFile(const ScenesFile& file)
{
auto dir = scenesDirectory();
if (!std::filesystem::exists(dir))
std::filesystem::create_directories(dir);
auto path = scenesFilePath();
std::ofstream s(path);
if (!s.is_open())
{
Lv2Log::error("Failed to write %s", path.c_str());
return;
}
json_writer writer(s, false);
writer.write(file);
s.close();
}
// ---------------------------------------------------------------------------
// MixerApi implementation
// ---------------------------------------------------------------------------
MixerApi::MixerApi()
{
}
MixerApi::~MixerApi()
{
}
void MixerApi::setChannelVolume(int channelIndex, float volumeDb)
{
if (!mixerEngine_) return;
auto* channel = mixerEngine_->getChannel(channelIndex);
if (channel) channel->setVolume(volumeDb);
}
void MixerApi::setChannelPan(int channelIndex, float pan)
{
if (!mixerEngine_) return;
auto* channel = mixerEngine_->getChannel(channelIndex);
if (channel) channel->setPan(pan);
}
void MixerApi::setChannelMute(int channelIndex, bool mute)
{
if (!mixerEngine_) return;
auto* channel = mixerEngine_->getChannel(channelIndex);
if (channel) channel->setMute(mute);
}
void MixerApi::setChannelSolo(int channelIndex, bool solo)
{
if (!mixerEngine_) return;
auto* channel = mixerEngine_->getChannel(channelIndex);
if (channel) channel->setSolo(solo);
}
void MixerApi::setChannelLabel(int channelIndex, const std::string& label)
{
if (!mixerEngine_) return;
auto* channel = mixerEngine_->getChannel(channelIndex);
if (channel) channel->setLabel(label);
}
void MixerApi::setChannelType(int channelIndex, const std::string& type)
{
if (!mixerEngine_) return;
auto* channel = mixerEngine_->getChannel(channelIndex);
if (!channel) return;
if (type == "Instrument" || type == "instrument") {
channel->setChannelType(MixerChannelType::Instrument);
} else if (type == "Mic" || type == "mic") {
channel->setChannelType(MixerChannelType::Mic);
} else if (type == "Line" || type == "line") {
channel->setChannelType(MixerChannelType::Line);
}
}
void MixerApi::setChannelHpf(int channelIndex, bool enabled, float frequency)
{
if (!mixerEngine_) return;
auto* channel = mixerEngine_->getChannel(channelIndex);
if (channel) {
channel->setHpEnabled(enabled);
if (frequency > 0) channel->setHpFrequency(frequency);
}
}
int MixerApi::addChannel(int physicalInputIndex)
{
if (!mixerEngine_) return -1;
auto* channel = mixerEngine_->addChannel(physicalInputIndex);
return channel ? channel->channelIndex() : -1;
}
void MixerApi::removeChannel(int channelIndex)
{
if (!mixerEngine_) return;
mixerEngine_->removeChannel(channelIndex);
}
void MixerApi::setBusVolume(int64_t busId, float volumeDb)
{
if (!mixerEngine_) return;
auto* bus = mixerEngine_->getBus(busId);
if (bus) bus->setVolume(volumeDb);
}
void MixerApi::setBusMute(int64_t busId, bool mute)
{
if (!mixerEngine_) return;
auto* bus = mixerEngine_->getBus(busId);
if (bus) bus->setMute(mute);
}
int64_t MixerApi::addBus(const std::string& type, const std::string& name, int channels)
{
if (!mixerEngine_) return -1;
MixerBusType busType = MixerBusType::Subgroup;
if (type == "Master" || type == "master") busType = MixerBusType::Master;
else if (type == "Aux" || type == "aux") busType = MixerBusType::Aux;
else if (type == "FxReturn" || type == "fxreturn") busType = MixerBusType::FxReturn;
else if (type == "Subgroup" || type == "subgroup") busType = MixerBusType::Subgroup;
return mixerEngine_->addBus(busType, name, channels);
}
void MixerApi::removeBus(int64_t busId)
{
if (!mixerEngine_) return;
mixerEngine_->removeBus(busId);
}
void MixerApi::routeChannelToBus(int channelIndex, int64_t busId, float levelDb)
{
if (!mixerEngine_) return;
mixerEngine_->routeChannelToBus(channelIndex, busId, levelDb);
}
void MixerApi::routeBusToBus(int64_t sourceBusId, int64_t targetBusId, float levelDb)
{
if (!mixerEngine_) return;
mixerEngine_->routeBusToBus(sourceBusId, targetBusId, levelDb);
}
void MixerApi::removeRoute(int64_t sourceId, int64_t targetBusId)
{
if (!mixerEngine_) return;
mixerEngine_->removeRoute(sourceId, targetBusId);
}
std::string MixerApi::getStateJson() const
{
if (!mixerEngine_) return "{}";
auto snapshot = mixerEngine_->captureSnapshot();
std::stringstream ss;
json_writer writer(ss, false);
writer.start_object();
writer.write_member("channels", "");
// Overwrite the empty string with raw array
writer.write_raw("[");
bool first = true;
for (const auto& cs : snapshot.channels) {
if (!first) writer.write_raw(",");
first = false;
writer.start_object();
writer.write_member("channelIndex", (int64_t)cs.channelIndex);
writer.write_member("volume", (double)cs.volume);
writer.write_member("pan", (double)cs.pan);
writer.write_member("mute", cs.mute);
writer.write_member("solo", cs.solo);
const char* typeStr = "Instrument";
switch (cs.channelType) {
case MixerChannelType::Mic: typeStr = "Mic"; break;
case MixerChannelType::Line: typeStr = "Line"; break;
case MixerChannelType::AuxReturn: typeStr = "AuxReturn"; break;
default: typeStr = "Instrument"; break;
}
writer.write_member("type", typeStr);
writer.write_member("label", cs.label);
writer.write_member("hpEnabled", cs.hpEnabled);
writer.write_member("hpFrequency", (double)cs.hpFrequency);
writer.write_member("vuLeft", (double)cs.vuLeft);
writer.write_member("vuRight", (double)cs.vuRight);
writer.write_member("gateOpen", cs.gateOpen);
writer.write_member("compressorReduction", (double)cs.compressorReduction);
writer.end_object();
}
writer.write_raw("]");
writer.write_member("buses", "");
writer.write_raw("[");
first = true;
for (const auto& bs : snapshot.buses) {
if (!first) writer.write_raw(",");
first = false;
writer.start_object();
writer.write_member("id", bs.id);
writer.write_member("name", bs.name);
const char* typeStr = "Subgroup";
switch (bs.type) {
case MixerBusType::Master: typeStr = "Master"; break;
case MixerBusType::Aux: typeStr = "Aux"; break;
case MixerBusType::FxReturn: typeStr = "FxReturn"; break;
default: typeStr = "Subgroup"; break;
}
writer.write_member("type", typeStr);
writer.write_member("volume", (double)bs.volume);
writer.write_member("mute", bs.mute);
writer.write_member("vuLeft", (double)bs.vuLeft);
writer.write_member("vuRight", (double)bs.vuRight);
writer.end_object();
}
writer.write_raw("]");
writer.write_member("routes", "");
writer.write_raw("[");
first = true;
for (const auto& route : snapshot.routes) {
if (!first) writer.write_raw(",");
first = false;
writer.start_object();
writer.write_member("sourceId", route.sourceId);
writer.write_member("targetBusId", route.targetBusId);
writer.write_member("level", (double)route.level);
const char* sourceType = (route.sourceType == MixerRouteEntry::SourceChannel) ? "channel" : "bus";
writer.write_member("sourceType", sourceType);
writer.end_object();
}
writer.write_raw("]");
// Output routing
writer.write_member("outputRoutes", "");
writer.write_raw("[");
first = true;
for (const auto& route : mixerEngine_->outputRoutes()) {
if (!first) writer.write_raw(",");
first = false;
writer.start_object();
writer.write_member("sourceBusId", route.sourceBusId);
writer.write_member("sourceStartChannel", (int64_t)route.sourceStartChannel);
writer.write_member("targetStartChannel", (int64_t)route.targetStartChannel);
writer.write_member("channels", (int64_t)route.channels);
writer.end_object();
}
writer.write_raw("]");
// Physical I/O info
writer.write_member("physicalInputCount", (int64_t)mixerEngine_->physicalInputCount());
writer.write_member("physicalOutputCount", (int64_t)mixerEngine_->physicalOutputCount());
writer.end_object();
return ss.str();
}
// ---------------------------------------------------------------------------
// Scene management
// ---------------------------------------------------------------------------
std::string MixerApi::saveScene(const std::string& name)
{
if (!mixerEngine_) {
return "{\"error\":\"no mixer engine\"}";
}
// Capture current mixer state
std::string stateJson = getStateJson();
// Load existing scenes
ScenesFile file = loadScenesFile();
// Create new scene
SceneEntry entry;
entry.id_ = file.nextId_++;
entry.name_ = name;
entry.state_ = raw_json_string(stateJson);
file.scenes_.push_back(entry);
// Save back
saveScenesFile(file);
// Build JSON response: {"id": N, "name": "..."}
std::stringstream ss;
json_writer writer(ss, false);
writer.start_object();
writer.write_member("id", entry.id_);
writer.write_member("name", entry.name_);
writer.end_object();
return ss.str();
}
bool MixerApi::loadScene(const std::string& sceneId)
{
if (!mixerEngine_) return false;
// Try parsing as integer
int64_t targetId;
try {
targetId = std::stoll(sceneId);
} catch (...) {
return false;
}
// Load scenes and find matching ID
ScenesFile file = loadScenesFile();
auto it = std::find_if(file.scenes_.begin(), file.scenes_.end(),
[targetId](const SceneEntry& e) { return e.id_ == targetId; });
if (it == file.scenes_.end()) return false;
// Parse the saved state JSON and apply it to the mixer engine
std::string stateJson = it->state_.as_string();
if (stateJson.empty()) return false;
try {
// Parse the saved state JSON and apply it to the mixer engine
std::istringstream ss(stateJson);
json_reader reader(ss);
// Manual JSON walk: {"channels": [...], "buses": [...], "routes": [...]}
reader.start_object(); // {
while (reader.peek() != '}')
{
std::string memberName = reader.read_string();
reader.consume(':');
if (memberName == "channels")
{
reader.consume('['); // start array
while (reader.peek() != ']')
{
int64_t channelIndex = 0;
double volume = 0.0;
double pan = 0.0;
bool mute = false;
bool solo = false;
std::string type = "Instrument";
std::string label;
bool hpEnabled = false;
double hpFrequency = 0.0;
reader.start_object(); // {
while (reader.peek() != '}')
{
std::string fieldName = reader.read_string();
reader.consume(':');
if (fieldName == "channelIndex") reader.read(&channelIndex);
else if (fieldName == "volume") reader.read(&volume);
else if (fieldName == "pan") reader.read(&pan);
else if (fieldName == "mute") reader.read(&mute);
else if (fieldName == "solo") reader.read(&solo);
else if (fieldName == "type") reader.read(&type);
else if (fieldName == "label") reader.read(&label);
else if (fieldName == "hpEnabled") reader.read(&hpEnabled);
else if (fieldName == "hpFrequency") reader.read(&hpFrequency);
else reader.skip_property();
if (reader.peek() == ',') reader.consume(',');
}
reader.end_object(); // }
// Apply to matching channel
auto* channel = mixerEngine_->getChannel((int)channelIndex);
if (channel) {
channel->setVolume((float)volume);
channel->setPan((float)pan);
channel->setMute(mute);
channel->setSolo(solo);
MixerChannelType ct = MixerChannelType::Instrument;
if (type == "Mic" || type == "mic") ct = MixerChannelType::Mic;
else if (type == "Line" || type == "line") ct = MixerChannelType::Line;
else if (type == "AuxReturn" || type == "auxreturn") ct = MixerChannelType::AuxReturn;
channel->setChannelType(ct);
channel->setLabel(label);
channel->setHpEnabled(hpEnabled);
if (hpFrequency > 0) channel->setHpFrequency((float)hpFrequency);
}
if (reader.peek() == ',') reader.consume(',');
}
reader.consume(']'); // end array
}
else if (memberName == "buses")
{
reader.consume('['); // start array
while (reader.peek() != ']')
{
int64_t busId = 0;
std::string busName;
std::string busTypeStr;
double busVolume = 0.0;
bool busMute = false;
reader.start_object(); // {
while (reader.peek() != '}')
{
std::string fieldName = reader.read_string();
reader.consume(':');
if (fieldName == "id") reader.read(&busId);
else if (fieldName == "name") reader.read(&busName);
else if (fieldName == "type") reader.read(&busTypeStr);
else if (fieldName == "volume") reader.read(&busVolume);
else if (fieldName == "mute") reader.read(&busMute);
else reader.skip_property();
if (reader.peek() == ',') reader.consume(',');
}
reader.end_object(); // }
auto* bus = mixerEngine_->getBus(busId);
if (bus) {
bus->setVolume((float)busVolume);
bus->setMute(busMute);
}
if (reader.peek() == ',') reader.consume(',');
}
reader.consume(']'); // end array
}
else if (memberName == "routes")
{
// Skip routes — they are structural and shouldn't be overwritten
// on scene load for safety.
reader.skip_property();
}
else
{
reader.skip_property();
}
if (reader.peek() == ',') reader.consume(',');
}
reader.end_object(); // }
return true;
} catch (const std::exception& e) {
Lv2Log::error("Failed to load scene %s: %s", sceneId.c_str(), e.what());
return false;
}
}
std::string MixerApi::listScenes() const
{
ScenesFile file = loadScenesFile();
std::stringstream ss;
json_writer writer(ss, false);
writer.start_object();
writer.write_member("scenes", "");
writer.write_raw("[");
bool first = true;
for (const auto& entry : file.scenes_) {
if (!first) writer.write_raw(",");
first = false;
writer.start_object();
writer.write_member("id", entry.id_);
writer.write_member("name", entry.name_);
writer.end_object();
}
writer.write_raw("]");
writer.end_object();
return ss.str();
}
bool MixerApi::deleteScene(const std::string& sceneId)
{
int64_t targetId;
try {
targetId = std::stoll(sceneId);
} catch (...) {
return false;
}
ScenesFile file = loadScenesFile();
auto it = std::find_if(file.scenes_.begin(), file.scenes_.end(),
[targetId](const SceneEntry& e) { return e.id_ == targetId; });
if (it == file.scenes_.end()) return false;
file.scenes_.erase(it);
saveScenesFile(file);
return true;
}
// ─── MIDI Control Surface Mapping ──────────────────────────────────────────
std::string MixerApi::getMidiMappingsJson() const
{
if (!mixerEngine_) return "[]";
return mixerEngine_->midiMapper().getMappingsJson();
}
void MixerApi::setMidiMappingsFromJson(const std::string& json)
{
if (!mixerEngine_) return;
mixerEngine_->midiMapper().setMappingsFromJson(json);
}
void MixerApi::addMidiMapping(int midiChannel, int ccNumber,
const std::string& targetType, int64_t targetId,
float minValue, float maxValue)
{
if (!mixerEngine_) return;
MidiMappingEntry entry;
entry.midiChannel = midiChannel;
entry.ccNumber = ccNumber;
entry.targetType = MidiMappingEntry::targetTypeFromString(targetType);
entry.targetId = targetId;
entry.minValue = minValue;
entry.maxValue = maxValue;
mixerEngine_->midiMapper().addMapping(entry);
mixerEngine_->midiMapper().saveToFile();
}
void MixerApi::removeMidiMapping(int midiChannel, int ccNumber)
{
if (!mixerEngine_) return;
mixerEngine_->midiMapper().removeMapping(midiChannel, ccNumber);
mixerEngine_->midiMapper().saveToFile();
}
void MixerApi::removeMidiMappingByIndex(size_t index)
{
if (!mixerEngine_) return;
mixerEngine_->midiMapper().removeMappingByIndex(index);
mixerEngine_->midiMapper().saveToFile();
}
void MixerApi::clearMidiMappings()
{
if (!mixerEngine_) return;
mixerEngine_->midiMapper().clearMappings();
mixerEngine_->midiMapper().saveToFile();
}
void MixerApi::setMidiLearnMode(bool enabled)
{
if (!mixerEngine_) return;
mixerEngine_->midiMapper().setLearnMode(enabled);
}
bool MixerApi::getMidiLearnMode() const
{
if (!mixerEngine_) return false;
return mixerEngine_->midiMapper().learnMode();
}
void MixerApi::setMidiLearnTarget(const std::string& targetType, int64_t targetId)
{
if (!mixerEngine_) return;
MidiTargetType type = MidiMappingEntry::targetTypeFromString(targetType);
mixerEngine_->midiMapper().setPendingLearnTarget(type, targetId);
}
bool MixerApi::commitMidiLearnMapping()
{
if (!mixerEngine_) return false;
return mixerEngine_->midiMapper().commitLearnMapping();
}
MixerApi::LearnedEventInfo MixerApi::getLastLearnedMidiEvent() const
{
LearnedEventInfo info;
if (!mixerEngine_) return info;
info.hasEvent = mixerEngine_->midiMapper().getLastLearnedEvent(info.midiChannel, info.ccNumber);
return info;
}
void MixerApi::saveMidiMappingsToFile() const
{
if (!mixerEngine_) return;
mixerEngine_->midiMapper().saveToFile();
}
void MixerApi::loadMidiMappingsFromFile()
{
if (!mixerEngine_) return;
mixerEngine_->midiMapper().loadFromFile();
}
// ---------------------------------------------------------------------------
// Output Routing
// ---------------------------------------------------------------------------
std::string MixerApi::getOutputRoutesJson() const
{
if (!mixerEngine_) return "[]";
const auto& routes = mixerEngine_->outputRoutes();
std::stringstream ss;
json_writer writer(ss, false);
writer.write_raw("[");
bool first = true;
for (const auto& route : routes) {
if (!first) writer.write_raw(",");
first = false;
writer.start_object();
writer.write_member("sourceBusId", route.sourceBusId);
writer.write_member("sourceStartChannel", (int64_t)route.sourceStartChannel);
writer.write_member("targetStartChannel", (int64_t)route.targetStartChannel);
writer.write_member("channels", (int64_t)route.channels);
writer.end_object();
}
writer.write_raw("]");
return ss.str();
}
void MixerApi::setOutputRoutesFromJson(const std::string& json)
{
if (!mixerEngine_) return;
std::vector<MixerEngine::MixerOutputRoute> routes;
std::istringstream ss(json);
json_reader reader(ss);
reader.consume('[');
while (reader.peek() != ']')
{
MixerEngine::MixerOutputRoute route{};
reader.start_object();
while (reader.peek() != '}')
{
std::string key = reader.read_string();
reader.consume(':');
if (key == "sourceBusId") reader.read(&route.sourceBusId);
else if (key == "sourceStartChannel") { int64_t v; reader.read(&v); route.sourceStartChannel = (int)v; }
else if (key == "targetStartChannel") { int64_t v; reader.read(&v); route.targetStartChannel = (int)v; }
else if (key == "channels") { int64_t v; reader.read(&v); route.channels = (int)v; }
else reader.skip_property();
if (reader.peek() == ',') reader.consume(',');
}
reader.end_object();
routes.push_back(route);
if (reader.peek() == ',') reader.consume(',');
}
reader.consume(']');
mixerEngine_->setOutputRoutes(routes);
}
+165
View File
@@ -0,0 +1,165 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#pragma once
#include <string>
#include <memory>
#include <functional>
namespace pipedal {
class MixerEngine;
/// Mixer API — bridges WebSocket messages to the MixerEngine.
///
/// Follows the existing PiPedalSocket pattern where handlers are registered
/// via REGISTER_MESSAGE_HANDLER and dispatched by message name.
///
/// This class provides the model-level methods that the socket handlers call.
class MixerApi {
public:
MixerApi();
~MixerApi();
/// Set the mixer engine this API talks to.
void setMixerEngine(MixerEngine* engine) { mixerEngine_ = engine; }
MixerEngine* mixerEngine() const { return mixerEngine_; }
/// --- Channel Control ---
/// Set channel volume in dB (-inf to +12)
void setChannelVolume(int channelIndex, float volumeDb);
/// Set channel pan (-1.0 left to +1.0 right)
void setChannelPan(int channelIndex, float pan);
/// Set channel mute state
void setChannelMute(int channelIndex, bool mute);
/// Set channel solo state
void setChannelSolo(int channelIndex, bool solo);
/// Set channel label
void setChannelLabel(int channelIndex, const std::string& label);
/// Set channel type (Instrument, Mic, Line)
void setChannelType(int channelIndex, const std::string& type);
/// Set channel HPF state
void setChannelHpf(int channelIndex, bool enabled, float frequency);
/// --- Channel Lifecycle ---
/// Add a new channel for the given physical input
int addChannel(int physicalInputIndex);
/// Remove a channel
void removeChannel(int channelIndex);
/// --- Bus Control ---
/// Set bus volume
void setBusVolume(int64_t busId, float volumeDb);
/// Set bus mute
void setBusMute(int64_t busId, bool mute);
/// Add a new bus
int64_t addBus(const std::string& type, const std::string& name, int channels);
/// Remove a bus
void removeBus(int64_t busId);
/// --- Routing ---
/// Route a channel to a bus
void routeChannelToBus(int channelIndex, int64_t busId, float levelDb);
/// Route a bus to another bus
void routeBusToBus(int64_t sourceBusId, int64_t targetBusId, float levelDb);
/// Remove a route
void removeRoute(int64_t sourceId, int64_t targetBusId);
/// --- State Queries ---
/// Get the full mixer state as a JSON string
std::string getStateJson() const;
/// --- Output Routing ---
/// Get output routes as a JSON array string
std::string getOutputRoutesJson() const;
/// Set output routes from a JSON array string
void setOutputRoutesFromJson(const std::string& json);
/// Apply a full mixer state from a JSON string
void setFullState(const std::string& stateJson);
/// --- Scenes ---
/// Save current mixer state as a scene
std::string saveScene(const std::string& name);
/// Load a scene by name
bool loadScene(const std::string& sceneId);
/// List available scenes
std::string listScenes() const;
/// Delete a scene
bool deleteScene(const std::string& sceneId);
/// --- MIDI Control Surface Mapping ---
/// Get all MIDI mappings as JSON
std::string getMidiMappingsJson() const;
/// Set all MIDI mappings from JSON
void setMidiMappingsFromJson(const std::string& json);
/// Add a single MIDI mapping
void addMidiMapping(int midiChannel, int ccNumber,
const std::string& targetType, int64_t targetId,
float minValue, float maxValue);
/// Remove MIDI mapping by CC and channel
void removeMidiMapping(int midiChannel, int ccNumber);
/// Remove MIDI mapping by index
void removeMidiMappingByIndex(size_t index);
/// Clear all MIDI mappings
void clearMidiMappings();
/// Toggle MIDI learn mode
void setMidiLearnMode(bool enabled);
bool getMidiLearnMode() const;
/// Set the pending learn target (which UI control was touched)
void setMidiLearnTarget(const std::string& targetType, int64_t targetId);
/// Commit a learned mapping (captured CC + pending target)
bool commitMidiLearnMapping();
/// Get last learned CC event info (for UI feedback)
struct LearnedEventInfo {
bool hasEvent = false;
int midiChannel = -1;
int ccNumber = -1;
};
LearnedEventInfo getLastLearnedMidiEvent() const;
/// Save MIDI mappings to config file
void saveMidiMappingsToFile() const;
/// Load MIDI mappings from config file
void loadMidiMappingsFromFile();
private:
MixerEngine* mixerEngine_ = nullptr;
};
} // namespace pipedal
+174
View File
@@ -0,0 +1,174 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#include "pch.h"
#include "MixerBus.hpp"
#include <cmath>
#include <algorithm>
#include <cstring>
using namespace pipedal;
MixerBus::MixerBus(int64_t id, MixerBusType type, const std::string& name, int channels)
: id_(id)
, type_(type)
, name_(name)
, channelCount_(channels)
{
buffers_.resize(channels);
}
void MixerBus::setVolume(float db)
{
volume_ = std::clamp(db, -96.0f, 12.0f);
}
void MixerBus::setMute(bool mute)
{
mute_ = mute;
}
void MixerBus::allocateBuffers(size_t maxFrames)
{
maxFrames_ = maxFrames;
for (auto& buf : buffers_) {
buf.resize(maxFrames, 0.0f);
}
}
void MixerBus::clear()
{
for (auto& buf : buffers_) {
std::fill(buf.begin(), buf.end(), 0.0f);
}
}
void MixerBus::accumulate(
const float* const* source,
uint32_t frames,
float gain,
int sourceChannels)
{
uint32_t n = std::min(frames, (uint32_t)maxFrames_);
int nChannels = std::min(sourceChannels, channelCount_);
if (std::abs(gain) < 0.0001f) return;
if (std::abs(gain - 1.0f) < 0.0001f) {
// Unity gain fast path
for (int ch = 0; ch < nChannels; ++ch) {
if (ch < (int)buffers_.size() && ch < sourceChannels && source[ch]) {
float* dst = buffers_[ch].data();
const float* src = source[ch];
for (uint32_t i = 0; i < n; ++i) {
dst[i] += src[i];
}
}
}
} else {
// Scaled accumulation
for (int ch = 0; ch < nChannels; ++ch) {
if (ch < (int)buffers_.size() && ch < sourceChannels && source[ch]) {
float* dst = buffers_[ch].data();
const float* src = source[ch];
for (uint32_t i = 0; i < n; ++i) {
dst[i] += src[i] * gain;
}
}
}
}
}
void MixerBus::accumulateMono(
const float* source,
uint32_t frames,
float gain)
{
if (!source || buffers_.empty()) return;
uint32_t n = std::min(frames, (uint32_t)maxFrames_);
float* dst = buffers_[0].data();
if (std::abs(gain) < 0.0001f) return;
if (std::abs(gain - 1.0f) < 0.0001f) {
for (uint32_t i = 0; i < n; ++i) {
dst[i] += source[i];
}
} else {
for (uint32_t i = 0; i < n; ++i) {
dst[i] += source[i] * gain;
}
}
}
void MixerBus::process(uint32_t frames)
{
uint32_t n = std::min(frames, (uint32_t)maxFrames_);
bool isMuted = mute_.load();
float volumeGain = isMuted ? 0.0f : std::pow(10.0f, volume_.load() / 20.0f);
// Peak VU tracking
float leftPeak = -96.0f;
float rightPeak = -96.0f;
if (std::abs(volumeGain) < 0.0001f) {
// Effectively mute
for (int ch = 0; ch < channelCount_; ++ch) {
if (ch < (int)buffers_.size()) {
std::fill(buffers_[ch].begin(), buffers_[ch].begin() + n, 0.0f);
}
}
} else if (std::abs(volumeGain - 1.0f) < 0.001f) {
// Unity gain — no scaling needed, just compute VU
for (uint32_t i = 0; i < n; ++i) {
if (buffers_.size() > 0) {
float absVal = std::abs(buffers_[0][i]);
if (absVal > leftPeak) leftPeak = absVal;
}
if (buffers_.size() > 1) {
float absVal = std::abs(buffers_[1][i]);
if (absVal > rightPeak) rightPeak = absVal;
}
}
} else {
// Apply volume gain
for (int ch = 0; ch < channelCount_; ++ch) {
if (ch >= (int)buffers_.size()) break;
float* buf = buffers_[ch].data();
for (uint32_t i = 0; i < n; ++i) {
buf[i] *= volumeGain;
}
}
// Compute VU from scaled signal
for (uint32_t i = 0; i < n; ++i) {
if (buffers_.size() > 0) {
float absVal = std::abs(buffers_[0][i]);
if (absVal > leftPeak) leftPeak = absVal;
}
if (buffers_.size() > 1) {
float absVal = std::abs(buffers_[1][i]);
if (absVal > rightPeak) rightPeak = absVal;
}
}
}
// Convert peak to dB with decay
float leftDb = (leftPeak > 0.00001f) ? 20.0f * std::log10(leftPeak) : -96.0f;
float rightDb = (rightPeak > 0.00001f) ? 20.0f * std::log10(rightPeak) : -96.0f;
float oldLeft = vuLeft_.load();
float oldRight = vuRight_.load();
if (leftDb > oldLeft) {
vuLeft_ = leftDb;
} else {
vuLeft_ = oldLeft * 0.95f + leftDb * 0.05f;
}
if (rightDb > oldRight) {
vuRight_ = rightDb;
} else {
vuRight_ = oldRight * 0.95f + rightDb * 0.05f;
}
}
+124
View File
@@ -0,0 +1,124 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#pragma once
#include <string>
#include <vector>
#include <memory>
#include <atomic>
#include <cstdint>
namespace pipedal {
/// Types of buses in the mixer architecture.
enum class MixerBusType {
Master, // Main L/R output — end of signal chain
Subgroup, // Named subgroup (Drums, Guitars, Vocals...)
Aux, // Aux send bus (monitor mix or FX send)
FxReturn, // Stereo return from a shared FX processor (reverb, delay)
};
/// An audio bus that accumulates contributions from multiple sources.
///
/// Buses form the mixing topology:
/// Channels → subgroups → master
/// Channels → aux sends → aux buses (monitor mixes)
/// Aux buses → FxReturn buses → subgroup or master
///
/// Key design decisions:
/// - Buses are flat accumulators: they sum incoming audio with gain
/// - Bus processing is minimal (volume, mute only)
/// - A bus can be fed INTO another bus via the routing graph
/// - All audio is floating-point, 32-bit
class MixerBus {
public:
MixerBus(int64_t id, MixerBusType type, const std::string& name, int channels = 2);
~MixerBus() = default;
/// Bus identity
int64_t id() const { return id_; }
MixerBusType type() const { return type_; }
const std::string& name() const { return name_; }
void setName(const std::string& name) { name_ = name; }
/// Channel count (1 = mono, 2 = stereo, N = multi-channel)
int channelCount() const { return channelCount_; }
/// --- Control surface (atomic for RT-safe writes) ---
/// Master volume in dB (-inf to +12.0)
float volume() const { return volume_.load(); }
void setVolume(float db);
/// Mute
bool mute() const { return mute_.load(); }
void setMute(bool mute);
/// --- Audio buffers ---
/// Allocate internal buffers. Must be called before processing.
void allocateBuffers(size_t maxFrames);
/// Get read/write pointer to internal buffer for a channel
float* buffer(int channel) {
if (channel >= 0 && channel < (int)buffers_.size())
return buffers_[channel].data();
return nullptr;
}
const float* buffer(int channel) const {
if (channel >= 0 && channel < (int)buffers_.size())
return buffers_[channel].data();
return nullptr;
}
/// Accumulate (sum) audio from a source into this bus with gain.
/// Performs: bus[ch][i] += source[ch][i] * gain for all channels
void accumulate(
const float* const* source,
uint32_t frames,
float gain,
int sourceChannels
);
/// Same as accumulate but for a single interleaved source buffer
void accumulateMono(
const float* source,
uint32_t frames,
float gain
);
/// Clear all bus buffers to zero (must be called at start of each cycle)
void clear();
/// Apply bus-level processing (volume, mute) to the internal mix.
/// Reads internal mix buffer, applies gain, writes back.
void process(uint32_t frames);
/// VU meter values after processing
float vuLeft() const { return vuLeft_.load(); }
float vuRight() const { return vuRight_.load(); }
/// Max frames this bus can handle
size_t maxFrames() const { return maxFrames_; }
private:
int64_t id_;
MixerBusType type_;
std::string name_;
int channelCount_;
std::atomic<float> volume_{0.0f}; // dB
std::atomic<bool> mute_{false};
// Internal accumulation buffers [channel][sample]
std::vector<std::vector<float>> buffers_;
// VU tracking
std::atomic<float> vuLeft_{-96.0f};
std::atomic<float> vuRight_{-96.0f};
size_t maxFrames_ = 512;
};
} // namespace pipedal
+280
View File
@@ -0,0 +1,280 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#include "pch.h"
#include "MixerChannelStrip.hpp"
#include "Lv2Effect.hpp"
#include "PiPedalMath.hpp"
#include <algorithm>
#include <cmath>
using namespace pipedal;
std::atomic<int64_t> MixerChannelStrip::nextInstanceId_{1};
MixerChannelStrip::MixerChannelStrip(int channelIndex)
: channelIndex_(channelIndex)
, instanceId_(nextInstanceId_++)
{
}
MixerChannelStrip::~MixerChannelStrip()
{
Unprepare();
}
void MixerChannelStrip::setVolume(float db)
{
volume_ = std::clamp(db, -96.0f, 12.0f);
}
void MixerChannelStrip::setPan(float pan)
{
pan_ = std::clamp(pan, -1.0f, 1.0f);
}
void MixerChannelStrip::setMute(bool mute)
{
mute_ = mute;
}
void MixerChannelStrip::setSolo(bool solo)
{
solo_ = solo;
}
void MixerChannelStrip::setAuxSend(int index, const AuxSendConfig& config)
{
if (index >= 0 && index < (int)auxSends_.size()) {
auxSends_[index] = config;
}
}
const AuxSendConfig& MixerChannelStrip::auxSend(int index) const
{
static const AuxSendConfig kDefault;
if (index >= 0 && index < (int)auxSends_.size()) {
return auxSends_[index];
}
return kDefault;
}
void MixerChannelStrip::resizeAuxSends(size_t count)
{
auxSends_.resize(count);
}
void MixerChannelStrip::setSampleRate(uint32_t sampleRate)
{
sampleRate_ = sampleRate;
hpfStates_.resize(2); // stereo HPF states
}
void MixerChannelStrip::setMaxBufferSize(size_t frames)
{
maxBufferSize_ = frames;
}
void MixerChannelStrip::prepareFx(IHost* pHost, Lv2PedalboardErrorList& errorList,
ExistingEffectMap* existingEffects)
{
// Create or re-create the Lv2Pedalboard for this channel's FX chain
if (!fxProcessor_) {
fxProcessor_ = std::make_unique<Lv2Pedalboard>();
}
// Allocate pre/post FX buffers (stereo, up to max buffer size)
preFxBuffers_.clear();
postFxBuffers_.clear();
for (int i = 0; i < 2; ++i) {
preFxBuffers_.emplace_back(maxBufferSize_, 0.0f);
postFxBuffers_.emplace_back(maxBufferSize_, 0.0f);
}
// Prepare the FX processor with this channel's pedalboard
fxProcessor_->Prepare(pHost, fxChain_, errorList, existingEffects);
fxProcessor_->Activate();
}
float MixerChannelStrip::effectiveAuxLevel(int auxIndex, bool anySoloActive) const
{
if (auxIndex < 0 || auxIndex >= (int)auxSends_.size()) return -96.0f;
const auto& send = auxSends_[auxIndex];
if (!send.isActive()) return -96.0f;
// Solo overrides: if any solo is active, only soloed channels are audible
if (anySoloActive && !solo_) return -96.0f;
if (mute_) return -96.0f;
return send.level;
}
void MixerChannelStrip::applyPan(float& leftGain, float& rightGain) const
{
float pan = pan_;
// Constant-power pan law: -3dB at center
// sin/cos distribution: L = cos(pan * PI/4), R = sin(pan * PI/4)
// Normalized so center = -3dB each
float angle = (pan * 0.5f + 0.5f) * (M_PI * 0.5f); // map -1..1 to 0..PI/2
leftGain = std::cos(angle);
rightGain = std::sin(angle);
// Compensate for equal-power pan: center should sum to unity
// Already handled by sin/cos distribution
}
void MixerChannelStrip::applyHpf(float* buffer, uint32_t frames, HpfState& state)
{
if (!hpEnabled_) return;
// Simple 1st-order IIR HPF: y[n] = 0.5 * (x[n] - x[n-1] + y[n-1])
// Cutoff ~ 80Hz at 48kHz. For sharper roll-off, use biquad.
// This is intentionally simple for real-time safety.
float fc = hpFrequency_ / sampleRate_;
float alpha = fc / (fc + 0.5f); // approximation: R = 1/(2*PI*fc)
for (uint32_t i = 0; i < frames; ++i) {
float x = buffer[i];
float y = alpha * (state.y1 + x - state.x1);
state.x1 = x;
state.y1 = y;
buffer[i] = y;
}
}
void MixerChannelStrip::process(
const float* const* inputBuffers,
size_t inputChannels,
float* const* outputBuffers,
size_t outputChannels,
uint32_t frames)
{
// Clamp frames to allocated buffer size
frames = std::min(frames, (uint32_t)maxBufferSize_);
// Step 1: Copy input to pre-FX buffers and apply HPF
for (size_t ch = 0; ch < std::min(inputChannels, (size_t)2); ++ch) {
if (ch < preFxBuffers_.size() && inputBuffers[ch]) {
std::copy(inputBuffers[ch], inputBuffers[ch] + frames,
preFxBuffers_[ch].begin());
applyHpf(preFxBuffers_[ch].data(), frames,
ch < hpfStates_.size() ? hpfStates_[ch] : hpfStates_[0]);
}
}
// Step 2: Run the FX chain (processes preFxBuffers_ -> postFxBuffers_)
if (fxProcessor_) {
// Build float* arrays for Lv2Pedalboard::Run
float* fxInputs[2];
float* fxOutputs[2];
for (int i = 0; i < 2; ++i) {
fxInputs[i] = i < (int)preFxBuffers_.size() ? preFxBuffers_[i].data() : nullptr;
fxOutputs[i] = i < (int)postFxBuffers_.size() ? postFxBuffers_[i].data() : nullptr;
}
// Run the FX chain (Lv2Pedalboard manages its internal routing)
fxProcessor_->Run(
(float**)fxInputs,
(float**)fxOutputs,
frames,
nullptr // no realtime ring buffer writer for now
);
} else {
// No FX chain — passthrough pre to post
for (size_t ch = 0; ch < std::min(inputChannels, (size_t)2); ++ch) {
if (ch < postFxBuffers_.size() && ch < preFxBuffers_.size()) {
std::copy(preFxBuffers_[ch].begin(),
preFxBuffers_[ch].begin() + frames,
postFxBuffers_[ch].begin());
}
}
}
// Step 3: Apply volume, pan, and mute/solo to create output
bool isMuted = mute_.load();
bool isSoloed = solo_.load();
// Calculate gain from volume dB
float volumeGain = isMuted ? 0.0f : std::pow(10.0f, volume_.load() / 20.0f);
// Calculate pan gains
float leftGain = 1.0f, rightGain = 1.0f;
applyPan(leftGain, rightGain);
// Apply to output buffers
for (size_t outCh = 0; outCh < std::min(outputChannels, (size_t)2); ++outCh) {
if (!outputBuffers[outCh]) continue;
float* dst = outputBuffers[outCh];
const float* src = (outCh < postFxBuffers_.size())
? postFxBuffers_[outCh].data()
: (postFxBuffers_.empty() ? nullptr : postFxBuffers_[0].data());
if (!src) {
std::fill(dst, dst + frames, 0.0f);
continue;
}
float panGain = (outCh == 0) ? leftGain : rightGain;
float finalGain = volumeGain * panGain;
if (finalGain < 0.001f) {
std::fill(dst, dst + frames, 0.0f);
} else if (std::abs(finalGain - 1.0f) < 0.001f) {
std::copy(src, src + frames, dst);
} else {
for (uint32_t i = 0; i < frames; ++i) {
dst[i] = src[i] * finalGain;
}
}
}
// Step 4: Update VU meters (peak, with 300ms decay)
for (size_t ch = 0; ch < std::min(outputChannels, (size_t)2); ++ch) {
if (ch >= postFxBuffers_.size()) break;
float peak = 0.0f;
const float* buf = postFxBuffers_[ch].data();
for (uint32_t i = 0; i < frames; ++i) {
float absVal = std::abs(buf[i]);
if (absVal > peak) peak = absVal;
}
float peakDb = (peak > 0.00001f) ? 20.0f * std::log10(peak) : -96.0f;
// Decay: 300ms time constant
float& vu = (ch == 0) ? vuLeft_ : vuRight_;
if (peakDb > vu) {
vu = peakDb; // Instant attack
} else {
// Decay at ~300ms: releaseRate = exp(-1 / (0.3 * sampleRate / frames))
static const float releaseRate = 0.95f;
vu = vu * releaseRate + peakDb * (1.0f - releaseRate);
}
}
}
void MixerChannelStrip::Activate()
{
if (fxProcessor_) {
fxProcessor_->Activate();
}
}
void MixerChannelStrip::Deactivate()
{
if (fxProcessor_) {
fxProcessor_->Deactivate();
}
}
void MixerChannelStrip::Unprepare()
{
if (fxProcessor_) {
fxProcessor_->Deactivate();
fxProcessor_.reset();
}
preFxBuffers_.clear();
postFxBuffers_.clear();
}
+229
View File
@@ -0,0 +1,229 @@
// Copyright (c) 2026 Ourpad Network
//
// 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 "Pedalboard.hpp"
#include "Lv2Pedalboard.hpp"
#include "IEffect.hpp"
#include "BufferPool.hpp"
#include <memory>
#include <vector>
#include <atomic>
#include <string>
namespace pipedal {
/// Channel type classification for mixer channel strips.
enum class MixerChannelType {
Instrument, // guitar, bass, keys — expects NAM/guitar amp modeling
Mic, // vocal mic — expects compressor, EQ, reverb chain
Line, // line-level input (backing tracks, synths, drum machines)
AuxReturn, // return from external FX processor
};
/// Configuration for a single aux send on a channel strip.
struct AuxSendConfig {
float level = -96.0f; // dB, -96 ≈ -inf (effectively off)
bool preFader = false; // true = pre-fader (monitor send), false = post-fader (FX send)
int64_t targetBusId = -1; // bus ID this sends to
bool isActive() const { return level > -90.0f && targetBusId >= 0; }
};
/// Per-channel high-pass filter configuration.
struct HpfConfig {
bool enabled = false;
float frequency = 80.0f; // Hz
// Filter state for simple biquad — sized for max buffer
// Allocated at prepare time
};
/// A single channel strip in the mixer.
///
/// Each MixerChannelStrip wraps:
/// - A mini-pedalboard (FX chain using existing Lv2Pedalboard)
/// - Volume fader, pan pot, mute, solo
/// - High-pass filter on input
/// - Aux sends (pre/post fader)
/// - VU metering
///
/// The channel operates in the real-time audio thread.
/// Control changes are made from the non-realtime thread via atomic snapshots.
class MixerChannelStrip {
public:
MixerChannelStrip(int channelIndex);
~MixerChannelStrip();
// Disable copy
MixerChannelStrip(const MixerChannelStrip&) = delete;
MixerChannelStrip& operator=(const MixerChannelStrip&) = delete;
/// Channel identity
int channelIndex() const { return channelIndex_; }
int64_t instanceId() const { return instanceId_; }
/// --- Control surface (thread-safe via atomics for simple values) ---
/// Volume in dB (-inf to +12.0)
float volume() const { return volume_; }
void setVolume(float db);
/// Pan: -1.0 (full left) to +1.0 (full right). 0.0 = center.
float pan() const { return pan_; }
void setPan(float pan);
/// Mute
bool mute() const { return mute_; }
void setMute(bool mute);
/// Solo — overrides mute for monitoring
bool solo() const { return solo_; }
void setSolo(bool solo);
/// Channel type for UI classification
MixerChannelType channelType() const { return channelType_; }
void setChannelType(MixerChannelType type) { channelType_ = type; }
/// User-assignable label
const std::string& label() const { return label_; }
void setLabel(const std::string& label) { label_ = label; }
/// --- Input processing ---
/// High-pass filter
bool hpEnabled() const { return hpEnabled_; }
void setHpEnabled(bool enabled) { hpEnabled_ = enabled; }
float hpFrequency() const { return hpFrequency_; }
void setHpFrequency(float freq) { hpFrequency_ = freq; }
/// --- FX Chain ---
/// Access the channel's pedalboard for plugin management
Pedalboard& fxChain() { return fxChain_; }
const Pedalboard& fxChain() const { return fxChain_; }
/// Get the real-time processor for this channel's FX chain
Lv2Pedalboard* fxProcessor() { return fxProcessor_.get(); }
/// Prepare the FX chain for processing
void prepareFx(IHost* pHost, Lv2PedalboardErrorList& errorList,
ExistingEffectMap* existingEffects = nullptr);
/// --- Aux Sends ---
void setAuxSend(int index, const AuxSendConfig& config);
const AuxSendConfig& auxSend(int index) const;
size_t auxSendCount() const { return auxSends_.size(); }
void resizeAuxSends(size_t count);
/// --- Audio Processing (real-time thread) ---
/// Set sample rate and max buffer size
void setSampleRate(uint32_t sampleRate);
void setMaxBufferSize(size_t frames);
/// Process one audio block through this channel strip.
/// Reads from input, runs HPF → FX chain, applies volume/pan.
/// Output goes to provided output buffer(s).
/// Returns the number of processed samples.
void process(
const float* const* inputBuffers,
size_t inputChannels,
float* const* outputBuffers,
size_t outputChannels,
uint32_t frames
);
/// Get post-FX, pre-fader audio for aux send calculation
const float* postFxBuffer(int channel) const {
if (channel < (int)postFxBuffers_.size()) return postFxBuffers_[channel].data();
return nullptr;
}
/// Get pre-FX audio for pre-fader aux sends
const float* preFxBuffer(int channel) const {
if (channel < (int)preFxBuffers_.size()) return preFxBuffers_[channel].data();
return nullptr;
}
/// Calcuate aux send level considering pre/post fader and mute
float effectiveAuxLevel(int auxIndex, bool anySoloActive) const;
/// VU meter values (peak, not RMS — computed during process)
float vuLeft() const { return vuLeft_; }
float vuRight() const { return vuRight_; }
/// --- Lifecycle ---
void Activate();
void Deactivate();
void Unprepare();
private:
int channelIndex_;
int64_t instanceId_;
static std::atomic<int64_t> nextInstanceId_;
// Control values (atomic for RT-safe reads from control thread)
std::atomic<float> volume_{-96.0f}; // dB, -inf default
std::atomic<float> pan_{0.0f};
std::atomic<bool> mute_{false};
std::atomic<bool> solo_{false};
std::atomic<bool> hpEnabled_{false};
std::atomic<float> hpFrequency_{80.0f};
MixerChannelType channelType_ = MixerChannelType::Instrument;
std::string label_;
// FX chain
Pedalboard fxChain_;
std::unique_ptr<Lv2Pedalboard> fxProcessor_;
// Aux sends
std::vector<AuxSendConfig> auxSends_;
// Audio buffers (allocated at prepare time)
std::vector<std::vector<float>> preFxBuffers_; // Before FX chain (for pre-fader sends)
std::vector<std::vector<float>> postFxBuffers_; // After FX chain, before fader
BufferPool bufferPool_;
// Sample rate / buffer size
uint32_t sampleRate_ = 48000;
size_t maxBufferSize_ = 512;
// VU tracking
float vuLeft_ = -96.0f;
float vuRight_ = -96.0f;
// Simple 1-pole HPF state (per channel)
struct HpfState {
float x1 = 0.0f, y1 = 0.0f;
};
std::vector<HpfState> hpfStates_;
// Apply pan law: constant power (-3dB center)
void applyPan(float& leftGain, float& rightGain) const;
// Apply HPF biquad to a buffer
void applyHpf(float* buffer, uint32_t frames, HpfState& state);
};
} // namespace pipedal
+656
View File
@@ -0,0 +1,656 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#include "pch.h"
#include "MixerEngine.hpp"
#include "MixerChannelStrip.hpp"
#include "MixerBus.hpp"
#include "Lv2Pedalboard.hpp"
#include "IHost.hpp"
#include "MidiEvent.hpp"
#include <algorithm>
#include <cmath>
using namespace pipedal;
std::atomic<int64_t> MixerEngine::nextBusId_{1};
MixerEngine::MixerEngine()
{
// Create the master bus by default
int64_t masterId = nextBusId_++;
auto master = std::make_unique<MixerBus>(masterId, MixerBusType::Master, "Master", 2);
masterBus_ = master.get();
buses_[masterId] = std::move(master);
}
MixerEngine::~MixerEngine()
{
Deactivate();
}
void MixerEngine::setSampleRate(uint32_t sampleRate)
{
sampleRate_ = sampleRate;
}
void MixerEngine::setMaxBufferSize(size_t frames)
{
maxBufferSize_ = frames;
}
// --- Channel Management ---
MixerChannelStrip* MixerEngine::addChannel(int physicalInputIndex)
{
auto channel = std::make_unique<MixerChannelStrip>(physicalInputIndex);
channel->setSampleRate(sampleRate_);
channel->setMaxBufferSize(maxBufferSize_);
channel->setLabel("Channel " + std::to_string(physicalInputIndex + 1));
// Default: route channel directly to master
auto* ptr = channel.get();
channels_.push_back(std::move(channel));
// Create default route: this channel → master bus at unity
MixerRouteEntry route;
route.sourceType = MixerRouteEntry::SourceChannel;
route.sourceId = ptr->instanceId();
route.targetBusId = masterBus_->id();
route.level = 0.0f; // unity
routes_.push_back(route);
return ptr;
}
void MixerEngine::removeChannel(int channelIndex)
{
if (channelIndex < 0 || channelIndex >= (int)channels_.size()) return;
auto* channel = channels_[channelIndex].get();
int64_t instanceId = channel->instanceId();
// Remove all routes referencing this channel
routes_.erase(
std::remove_if(routes_.begin(), routes_.end(),
[instanceId](const MixerRouteEntry& r) {
return r.sourceType == MixerRouteEntry::SourceChannel &&
r.sourceId == instanceId;
}),
routes_.end()
);
// Unprepare the channel
channel->Unprepare();
channels_.erase(channels_.begin() + channelIndex);
}
MixerChannelStrip* MixerEngine::getChannel(int channelIndex)
{
if (channelIndex >= 0 && channelIndex < (int)channels_.size())
return channels_[channelIndex].get();
return nullptr;
}
const MixerChannelStrip* MixerEngine::getChannel(int channelIndex) const
{
if (channelIndex >= 0 && channelIndex < (int)channels_.size())
return channels_[channelIndex].get();
return nullptr;
}
// --- Bus Management ---
int64_t MixerEngine::addBus(MixerBusType type, const std::string& name, int channels)
{
int64_t id = nextBusId_++;
auto bus = std::make_unique<MixerBus>(id, type, name, channels);
bus->allocateBuffers(maxBufferSize_);
buses_[id] = std::move(bus);
return id;
}
void MixerEngine::removeBus(int64_t busId)
{
if (busId == masterBus_->id()) return; // Can't remove master
// Remove all routes targeting this bus
routes_.erase(
std::remove_if(routes_.begin(), routes_.end(),
[busId](const MixerRouteEntry& r) {
return r.targetBusId == busId;
}),
routes_.end()
);
buses_.erase(busId);
}
MixerBus* MixerEngine::getBus(int64_t busId)
{
auto it = buses_.find(busId);
return (it != buses_.end()) ? it->second.get() : nullptr;
}
const MixerBus* MixerEngine::getBus(int64_t busId) const
{
auto it = buses_.find(busId);
return (it != buses_.end()) ? it->second.get() : nullptr;
}
std::vector<int64_t> MixerEngine::busIds() const
{
std::vector<int64_t> ids;
ids.reserve(buses_.size());
for (const auto& [id, _] : buses_) {
ids.push_back(id);
}
return ids;
}
// --- Routing ---
void MixerEngine::routeChannelToBus(int channelIndex, int64_t busId, float levelDb)
{
if (channelIndex < 0 || channelIndex >= (int)channels_.size()) return;
if (!getBus(busId)) return;
auto* channel = channels_[channelIndex].get();
// Check if route already exists — update level
for (auto& route : routes_) {
if (route.sourceType == MixerRouteEntry::SourceChannel &&
route.sourceId == channel->instanceId() &&
route.targetBusId == busId) {
route.level = levelDb;
return;
}
}
// Add new route
MixerRouteEntry route;
route.sourceType = MixerRouteEntry::SourceChannel;
route.sourceId = channel->instanceId();
route.targetBusId = busId;
route.level = levelDb;
routes_.push_back(route);
}
void MixerEngine::routeBusToBus(int64_t sourceBusId, int64_t targetBusId, float levelDb)
{
if (!getBus(sourceBusId) || !getBus(targetBusId)) return;
for (auto& route : routes_) {
if (route.sourceType == MixerRouteEntry::SourceBus &&
route.sourceId == sourceBusId &&
route.targetBusId == targetBusId) {
route.level = levelDb;
return;
}
}
MixerRouteEntry route;
route.sourceType = MixerRouteEntry::SourceBus;
route.sourceId = sourceBusId;
route.targetBusId = targetBusId;
route.level = levelDb;
routes_.push_back(route);
}
void MixerEngine::removeRoute(int64_t sourceId, int64_t targetBusId)
{
routes_.erase(
std::remove_if(routes_.begin(), routes_.end(),
[sourceId, targetBusId](const MixerRouteEntry& r) {
return r.sourceId == sourceId && r.targetBusId == targetBusId;
}),
routes_.end()
);
}
void MixerEngine::clearRoutes()
{
routes_.clear();
}
// --- Lifecycle ---
void MixerEngine::Prepare(IHost* pHost, Lv2PedalboardErrorList& errorList)
{
pHost_ = pHost;
// Allocate bus buffers
for (auto& [_, bus] : buses_) {
bus->allocateBuffers(maxBufferSize_);
}
// Allocate per-channel output buffers (for routing accumulation)
channelOutputBuffers_.resize(std::max((size_t)1, channels_.size()));
for (auto& buf : channelOutputBuffers_) {
buf.resize(maxBufferSize_ * 2, 0.0f); // stereo output per channel
}
// Prepare each channel's FX chain
for (auto& channel : channels_) {
channel->setSampleRate(sampleRate_);
channel->setMaxBufferSize(maxBufferSize_);
channel->prepareFx(pHost, errorList, nullptr);
}
}
void MixerEngine::Activate()
{
for (auto& channel : channels_) {
channel->Activate();
}
}
void MixerEngine::Deactivate()
{
for (auto& channel : channels_) {
channel->Deactivate();
}
}
// --- Solo ---
bool MixerEngine::anySoloActive() const
{
for (const auto& channel : channels_) {
if (channel->solo()) return true;
}
return false;
}
// --- Audio Processing ---
std::vector<MixerRouteEntry*> MixerEngine::findRoutesForSource(int64_t sourceId)
{
std::vector<MixerRouteEntry*> result;
for (auto& route : routes_) {
if (route.sourceId == sourceId) {
result.push_back(&route);
}
}
return result;
}
void MixerEngine::routeChannelOutput(
MixerChannelStrip* channel,
float** channelOutput,
uint32_t frames)
{
bool soloActive = anySoloActive();
// Find all routes for this channel
int64_t channelId = channel->instanceId();
auto channelRoutes = findRoutesForSource(channelId);
for (auto* route : channelRoutes) {
MixerBus* targetBus = getBus(route->targetBusId);
if (!targetBus) continue;
float levelLinear = std::pow(10.0f, route->level / 20.0f);
// Check aux sends if this is an aux bus
// For standard bus routing, just accumulate
targetBus->accumulate(
(const float* const*)channelOutput,
frames,
levelLinear,
2 // channelOutput is always stereo
);
}
// Process aux sends
size_t numAuxSends = channel->auxSendCount();
for (size_t auxIdx = 0; auxIdx < numAuxSends; ++auxIdx) {
float effectiveLevel = channel->effectiveAuxLevel(auxIdx, soloActive);
if (effectiveLevel < -90.0f) continue;
const auto& sendConfig = channel->auxSend(auxIdx);
MixerBus* auxBus = getBus(sendConfig.targetBusId);
if (!auxBus) continue;
float sendGain = std::pow(10.0f, effectiveLevel / 20.0f);
if (sendConfig.preFader) {
// Pre-fader: use the pre-FX buffer
// This means we need access to the pre-fader buffer from the channel
const float* preFx0 = channel->preFxBuffer(0);
const float* preFx1 = channel->preFxBuffer(1);
if (preFx0) {
const float* preFx[2] = { preFx0, preFx1 };
auxBus->accumulate(preFx, frames, sendGain, 2);
}
} else {
// Post-fader: use the same output that goes to buses
auxBus->accumulate(
(const float* const*)channelOutput,
frames,
sendGain,
2
);
}
}
}
void MixerEngine::processBusRouting(uint32_t frames)
{
// Process bus-to-bus routes
// This is simple: for each bus route, accumulate source bus output to target bus
for (auto& route : routes_) {
if (route.sourceType != MixerRouteEntry::SourceBus) continue;
MixerBus* sourceBus = getBus(route.sourceId);
MixerBus* targetBus = getBus(route.targetBusId);
if (!sourceBus || !targetBus) continue;
// Build float* array from source bus
int nChannels = sourceBus->channelCount();
std::vector<const float*> srcPtrs(nChannels);
for (int ch = 0; ch < nChannels; ++ch) {
srcPtrs[ch] = sourceBus->buffer(ch);
}
float levelLinear = std::pow(10.0f, route.level / 20.0f);
targetBus->accumulate(srcPtrs.data(), frames, levelLinear, nChannels);
}
}
void MixerEngine::process(
float** deviceInputs,
uint32_t inputChannels,
float** deviceOutputs,
uint32_t outputChannels,
uint32_t frames)
{
// Clamp
frames = std::min(frames, (uint32_t)maxBufferSize_);
// Step 1: Clear all bus buffers
for (auto& [_, bus] : buses_) {
bus->clear();
}
// Step 2: Process each channel
size_t numChannels = channels_.size();
// Determine channel pairing mode:
// For 1-2 total input channels: stereo pairing per strip (backward compatible)
// For 3+ input channels: each strip is mono (one input channel → stereo output with pan)
bool stereoPairing = (inputChannels <= 2);
for (size_t ch = 0; ch < numChannels; ++ch) {
auto* channel = channels_[ch].get();
// Build input buffer pointers for this channel
float* channelInputs[2] = { nullptr, nullptr };
if (stereoPairing) {
// Stereo pairing: channel 0 ← inputs[0,1], channel 1 ← inputs[2,3], etc.
uint32_t baseInput = (uint32_t)(ch * 2);
if (baseInput < inputChannels) {
channelInputs[0] = deviceInputs[baseInput];
if (baseInput + 1 < inputChannels) {
channelInputs[1] = deviceInputs[baseInput + 1];
}
}
} else {
// Mono per strip: each channel gets exactly one input
if (ch < inputChannels) {
channelInputs[0] = deviceInputs[ch];
// Second input stays nullptr → mono processing
}
}
// Build output buffer (stereo, from our per-channel scratch buffers)
float* channelOutputs[2] = { nullptr, nullptr };
if (ch < channelOutputBuffers_.size()) {
channelOutputs[0] = channelOutputBuffers_[ch].data();
channelOutputs[1] = channelOutputBuffers_[ch].data() + maxBufferSize_;
}
// Determine the actual number of input channels for this strip
size_t stripInputChannels = 0;
if (channelInputs[0] != nullptr) stripInputChannels = 1;
if (channelInputs[1] != nullptr) stripInputChannels = 2;
// Process the channel strip
channel->process(
(const float* const*)channelInputs,
stripInputChannels,
channelOutputs,
2,
frames
);
// Route channel output to buses
routeChannelOutput(channel, channelOutputs, frames);
}
// Step 3: Process bus-to-bus routing
processBusRouting(frames);
// Step 4: Process each bus (apply volume, compute VU)
for (auto& [_, bus] : buses_) {
bus->process(frames);
}
// Step 5: Write buses to physical outputs according to output routing
if (outputRoutes_.empty()) {
// Legacy fallback: write master bus to device outputs 1:1
if (masterBus_) {
for (uint32_t outCh = 0; outCh < outputChannels; ++outCh) {
if (deviceOutputs[outCh] == nullptr) continue;
const float* src = masterBus_->buffer(outCh);
if (src) {
std::copy(src, src + frames, deviceOutputs[outCh]);
} else if (outCh == 1) {
const float* srcL = masterBus_->buffer(0);
if (srcL) {
std::copy(srcL, srcL + frames, deviceOutputs[outCh]);
}
}
}
}
} else {
// Use configured output routes
for (const auto& route : outputRoutes_) {
MixerBus* sourceBus = getBus(route.sourceBusId);
if (!sourceBus) continue;
for (int ch = 0; ch < route.channels; ++ch) {
uint32_t targetCh = (uint32_t)(route.targetStartChannel + ch);
if (targetCh >= outputChannels) break;
if (deviceOutputs[targetCh] == nullptr) continue;
const float* src = sourceBus->buffer(route.sourceStartChannel + ch);
if (src) {
std::copy(src, src + frames, deviceOutputs[targetCh]);
}
}
}
}
}
// --- MIDI Control Surface Mapping ---
bool MixerEngine::processMidiEvent(const MidiEvent& event)
{
midiMapper_.setMixerEngine(this);
return midiMapper_.processEvent(event);
}
// --- State Serialization ---
MixerEngine::MixerSnapshot MixerEngine::captureSnapshot() const
{
MixerSnapshot snap;
for (const auto& channel : channels_) {
MixerSnapshot::ChannelState cs;
cs.channelIndex = channel->channelIndex();
cs.volume = channel->volume();
cs.pan = channel->pan();
cs.mute = channel->mute();
cs.solo = channel->solo();
cs.channelType = channel->channelType();
cs.label = channel->label();
cs.hpEnabled = channel->hpEnabled();
cs.hpFrequency = channel->hpFrequency();
cs.vuLeft = channel->vuLeft();
cs.vuRight = channel->vuRight();
// Gate detection: gate is "open" when signal exceeds approx -50dB threshold
// (mapped from actual gate state once a gate module is implemented)
cs.gateOpen = (cs.vuLeft > -50.0f || cs.vuRight > -50.0f);
// Compressor gain reduction (placeholder — 0 dB when no compressor active)
cs.compressorReduction = 0.0f;
for (size_t i = 0; i < channel->auxSendCount(); ++i) {
cs.auxSendLevels.push_back(channel->auxSend(i).level);
}
snap.channels.push_back(cs);
}
for (const auto& [id, bus] : buses_) {
MixerSnapshot::BusState bs;
bs.id = id;
bs.name = bus->name();
bs.type = bus->type();
bs.volume = bus->volume();
bs.mute = bus->mute();
bs.vuLeft = bus->vuLeft();
bs.vuRight = bus->vuRight();
snap.buses.push_back(bs);
}
snap.routes = routes_;
return snap;
}
// --- Output Routing ---
void MixerEngine::setOutputRoutes(const std::vector<MixerOutputRoute>& routes)
{
outputRoutes_ = routes;
}
void MixerEngine::addOutputRoute(int64_t busId, int sourceStartChannel, int targetStartChannel, int channels)
{
// Remove any existing route that conflicts with the target
outputRoutes_.erase(
std::remove_if(outputRoutes_.begin(), outputRoutes_.end(),
[busId, targetStartChannel](const MixerOutputRoute& r) {
return r.sourceBusId == busId &&
r.targetStartChannel == targetStartChannel;
}),
outputRoutes_.end()
);
MixerOutputRoute route;
route.sourceBusId = busId;
route.sourceStartChannel = sourceStartChannel;
route.targetStartChannel = targetStartChannel;
route.channels = channels;
outputRoutes_.push_back(route);
}
void MixerEngine::removeOutputRoutes(int64_t busId)
{
outputRoutes_.erase(
std::remove_if(outputRoutes_.begin(), outputRoutes_.end(),
[busId](const MixerOutputRoute& r) { return r.sourceBusId == busId; }),
outputRoutes_.end()
);
}
std::vector<MixerEngine::MixerOutputRoute> MixerEngine::findOutputRoutesForBus(int64_t busId) const
{
std::vector<MixerOutputRoute> result;
for (const auto& route : outputRoutes_) {
if (route.sourceBusId == busId) {
result.push_back(route);
}
}
return result;
}
// --- Auto channel creation ---
void MixerEngine::autoCreateChannels(uint32_t inputChannelCount, uint32_t outputChannelCount)
{
// Store the physical I/O channel counts for later queries
physicalInputCount_ = inputChannelCount;
physicalOutputCount_ = outputChannelCount;
// Remove existing channels if any
for (int i = (int)channels_.size() - 1; i >= 0; --i) {
removeChannel(i);
}
// Create one channel strip per input
for (uint32_t i = 0; i < inputChannelCount; ++i) {
addChannel((int)i);
auto* ch = getChannel((int)i);
if (ch) {
char label[32];
snprintf(label, sizeof(label), "Input %u", i + 1);
ch->setLabel(label);
}
}
// Set default output routes:
// Always route master bus to physical 1-2 (the main monitor output)
// For multi-channel setups, individual channel routing is configured via the output routing UI.
outputRoutes_.clear();
if (!masterBus_) return;
MixerOutputRoute masterRoute;
masterRoute.sourceBusId = masterBus_->id();
masterRoute.sourceStartChannel = 0;
masterRoute.targetStartChannel = 0;
masterRoute.channels = 2;
outputRoutes_.push_back(masterRoute);
}
void MixerEngine::applySnapshot(const MixerSnapshot& snapshot)
{
// Apply channel states
for (const auto& cs : snapshot.channels) {
auto* channel = getChannel(cs.channelIndex);
if (!channel) continue;
channel->setVolume(cs.volume);
channel->setPan(cs.pan);
channel->setMute(cs.mute);
channel->setSolo(cs.solo);
channel->setChannelType(cs.channelType);
channel->setLabel(cs.label);
channel->setHpEnabled(cs.hpEnabled);
channel->setHpFrequency(cs.hpFrequency);
for (size_t i = 0; i < cs.auxSendLevels.size() && i < channel->auxSendCount(); ++i) {
auto config = channel->auxSend(i);
config.level = cs.auxSendLevels[i];
channel->setAuxSend(i, config);
}
}
// Apply bus states
for (const auto& bs : snapshot.buses) {
auto* bus = getBus(bs.id);
if (!bus) continue;
bus->setName(bs.name);
bus->setVolume(bs.volume);
bus->setMute(bs.mute);
}
// Replace routes
routes_ = snapshot.routes;
}
+285
View File
@@ -0,0 +1,285 @@
// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#pragma once
#include <memory>
#include <vector>
#include <map>
#include <atomic>
#include <cstdint>
#include <functional>
#include "MixerChannelStrip.hpp"
#include "MixerBus.hpp"
#include "MidiMapper.hpp"
namespace pipedal {
class MixerChannelStrip;
class MixerBus;
class Lv2PedalboardErrorList;
class IHost;
/// Routing entry: a source (channel or bus) feeds a target bus with a level.
struct MixerRouteEntry {
enum SourceType {
SourceChannel,
SourceBus
};
SourceType sourceType;
int64_t sourceId; // channel instanceId or bus ID
int64_t targetBusId; // the bus being fed into
float level = 0.0f; // dB
};
/// The MixerEngine is the heart of the band-in-a-box digital mixer.
///
/// It owns and manages:
/// - N channel strips (MixerChannelStrip), one per physical/logical input
/// - M buses (MixerBus), including master, subgroups, aux sends
/// - A routing graph connecting channels to buses and buses to buses
///
/// Processing order per audio cycle:
/// 1. Clear all bus buffers
/// 2. For each channel: process FX chain → apply volume/pan → accumulate to routed buses
/// 3. For each aux send: calculate send level, accumulate to aux buses
/// 4. Route buses to buses according to routing matrix
/// 5. Process each bus (apply volume, compute VU)
/// 6. Master bus outputs are the final mix
///
/// All control methods are thread-safe for use from the non-RT thread.
/// The process() method runs in the RT audio thread.
class MixerEngine {
public:
MixerEngine();
~MixerEngine();
// Disable copy
MixerEngine(const MixerEngine&) = delete;
MixerEngine& operator=(const MixerEngine&) = delete;
/// --- Configuration ---
/// Set sample rate and max buffer size before preparation
void setSampleRate(uint32_t sampleRate);
void setMaxBufferSize(size_t frames);
/// --- Channel Management ---
/// Add a new channel strip for the given physical input index.
/// Returns a pointer to the new channel (valid until removed).
MixerChannelStrip* addChannel(int physicalInputIndex);
/// Auto-create channels based on the number of detected input channels.
/// Removes existing channels and creates one strip per input.
/// Also sets up default output routes.
/// @param inputChannelCount Number of physical input channels detected
/// @param outputChannelCount Number of physical output channels detected
void autoCreateChannels(uint32_t inputChannelCount, uint32_t outputChannelCount = 2);
/// Remove a channel by its channel index.
void removeChannel(int channelIndex);
/// Get a channel by index. Returns nullptr if not found.
MixerChannelStrip* getChannel(int channelIndex);
const MixerChannelStrip* getChannel(int channelIndex) const;
/// Number of channels currently in the mixer.
size_t channelCount() const { return channels_.size(); }
/// --- Bus Management ---
/// Add a new bus and return its ID.
int64_t addBus(MixerBusType type, const std::string& name, int channels = 2);
/// Remove a bus by ID.
void removeBus(int64_t busId);
/// Get a bus by ID. Returns nullptr if not found.
MixerBus* getBus(int64_t busId);
const MixerBus* getBus(int64_t busId) const;
/// Access the master bus (always present).
MixerBus* masterBus() { return masterBus_; }
const MixerBus* masterBus() const { return masterBus_; }
/// Get all bus IDs (for iteration).
std::vector<int64_t> busIds() const;
/// --- Routing ---
/// Route a channel to a bus with a given level in dB.
void routeChannelToBus(int channelIndex, int64_t busId, float levelDb = 0.0f);
/// Route a bus to another bus (e.g., subgroup to master).
void routeBusToBus(int64_t sourceBusId, int64_t targetBusId, float levelDb = 0.0f);
/// Remove a route.
void removeRoute(int64_t sourceId, int64_t targetBusId);
/// Clear all routes.
void clearRoutes();
/// --- Output Routing ---
/// Describes a mapping from a mixer bus to physical output channels.
/// Multiple routes can be active simultaneously (e.g. Master→1-2, Aux1→3-4).
struct MixerOutputRoute {
int64_t sourceBusId; // Bus to route from
int sourceStartChannel; // Starting channel on the bus (0=L, 1=R)
int targetStartChannel; // Starting physical output channel
int channels; // Number of consecutive channels to route (1 or 2 typically)
};
/// Get the current output routing table (bus → physical output channel mapping).
const std::vector<MixerOutputRoute>& outputRoutes() const { return outputRoutes_; }
/// Set the entire output routing table.
void setOutputRoutes(const std::vector<MixerOutputRoute>& routes);
/// Add a single output route.
void addOutputRoute(int64_t busId, int sourceStartChannel, int targetStartChannel, int channels);
/// Remove all output routes for a given bus.
void removeOutputRoutes(int64_t busId);
/// Get all routes for a given bus.
std::vector<MixerOutputRoute> findOutputRoutesForBus(int64_t busId) const;
/// Get all current routes.
const std::vector<MixerRouteEntry>& routes() const { return routes_; }
/// --- Physical I/O Info ---
/// Number of physical input channels detected on the audio device.
uint32_t physicalInputCount() const { return physicalInputCount_; }
/// Number of physical output channels detected on the audio device.
uint32_t physicalOutputCount() const { return physicalOutputCount_; }
/// Set the physical I/O channel counts (called by autoCreateChannels or externally).
void setPhysicalChannelCounts(uint32_t inputs, uint32_t outputs) {
physicalInputCount_ = inputs;
physicalOutputCount_ = outputs;
}
/// --- Audio Processing (real-time thread) ---
/// Prepare all channels and allocate buffers.
void Prepare(IHost* pHost, Lv2PedalboardErrorList& errorList);
/// Activate all channels.
void Activate();
/// Deactivate all channels.
void Deactivate();
/// Process one full mixer cycle.
/// deviceInputs/outputs are the raw audio interface buffers.
/// The mixer reads from inputs, processes through channels → buses, writes to outputs.
void process(
float** deviceInputs,
uint32_t inputChannels,
float** deviceOutputs,
uint32_t outputChannels,
uint32_t frames
);
/// --- Solo Management ---
/// True if any channel has solo engaged.
bool anySoloActive() const;
/// --- MIDI Control Surface Mapping ---
/// Access the MIDI mapper for CC control surface mapping.
MidiMapper& midiMapper() { return midiMapper_; }
const MidiMapper& midiMapper() const { return midiMapper_; }
/// Process a MIDI event (typically from the real-time audio thread).
/// Routes CC messages to the midi mapper. Returns true if consumed.
bool processMidiEvent(const struct MidiEvent& event);
/// --- State Serialization ---
struct MixerSnapshot {
struct ChannelState {
int channelIndex;
float volume;
float pan;
bool mute;
bool solo;
MixerChannelType channelType;
std::string label;
bool hpEnabled;
float hpFrequency;
std::vector<float> auxSendLevels; // indexed by aux bus index
float vuLeft = -96.0f; // dB — peak VU level
float vuRight = -96.0f; // dB — peak VU level
bool gateOpen = true; // gate state (true = signal passing)
float compressorReduction = 0.0f; // dB of gain reduction (0 = no reduction)
};
struct BusState {
int64_t id;
std::string name;
MixerBusType type;
float volume;
bool mute;
float vuLeft = -96.0f; // dB — peak VU level
float vuRight = -96.0f; // dB — peak VU level
};
std::vector<ChannelState> channels;
std::vector<BusState> buses;
std::vector<MixerRouteEntry> routes;
};
MixerSnapshot captureSnapshot() const;
void applySnapshot(const MixerSnapshot& snapshot);
private:
std::vector<std::unique_ptr<MixerChannelStrip>> channels_;
std::map<int64_t, std::unique_ptr<MixerBus>> buses_;
MixerBus* masterBus_ = nullptr;
// Routing entries
std::vector<MixerRouteEntry> routes_;
// Output routing entries (bus → physical output channel mapping)
std::vector<MixerOutputRoute> outputRoutes_;
// Audio configuration
uint32_t sampleRate_ = 48000;
size_t maxBufferSize_ = 512;
// IHost reference for FX preparation
IHost* pHost_ = nullptr;
// MIDI CC control surface mapper
MidiMapper midiMapper_;
// Next bus ID counter
static std::atomic<int64_t> nextBusId_;
// Temporary per-channel output buffers for routing
// Allocated once at prepare time
std::vector<std::vector<float>> channelOutputBuffers_;
// Physical I/O channel counts (from audio device auto-detection)
uint32_t physicalInputCount_ = 0;
uint32_t physicalOutputCount_ = 0;
// Internal helper: accumulate a channel's output to all its routed buses
void routeChannelOutput(
MixerChannelStrip* channel,
float** channelOutput,
uint32_t frames
);
// Internal helper: process all bus-to-bus routing
void processBusRouting(uint32_t frames);
// Build a list of all routes from a given source
std::vector<MixerRouteEntry*> findRoutesForSource(int64_t sourceId);
};
} // namespace pipedal
+29 -1
View File
@@ -33,6 +33,7 @@
#include "CpuGovernor.hpp"
#include "RegDb.hpp"
#include "RingBufferReader.hpp"
#include "MixerEngine.hpp"
#include "PiPedalUI.hpp"
#include "atom_object.hpp"
#include "Lv2PluginChangeMonitor.hpp"
@@ -438,7 +439,7 @@ void PiPedalModel::Load()
UpdateDefaults(&this->pedalboard);
std::unique_ptr<AudioHost> p{AudioHost::CreateInstance(pluginHost.asIHost())};
std::unique_ptr<AudioHost> p{AudioHost::CreateInstance(pluginHost.asIHost(), this->driverType_)};
this->audioHost = std::move(p);
this->audioHost->SetNotificationCallbacks(this);
@@ -3081,6 +3082,33 @@ std::shared_ptr<Lv2Pedalboard> PiPedalModel::GetLv2Pedalboard()
return lv2Pedalboard;
}
void PiPedalModel::SetMixerEngine(const std::shared_ptr<MixerEngine>& engine)
{
this->mixerEngine = engine;
if (this->audioHost)
{
this->audioHost->SetMixerEngine(engine);
}
if (engine && this->audioHost && this->audioHost->IsOpen())
{
// Auto-create channels based on detected input channel count
uint32_t inputChannels = this->audioHost->GetDeviceCaptureChannels();
if (inputChannels == 0) inputChannels = 2; // default to stereo
engine->autoCreateChannels(inputChannels);
Lv2Log::info(SS("Mixer engine initialized with "
<< inputChannels << " channels, "
<< engine->channelCount() << " strips created."));
}
// Load saved MIDI control surface mappings
if (engine) {
engine->midiMapper().loadFromFile();
}
}
void PiPedalModel::SetSelectedPedalboardPlugin(uint64_t clientId, uint64_t pedalboardId)
{
// Thinking on this:
+7
View File
@@ -210,6 +210,7 @@ namespace pipedal
std::unique_ptr<AudioHost> audioHost;
JackConfiguration jackConfiguration;
std::shared_ptr<Lv2Pedalboard> lv2Pedalboard;
std::shared_ptr<MixerEngine> mixerEngine;
std::filesystem::path webRoot;
using SubscriberList = std::vector<std::shared_ptr<IPiPedalModelSubscriber>>;
@@ -285,6 +286,7 @@ namespace pipedal
void UpdateVst3Settings(Pedalboard &pedalboard);
PiPedalConfiguration configuration;
std::string driverType_ = "alsa";
void CheckForResourceInitialization(Pedalboard &pedalboard);
UiFileProperty::ptr FindLoadedPatchProperty(int64_t instanceId, const std::string &patchPropertyUri);
@@ -293,6 +295,9 @@ namespace pipedal
PiPedalModel();
virtual ~PiPedalModel();
void SetDriverType(const std::string &driverType) { driverType_ = driverType; }
const std::string &GetDriverType() const { return driverType_; }
enum class Direction
{
Increase,
@@ -401,6 +406,8 @@ namespace pipedal
void SetPedalboard(int64_t clientId, Pedalboard &pedalboard);
Pedalboard &GetPedalboard();
std::shared_ptr<Lv2Pedalboard> GetLv2Pedalboard();
std::shared_ptr<MixerEngine> GetMixerEngine() { return mixerEngine; }
void SetMixerEngine(const std::shared_ptr<MixerEngine>& engine);
void UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalboard);
+291
View File
@@ -21,6 +21,7 @@
#include "Curl.hpp"
#include "PiPedalSocket.hpp"
#include "MixerApi.hpp"
#include "Updater.hpp"
#include "json.hpp"
#include "viewstream.hpp"
@@ -707,6 +708,7 @@ private:
std::recursive_mutex writeMutex;
PiPedalModel &model;
MixerApi mixerApi;
static std::atomic<uint64_t> nextClientId;
std::string imageList;
@@ -814,6 +816,12 @@ public:
PiPedalSocketHandler(PiPedalModel &model)
: model(model), clientId(++nextClientId)
{
// Wire MixerEngine to MixerApi if available
auto engine = model.GetMixerEngine();
if (engine) {
this->mixerApi.setMixerEngine(engine.get());
}
std::stringstream imageList;
const std::filesystem::path &webRoot = model.GetWebRoot() / "img";
bool firstTime = true;
@@ -1217,6 +1225,289 @@ public:
}
REGISTER_MESSAGE_HANDLER(writeTone3000Readme)
/************************************************************************/
/* Band-in-a-Box Mixer Messages */
/************************************************************************/
void handle_mixerSetChannelVolume(int replyTo, json_reader *pReader)
{
int channelIndex;
double volume;
pReader->read(&channelIndex);
pReader->read(&volume);
this->mixerApi.setChannelVolume(channelIndex, (float)volume);
}
REGISTER_MESSAGE_HANDLER(mixerSetChannelVolume)
void handle_mixerSetChannelPan(int replyTo, json_reader *pReader)
{
int channelIndex;
double pan;
pReader->read(&channelIndex);
pReader->read(&pan);
this->mixerApi.setChannelPan(channelIndex, (float)pan);
}
REGISTER_MESSAGE_HANDLER(mixerSetChannelPan)
void handle_mixerSetChannelMute(int replyTo, json_reader *pReader)
{
int channelIndex;
bool mute;
pReader->read(&channelIndex);
pReader->read(&mute);
this->mixerApi.setChannelMute(channelIndex, mute);
}
REGISTER_MESSAGE_HANDLER(mixerSetChannelMute)
void handle_mixerSetChannelSolo(int replyTo, json_reader *pReader)
{
int channelIndex;
bool solo;
pReader->read(&channelIndex);
pReader->read(&solo);
this->mixerApi.setChannelSolo(channelIndex, solo);
}
REGISTER_MESSAGE_HANDLER(mixerSetChannelSolo)
void handle_mixerSetChannelLabel(int replyTo, json_reader *pReader)
{
int channelIndex;
std::string label;
pReader->read(&channelIndex);
pReader->read(&label);
this->mixerApi.setChannelLabel(channelIndex, label);
}
REGISTER_MESSAGE_HANDLER(mixerSetChannelLabel)
void handle_mixerSetChannelHpf(int replyTo, json_reader *pReader)
{
int channelIndex;
bool enabled;
double frequency;
pReader->read(&channelIndex);
pReader->read(&enabled);
pReader->read(&frequency);
this->mixerApi.setChannelHpf(channelIndex, enabled, (float)frequency);
}
REGISTER_MESSAGE_HANDLER(mixerSetChannelHpf)
void handle_mixerGetState(int replyTo, json_reader *pReader)
{
std::string stateJson = this->mixerApi.getStateJson();
this->JsonReply(replyTo, "mixerState", stateJson.c_str());
}
REGISTER_MESSAGE_HANDLER(mixerGetState)
void handle_mixerAddChannel(int replyTo, json_reader *pReader)
{
int physicalInputIndex;
pReader->read(&physicalInputIndex);
int channelIndex = this->mixerApi.addChannel(physicalInputIndex);
this->Reply(replyTo, "mixerAddChannel", (int64_t)channelIndex);
}
REGISTER_MESSAGE_HANDLER(mixerAddChannel)
void handle_mixerRemoveChannel(int replyTo, json_reader *pReader)
{
int channelIndex;
pReader->read(&channelIndex);
this->mixerApi.removeChannel(channelIndex);
}
REGISTER_MESSAGE_HANDLER(mixerRemoveChannel)
void handle_mixerSetBusVolume(int replyTo, json_reader *pReader)
{
int64_t busId;
double volume;
pReader->read(&busId);
pReader->read(&volume);
this->mixerApi.setBusVolume(busId, (float)volume);
}
REGISTER_MESSAGE_HANDLER(mixerSetBusVolume)
void handle_mixerSetBusMute(int replyTo, json_reader *pReader)
{
int64_t busId;
bool mute;
pReader->read(&busId);
pReader->read(&mute);
this->mixerApi.setBusMute(busId, mute);
}
REGISTER_MESSAGE_HANDLER(mixerSetBusMute)
void handle_mixerRouteChannelToBus(int replyTo, json_reader *pReader)
{
int channelIndex;
int64_t busId;
double level;
pReader->read(&channelIndex);
pReader->read(&busId);
pReader->read(&level);
this->mixerApi.routeChannelToBus(channelIndex, busId, (float)level);
}
REGISTER_MESSAGE_HANDLER(mixerRouteChannelToBus)
void handle_mixerAddBus(int replyTo, json_reader *pReader)
{
std::string type;
std::string name;
int channels;
pReader->read(&type);
pReader->read(&name);
pReader->read(&channels);
int64_t busId = this->mixerApi.addBus(type, name, channels);
this->Reply(replyTo, "mixerAddBus", busId);
}
REGISTER_MESSAGE_HANDLER(mixerAddBus)
void handle_mixerRemoveBus(int replyTo, json_reader *pReader)
{
int64_t busId;
pReader->read(&busId);
this->mixerApi.removeBus(busId);
}
REGISTER_MESSAGE_HANDLER(mixerRemoveBus)
void handle_mixerSaveScene(int replyTo, json_reader *pReader)
{
int64_t sceneId;
std::string name;
pReader->read(&sceneId);
pReader->read(&name);
std::string result = this->mixerApi.saveScene(name);
this->JsonReply(replyTo, "mixerSaveScene", result.c_str());
}
REGISTER_MESSAGE_HANDLER(mixerSaveScene)
void handle_mixerLoadScene(int replyTo, json_reader *pReader)
{
int64_t sceneId;
pReader->read(&sceneId);
bool ok = this->mixerApi.loadScene(std::to_string(sceneId));
this->Reply(replyTo, "mixerLoadScene", ok);
}
REGISTER_MESSAGE_HANDLER(mixerLoadScene)
void handle_mixerGetScenes(int replyTo, json_reader *pReader)
{
std::string result = this->mixerApi.listScenes();
this->JsonReply(replyTo, "mixerGetScenes", result.c_str());
}
REGISTER_MESSAGE_HANDLER(mixerGetScenes)
/************************************************************************/
/* Mixer Output Routing Messages */
/************************************************************************/
void handle_mixerGetOutputRoutes(int replyTo, json_reader *pReader)
{
std::string json = this->mixerApi.getOutputRoutesJson();
this->JsonReply(replyTo, "mixerOutputRoutes", json.c_str());
}
REGISTER_MESSAGE_HANDLER(mixerGetOutputRoutes)
void handle_mixerSetOutputRoutes(int replyTo, json_reader *pReader)
{
std::string json;
pReader->read(&json);
this->mixerApi.setOutputRoutesFromJson(json);
}
REGISTER_MESSAGE_HANDLER(mixerSetOutputRoutes)
/************************************************************************/
/* MIDI Control Surface Mapping Messages */
/************************************************************************/
void handle_mixerGetMidiMappings(int replyTo, json_reader *pReader)
{
std::string json = this->mixerApi.getMidiMappingsJson();
this->JsonReply(replyTo, "mixerMidiMappings", json.c_str());
}
REGISTER_MESSAGE_HANDLER(mixerGetMidiMappings)
void handle_mixerSetMidiMappings(int replyTo, json_reader *pReader)
{
std::string json;
pReader->read(&json);
this->mixerApi.setMidiMappingsFromJson(json);
}
REGISTER_MESSAGE_HANDLER(mixerSetMidiMappings)
void handle_mixerAddMidiMapping(int replyTo, json_reader *pReader)
{
int midiChannel;
int ccNumber;
std::string targetType;
int64_t targetId;
double minValue = 0.0;
double maxValue = 1.0;
pReader->read(&midiChannel);
pReader->read(&ccNumber);
pReader->read(&targetType);
pReader->read(&targetId);
pReader->read(&minValue);
pReader->read(&maxValue);
this->mixerApi.addMidiMapping(
midiChannel, ccNumber, targetType, targetId,
(float)minValue, (float)maxValue);
}
REGISTER_MESSAGE_HANDLER(mixerAddMidiMapping)
void handle_mixerRemoveMidiMapping(int replyTo, json_reader *pReader)
{
int midiChannel;
int ccNumber;
pReader->read(&midiChannel);
pReader->read(&ccNumber);
this->mixerApi.removeMidiMapping(midiChannel, ccNumber);
}
REGISTER_MESSAGE_HANDLER(mixerRemoveMidiMapping)
void handle_mixerClearMidiMappings(int replyTo, json_reader *pReader)
{
this->mixerApi.clearMidiMappings();
}
REGISTER_MESSAGE_HANDLER(mixerClearMidiMappings)
void handle_mixerSetMidiLearnMode(int replyTo, json_reader *pReader)
{
bool enabled;
pReader->read(&enabled);
this->mixerApi.setMidiLearnMode(enabled);
}
REGISTER_MESSAGE_HANDLER(mixerSetMidiLearnMode)
void handle_mixerSetMidiLearnTarget(int replyTo, json_reader *pReader)
{
std::string targetType;
int64_t targetId;
pReader->read(&targetType);
pReader->read(&targetId);
this->mixerApi.setMidiLearnTarget(targetType, targetId);
}
REGISTER_MESSAGE_HANDLER(mixerSetMidiLearnTarget)
void handle_mixerCommitMidiLearn(int replyTo, json_reader *pReader)
{
bool success = this->mixerApi.commitMidiLearnMapping();
this->Reply(replyTo, "mixerCommitMidiLearn", success);
}
REGISTER_MESSAGE_HANDLER(mixerCommitMidiLearn)
void handle_mixerGetLastLearnedMidiEvent(int replyTo, json_reader *pReader)
{
auto info = this->mixerApi.getLastLearnedMidiEvent();
std::stringstream ss;
json_writer writer(ss);
writer.start_object();
writer.write_member("hasEvent", info.hasEvent);
writer.write_member("midiChannel", (int64_t)info.midiChannel);
writer.write_member("ccNumber", (int64_t)info.ccNumber);
writer.end_object();
this->JsonReply(replyTo, "mixerLastLearnedMidiEvent", ss.str().c_str());
}
REGISTER_MESSAGE_HANDLER(mixerGetLastLearnedMidiEvent)
void handle_sha256Base64url(int replyTo, json_reader *pReader)
{
std::string input;
+1 -1
View File
@@ -54,7 +54,7 @@ static std::string MakeWebAddress(const std::string &address, uint16_t port)
PiPedalVersion::PiPedalVersion(PiPedalModel&model)
{
server_ = "PiPedal Server";
server_ = "OP-Pedal / Ourpad Pedal Server";
// defined on build command line.
serverVersion_ = PROJECT_DISPLAY_VERSION;
#ifdef _WIN32
+911
View File
@@ -0,0 +1,911 @@
/*
* MIT License
*
* Copyright (c) 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 "pch.h"
#include "PiPedalCommon.hpp"
#include "util.hpp"
#include <cmath>
#include "Finally.hpp"
#include <bit>
#include <memory>
#include "ss.hpp"
#include "PipeWireDriver.hpp"
#include "JackServerSettings.hpp"
#include <thread>
#include "RtInversionGuard.hpp"
#include "PiPedalException.hpp"
#include "SchedulerPriority.hpp"
#include "CrashGuard.hpp"
#include <iostream>
#include <iomanip>
#include "ChannelRouterSettings.hpp"
#include "CpuUse.hpp"
#include <pipewire/pipewire.h>
#include <pipewire/filter.h>
#include <spa/param/audio/format-utils.h>
#include <spa/param/props.h>
#include "Lv2Log.hpp"
#include <limits>
#include "ss.hpp"
using namespace pipedal;
namespace pipedal
{
class PipeWireDriverImpl : public AudioDriver
{
private:
// ---- PipeWire state ----
pw_filter *filter = nullptr;
pw_thread_loop *threadLoop = nullptr;
void *inputPortData = nullptr;
void *outputPortData = nullptr;
bool pwInitialized = false;
// ---- Audio parameters ----
uint32_t sampleRate = 0;
uint32_t bufferSize = 0;
uint32_t captureChannels = 0;
uint32_t playbackChannels = 0;
// ---- Buffer management (mirrors AlsaDriver pattern) ----
std::vector<std::vector<float>> allocatedBuffers;
std::vector<float *> deviceCaptureBuffers;
std::vector<float *> devicePlaybackBuffers;
float *zeroInputBuffer = nullptr;
float *discardOutputBuffer = nullptr;
std::vector<float *> mainCaptureBuffers;
std::vector<float *> mainPlaybackBuffers;
std::vector<float *> auxCaptureBuffers;
std::vector<float *> auxPlaybackBuffers;
// ---- Mix ops for channel routing ----
using MixOp = std::function<void(size_t nFrames)>;
std::vector<MixOp> mixOps;
void AddMixCopyOp(float *inputBuffer, float *outputBuffer)
{
mixOps.push_back([inputBuffer, outputBuffer](size_t nFrames)
{
float * PIPEDAL_RESTRICT pIn = inputBuffer;
float * PIPEDAL_RESTRICT pOut = outputBuffer;
for (size_t i = 0; i < nFrames; ++i)
pOut[i] = pIn[i]; });
}
void AddMixAddOp(float *inputBuffer, float *outputBuffer)
{
mixOps.push_back([inputBuffer, outputBuffer](size_t nFrames)
{
float * PIPEDAL_RESTRICT pIn = inputBuffer;
float * PIPEDAL_RESTRICT pOut = outputBuffer;
for (size_t i = 0; i < nFrames; ++i)
pOut[i] += pIn[i]; });
}
// ---- CPU monitoring ----
pipedal::CpuUse cpuUse;
// ---- Lifecycle state ----
bool open = false;
bool activated = false;
bool isDummyDriver = false;
AudioDriverHost *driverHost = nullptr;
ChannelSelection channelSelection;
JackServerSettings jackServerSettings;
AlsaSequencer::ptr alsaSequencer;
// ---- MIDI (unused by PipeWire, sequencer handles it) ----
std::vector<MidiEvent> midiEvents;
size_t midiEventCount = 0;
public:
PipeWireDriverImpl(AudioDriverHost *driverHost)
: driverHost(driverHost)
{
}
virtual ~PipeWireDriverImpl()
{
Close();
}
// ----------------------------------------------------------------
// AudioDriver interface implementation
// ----------------------------------------------------------------
virtual float CpuUse() override
{
return cpuUse.GetCpuUse();
}
virtual float CpuOverhead() override
{
return cpuUse.GetCpuOverhead();
}
virtual uint32_t GetSampleRate() override
{
return this->sampleRate;
}
virtual uint32_t GetDeviceCaptureChannels() const override
{
return this->captureChannels;
}
virtual uint32_t GetDevicePlaybackChannels() const override
{
return this->playbackChannels;
}
virtual size_t GetMidiInputEventCount() override
{
return midiEventCount;
}
virtual MidiEvent *GetMidiEvents() override
{
return this->midiEvents.data();
}
virtual const ChannelSelection &GetChannelSelection() const override
{
return channelSelection;
}
virtual std::vector<float *> &DeviceInputBuffers() override { return this->deviceCaptureBuffers; }
virtual size_t DeviceInputBufferCount() const override { return deviceCaptureBuffers.size(); }
virtual float *GetDeviceInputBuffer(size_t channel) const override
{
if (channel >= deviceCaptureBuffers.size())
return nullptr;
return deviceCaptureBuffers[channel];
}
virtual std::vector<float *> &DeviceOutputBuffers() override { return this->devicePlaybackBuffers; }
virtual size_t DeviceOutputBufferCount() const override { return devicePlaybackBuffers.size(); }
virtual float *GetDeviceOutputBuffer(size_t channel) const override
{
if (channel >= devicePlaybackBuffers.size())
return nullptr;
return devicePlaybackBuffers[channel];
}
virtual std::vector<float *> &MainInputBuffers() override { return this->mainCaptureBuffers; }
virtual size_t MainInputBufferCount() const override { return mainCaptureBuffers.size(); }
virtual float *GetMainInputBuffer(size_t channel) override
{
if (channel >= (int64_t)mainCaptureBuffers.size())
throw std::runtime_error("Argument out of range.");
return mainCaptureBuffers[channel];
}
virtual std::vector<float *> &MainOutputBuffers() override { return this->mainPlaybackBuffers; }
virtual size_t MainOutputBufferCount() const override { return mainPlaybackBuffers.size(); }
virtual float *GetMainOutputBuffer(size_t channel) override
{
return mainPlaybackBuffers[channel];
}
virtual std::vector<float *> &AuxInputBuffers() override { return this->auxCaptureBuffers; }
virtual size_t AuxInputBufferCount() const override { return auxCaptureBuffers.size(); }
virtual float *GetAuxInputBuffer(size_t channel) override
{
return auxCaptureBuffers[channel];
}
virtual std::vector<float *> &AuxOutputBuffers() override { return this->auxPlaybackBuffers; }
virtual size_t AuxOutputBufferCount() const override { return auxPlaybackBuffers.size(); }
virtual float *GetAuxOutputBuffer(size_t channel) override
{
return auxPlaybackBuffers[channel];
}
virtual float *GetZeroInputBuffer() override
{
if (zeroInputBuffer == nullptr)
zeroInputBuffer = AllocateAudioBuffer();
return zeroInputBuffer;
}
virtual float *GetDiscardOutputBuffer() override
{
if (discardOutputBuffer == nullptr)
discardOutputBuffer = AllocateAudioBuffer();
return discardOutputBuffer;
}
// ----------------------------------------------------------------
// Open - Initialize PipeWire and create the filter
// ----------------------------------------------------------------
virtual void Open(const JackServerSettings &jackServerSettings_,
const ChannelSelection &channelSelection_) override
{
if (open)
throw PiPedalStateException("Already open.");
this->jackServerSettings = jackServerSettings_;
this->channelSelection = channelSelection_;
this->isDummyDriver = jackServerSettings_.IsDummyAudioDevice();
this->bufferSize = jackServerSettings_.GetBufferSize();
this->sampleRate = (uint32_t)jackServerSettings_.GetSampleRate();
if (this->sampleRate == 0)
this->sampleRate = 48000;
if (this->bufferSize == 0)
this->bufferSize = 256;
open = true;
try
{
if (!pwInitialized)
{
pw_init(nullptr, nullptr);
pwInitialized = true;
}
// Create thread loop for non-blocking lifecycle management
threadLoop = pw_thread_loop_new("pipedal-pw-loop", nullptr);
if (!threadLoop)
{
throw PiPedalStateException("Failed to create PipeWire thread loop.");
}
// Create filter using the thread loop's pw_loop
struct pw_properties *props = pw_properties_new(
PW_KEY_NODE_NAME, "PiPedal",
PW_KEY_NODE_DESCRIPTION, "PiPedal Guitar Effects Processor",
PW_KEY_MEDIA_NAME, "PiPedal Audio",
PW_KEY_MEDIA_TYPE, "Audio",
PW_KEY_MEDIA_CATEGORY, "Filter",
PW_KEY_MEDIA_CLASS, "Audio/Sink",
PW_KEY_APP_NAME, "PiPedal",
PW_KEY_APP_PROCESS_BINARY, "pipedald",
PW_KEY_PRIORITY_SESSION, "0",
nullptr);
static const struct pw_filter_events filterEvents = {
.version = PW_VERSION_FILTER_EVENTS,
.state_changed = on_filter_state_changed_static,
.process = on_filter_process_static,
};
filter = pw_filter_new_simple(
pw_thread_loop_get_loop(threadLoop),
"PiPedal",
props,
&filterEvents,
this);
if (!filter)
{
pw_thread_loop_destroy(threadLoop);
threadLoop = nullptr;
throw PiPedalStateException("Failed to create PipeWire filter.");
}
// Determine channel count from channel selection
captureChannels = (uint32_t)channelSelection_.mainInputChannels().size();
playbackChannels = (uint32_t)channelSelection_.mainOutputChannels().size();
if (captureChannels == 0)
captureChannels = 2; // default stereo
if (playbackChannels == 0)
playbackChannels = 2;
// Build audio format params for input (capture) port
{
uint8_t buffer[1024];
spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
spa_audio_info_raw audioFormat = {};
audioFormat.format = SPA_AUDIO_FORMAT_F32;
audioFormat.rate = this->sampleRate;
audioFormat.channels = captureChannels;
// Set channel positions
SetChannelPositions(audioFormat, captureChannels);
const spa_pod *params[1];
params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &audioFormat);
inputPortData = pw_filter_add_port(
filter,
PW_DIRECTION_INPUT,
PW_FILTER_PORT_FLAG_MAP_BUFFERS,
sizeof(void *),
pw_properties_new(
PW_KEY_PORT_NAME, "Input",
PW_KEY_AUDIO_CHANNELS, std::to_string(captureChannels).c_str(),
nullptr),
params, 1);
if (!inputPortData)
{
pw_filter_destroy(filter);
filter = nullptr;
pw_thread_loop_destroy(threadLoop);
threadLoop = nullptr;
throw PiPedalStateException("Failed to add PipeWire input port.");
}
}
// Build audio format params for output (playback) port
{
uint8_t buffer[1024];
spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
spa_audio_info_raw audioFormat = {};
audioFormat.format = SPA_AUDIO_FORMAT_F32;
audioFormat.rate = this->sampleRate;
audioFormat.channels = playbackChannels;
SetChannelPositions(audioFormat, playbackChannels);
const spa_pod *params[1];
params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &audioFormat);
outputPortData = pw_filter_add_port(
filter,
PW_DIRECTION_OUTPUT,
PW_FILTER_PORT_FLAG_MAP_BUFFERS,
sizeof(void *),
pw_properties_new(
PW_KEY_PORT_NAME, "Output",
PW_KEY_AUDIO_CHANNELS, std::to_string(playbackChannels).c_str(),
nullptr),
params, 1);
if (!outputPortData)
{
pw_filter_destroy(filter);
filter = nullptr;
pw_thread_loop_destroy(threadLoop);
threadLoop = nullptr;
throw PiPedalStateException("Failed to add PipeWire output port.");
}
}
// Connect the filter to the PipeWire graph
int res = pw_filter_connect(
filter,
PW_FILTER_FLAG_RT_PROCESS,
nullptr, 0);
if (res < 0)
{
pw_filter_destroy(filter);
filter = nullptr;
pw_thread_loop_destroy(threadLoop);
threadLoop = nullptr;
throw PiPedalStateException(
std::string("Failed to connect PipeWire filter: ") + strerror(-res));
}
// Start the thread loop
pw_thread_loop_start(threadLoop);
Lv2Log::info(SS("PipeWire driver opened: " << captureChannels
<< " capture channels, "
<< playbackChannels
<< " playback channels, "
<< this->sampleRate << "Hz, "
<< this->bufferSize << " frames"));
}
catch (const std::exception &e)
{
Close();
throw;
}
}
// ----------------------------------------------------------------
// Activate - allocate buffers and start processing
// ----------------------------------------------------------------
virtual void Activate() override
{
if (activated)
throw PiPedalStateException("Already activated.");
activated = true;
// Reset previously allocated buffers
allocatedBuffers.resize(0);
// Allocate device capture buffers
zeroInputBuffer = AllocateAudioBuffer();
deviceCaptureBuffers.resize(captureChannels);
for (size_t i = 0; i < captureChannels; ++i)
{
deviceCaptureBuffers[i] = AllocateAudioBuffer();
}
// Allocate device playback buffers
devicePlaybackBuffers.resize(playbackChannels);
for (size_t i = 0; i < playbackChannels; ++i)
{
devicePlaybackBuffers[i] = AllocateAudioBuffer();
}
// Allocate input channel routing (main)
AllocateInputChannels(
channelSelection.mainInputChannels(),
this->mainCaptureBuffers);
// Allocate output channel routing (main)
AllocateOutputChannels(
channelSelection.mainOutputChannels(),
this->mainPlaybackBuffers);
// Allocate aux channels
AllocateAuxChannels();
// Set up mix operations for channel routing
AddMixOps();
}
// ----------------------------------------------------------------
// Deactivate - stop processing
// ----------------------------------------------------------------
virtual void Deactivate() override
{
if (!activated)
return;
activated = false;
// The pw_filter will continue to call process but we won't
// do anything since activated is false. The PipeWire graph
// will eventually stop scheduling us.
// Clear mix ops
mixOps.clear();
}
// ----------------------------------------------------------------
// Close - tear down PipeWire resources
// ----------------------------------------------------------------
virtual void Close() override
{
if (!open)
return;
open = false;
Deactivate();
// Clean up PipeWire filter
if (filter)
{
pw_filter_disconnect(filter);
pw_filter_destroy(filter);
filter = nullptr;
}
// Stop and destroy thread loop
if (threadLoop)
{
pw_thread_loop_stop(threadLoop);
pw_thread_loop_destroy(threadLoop);
threadLoop = nullptr;
}
// Deinitialize PipeWire (only if we initialized it)
if (pwInitialized)
{
// pw_deinit(); // Note: we don't call this to avoid issues with
// other PipeWire users in the process. It's safe to leave initialized.
}
DeleteBuffers();
this->alsaSequencer = nullptr;
Lv2Log::info("PipeWire driver closed.");
}
// ----------------------------------------------------------------
// SetAlsaSequencer - store for MIDI routing (not used by PipeWire)
// ----------------------------------------------------------------
virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override
{
this->alsaSequencer = alsaSequencer;
}
// ----------------------------------------------------------------
// GetConfigurationDescription - human-readable description
// ----------------------------------------------------------------
virtual std::string GetConfigurationDescription() override
{
std::stringstream s;
s << "PipeWire: " << captureChannels << " in, "
<< playbackChannels << " out, "
<< this->sampleRate << " Hz, "
<< this->bufferSize << " frames";
return s.str();
}
// ----------------------------------------------------------------
// DumpBufferTrace - stub (no PipeWire equivalent)
// ----------------------------------------------------------------
virtual void DumpBufferTrace(size_t nEntries) override
{
// Not implemented for PipeWire driver
}
private:
// ----------------------------------------------------------------
// Static PipeWire filter event callbacks
// ----------------------------------------------------------------
static void on_filter_process_static(void *data, struct spa_io_position *position)
{
auto *self = static_cast<PipeWireDriverImpl *>(data);
self->on_filter_process(position);
}
static void on_filter_state_changed_static(
void *data,
enum pw_filter_state old,
enum pw_filter_state state,
const char *error)
{
auto *self = static_cast<PipeWireDriverImpl *>(data);
self->on_filter_state_changed(old, state, error);
}
// ----------------------------------------------------------------
// Filter state change handler
// ----------------------------------------------------------------
void on_filter_state_changed(
enum pw_filter_state old,
enum pw_filter_state state,
const char *error)
{
if (state == PW_FILTER_STATE_ERROR)
{
Lv2Log::error(SS("PipeWire filter error: " << (error ? error : "unknown")));
}
}
// ----------------------------------------------------------------
// Main processing callback - called from PipeWire RT thread
// ----------------------------------------------------------------
void on_filter_process(struct spa_io_position *position)
{
if (!activated)
return;
pw_buffer *inBuf = nullptr;
pw_buffer *outBuf = nullptr;
// Dequeue input buffer (capture)
inBuf = (pw_buffer *)pw_filter_dequeue_buffer(inputPortData);
if (!inBuf)
return;
// Dequeue output buffer (playback)
outBuf = (pw_buffer *)pw_filter_dequeue_buffer(outputPortData);
if (!outBuf)
{
pw_filter_queue_buffer(inputPortData, inBuf);
return;
}
spa_buffer *spaIn = inBuf->buffer;
spa_buffer *spaOut = outBuf->buffer;
uint32_t nInputChannels = std::min(spaIn->n_datas, captureChannels);
uint32_t nOutputChannels = std::min(spaOut->n_datas, playbackChannels);
// Determine frame count from the input buffer data size
uint32_t nFrames = 0;
if (spaIn->n_datas > 0 && spaIn->datas[0].chunk)
{
nFrames = spaIn->datas[0].chunk->size / sizeof(float);
if (nInputChannels > 1)
nFrames /= nInputChannels;
}
if (nFrames == 0)
nFrames = this->bufferSize;
// Clamp to our buffer size
if (nFrames > this->bufferSize)
nFrames = this->bufferSize;
// ---- Read input from PipeWire ----
// Copy input data from PipeWire buffers to our device capture buffers.
// PipeWire uses planar (non-interleaved) format where each data chunk is one channel.
for (uint32_t ch = 0; ch < nInputChannels; ++ch)
{
if (ch < deviceCaptureBuffers.size() && spaIn->datas[ch].data != nullptr)
{
float *src = (float *)((uint8_t *)spaIn->datas[ch].data +
(spaIn->datas[ch].chunk ? spaIn->datas[ch].chunk->offset : 0));
float *dst = deviceCaptureBuffers[ch];
for (uint32_t i = 0; i < nFrames; ++i)
{
dst[i] = src[i];
}
}
}
// Zero out any remaining input channels that PipeWire didn't fill
for (uint32_t ch = nInputChannels; ch < captureChannels; ++ch)
{
if (ch < deviceCaptureBuffers.size())
{
float *dst = deviceCaptureBuffers[ch];
memset(dst, 0, nFrames * sizeof(float));
}
}
// ---- Channel routing (input) ----
for (auto &mixOp : mixOps)
{
mixOp(nFrames);
}
// ---- Process audio via the host ----
// This is where PiPedal's DSP pipeline runs (on the RT thread)
driverHost->OnProcess(nFrames);
// ---- Write output to PipeWire ----
for (uint32_t ch = 0; ch < nOutputChannels; ++ch)
{
if (ch < devicePlaybackBuffers.size() && spaOut->datas[ch].data != nullptr)
{
float *dst = (float *)((uint8_t *)spaOut->datas[ch].data +
(spaOut->datas[ch].chunk ? spaOut->datas[ch].chunk->offset : 0));
float *src = devicePlaybackBuffers[ch];
uint32_t copyFrames = std::min(nFrames,
(uint32_t)(spaOut->datas[ch].chunk ?
spaOut->datas[ch].chunk->size / sizeof(float) : nFrames));
for (uint32_t i = 0; i < copyFrames; ++i)
{
dst[i] = src[i];
}
}
}
// Queue both buffers back to PipeWire
pw_filter_queue_buffer(outputPortData, outBuf);
pw_filter_queue_buffer(inputPortData, inBuf);
}
// ----------------------------------------------------------------
// Buffer allocation helpers (mirrors AlsaDriver pattern)
// ----------------------------------------------------------------
float *AllocateAudioBuffer()
{
std::vector<float> buffer;
buffer.resize(this->bufferSize);
float *pBuffer = buffer.data();
allocatedBuffers.push_back(std::move(buffer));
return pBuffer;
}
void DeleteBuffers()
{
mainCaptureBuffers.clear();
mainPlaybackBuffers.clear();
auxCaptureBuffers.clear();
auxPlaybackBuffers.clear();
deviceCaptureBuffers.clear();
devicePlaybackBuffers.clear();
zeroInputBuffer = nullptr;
discardOutputBuffer = nullptr;
allocatedBuffers.clear();
}
// ----------------------------------------------------------------
// Channel allocation helpers (mirrors AlsaDriver pattern)
// ----------------------------------------------------------------
void AllocateInputChannels(
const std::vector<int64_t> &channelSelection,
std::vector<float *> &channelBuffers)
{
size_t nChannels = channelSelection.size();
if (nChannels == 0)
{
channelBuffers.resize(0);
return;
}
channelBuffers.resize(nChannels);
for (size_t i = 0; i < nChannels; ++i)
{
int64_t deviceChannel = channelSelection[i];
if (deviceChannel == -1 || (size_t)deviceChannel >= captureChannels)
{
channelBuffers[i] = zeroInputBuffer;
}
else
{
channelBuffers[i] = deviceCaptureBuffers[deviceChannel];
}
}
}
void AllocateOutputChannels(
const std::vector<int64_t> &channelSelection,
std::vector<float *> &channelBuffers)
{
size_t nChannels = channelSelection.size();
if (nChannels == 0)
{
channelBuffers.resize(0);
return;
}
channelBuffers.resize(nChannels);
for (size_t i = 0; i < nChannels; ++i)
{
int64_t deviceChannel = channelSelection[i];
if (deviceChannel == -1)
{
channelBuffers[i] = this->GetDiscardOutputBuffer();
}
else
{
float *mixBuffer = AllocateAudioBuffer();
channelBuffers[i] = mixBuffer;
}
}
}
void AllocateAuxChannels()
{
for (auto ix : channelSelection.auxInputChannels())
{
if ((size_t)ix < deviceCaptureBuffers.size())
auxCaptureBuffers.push_back(this->deviceCaptureBuffers[ix]);
else
auxCaptureBuffers.push_back(this->zeroInputBuffer);
}
for (auto ix : channelSelection.auxOutputChannels())
{
if ((size_t)ix < devicePlaybackBuffers.size())
auxPlaybackBuffers.push_back(this->devicePlaybackBuffers[ix]);
else
auxPlaybackBuffers.push_back(this->GetDiscardOutputBuffer());
}
}
// ----------------------------------------------------------------
// Mix operations for channel routing (mirrors AlsaDriver)
// ----------------------------------------------------------------
void AddMixOps()
{
// Main input: copy from device capture channels to main capture channels
for (size_t i = 0; i < mainCaptureBuffers.size(); ++i)
{
if (mainCaptureBuffers[i] != deviceCaptureBuffers[channelSelection.mainInputChannels()[i]] &&
mainCaptureBuffers[i] != zeroInputBuffer)
{
// Direct pointer, no copy needed (already set up in AllocateInputChannels)
}
}
// Main output: copy from main playback to device playback channels
for (size_t i = 0; i < mainPlaybackBuffers.size(); ++i)
{
int64_t deviceChannel = channelSelection.mainOutputChannels()[i];
if (deviceChannel >= 0 && (size_t)deviceChannel < devicePlaybackBuffers.size())
{
if (mainPlaybackBuffers[i] != devicePlaybackBuffers[deviceChannel])
{
// Mix copy: main playback buffer → device playback buffer
AddMixCopyOp(mainPlaybackBuffers[i], devicePlaybackBuffers[deviceChannel]);
}
}
}
}
// ----------------------------------------------------------------
// Channel position helper for SPA audio format
// ----------------------------------------------------------------
static void SetChannelPositions(spa_audio_info_raw &format, uint32_t channels)
{
switch (channels)
{
case 1:
format.position[0] = SPA_AUDIO_CHANNEL_MONO;
break;
case 2:
format.position[0] = SPA_AUDIO_CHANNEL_FL;
format.position[1] = SPA_AUDIO_CHANNEL_FR;
break;
case 3:
format.position[0] = SPA_AUDIO_CHANNEL_FL;
format.position[1] = SPA_AUDIO_CHANNEL_FR;
format.position[2] = SPA_AUDIO_CHANNEL_FC;
break;
case 4:
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;
break;
case 5:
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;
format.position[4] = SPA_AUDIO_CHANNEL_RL;
break;
case 6:
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;
format.position[4] = SPA_AUDIO_CHANNEL_RL;
format.position[5] = SPA_AUDIO_CHANNEL_RR;
break;
case 7:
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;
format.position[4] = SPA_AUDIO_CHANNEL_RL;
format.position[5] = SPA_AUDIO_CHANNEL_RR;
format.position[6] = SPA_AUDIO_CHANNEL_SL;
break;
case 8:
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;
format.position[4] = SPA_AUDIO_CHANNEL_RL;
format.position[5] = SPA_AUDIO_CHANNEL_RR;
format.position[6] = SPA_AUDIO_CHANNEL_SL;
format.position[7] = SPA_AUDIO_CHANNEL_SR;
break;
default:
// For > 8 channels, assign Aux channels
for (uint32_t i = 0; i < channels && i < SPA_AUDIO_MAX_CHANNELS; ++i)
{
format.position[i] = SPA_AUDIO_CHANNEL_AUX0 + i;
}
break;
}
}
};
// ----------------------------------------------------------------
// Factory function
// ----------------------------------------------------------------
AudioDriver *CreatePipeWireDriver(AudioDriverHost *driverHost)
{
return new PipeWireDriverImpl(driverHost);
}
} // namespace pipedal
+33
View File
@@ -0,0 +1,33 @@
/*
* MIT License
*
* Copyright (c) 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 "AudioDriver.hpp"
namespace pipedal {
AudioDriver* CreatePipeWireDriver(AudioDriverHost* driverHost);
}
+17
View File
@@ -29,6 +29,7 @@
#include "WebServerConfig.hpp"
#include <execinfo.h>
#include "PiPedalSocket.hpp"
#include "MixerEngine.hpp"
#include "PluginHost.hpp"
#include <boost/system/error_code.hpp>
#include <filesystem>
@@ -185,6 +186,7 @@ int main(int argc, char *argv[])
bool testExtraDevice = false;
std::string logLevel;
std::string portOption;
std::string driverOption = "alsa";
CommandLineParser parser;
parser.AddOption("-h", &help);
@@ -193,6 +195,7 @@ int main(int argc, char *argv[])
parser.AddOption("-log-level", &logLevel);
parser.AddOption("-port", &portOption);
parser.AddOption("-test-extra-device", &testExtraDevice); // advertise two different devices (for testing multi-device connect)
parser.AddOption("--driver", &driverOption);
try
{
@@ -226,6 +229,7 @@ int main(int argc, char *argv[])
<< " -systemd: Log to systemd journals instead of to the console.\n"
<< " -port: Port to listen on e.g. 80, or 0.0.0.0:80\n"
<< " -log-level: (debug|info|warning|error)\n"
<< " --driver: Audio driver (alsa|pipewire, default: alsa)\n"
<< "Example:\n"
<< " pipedald /etc/pipedal/config /etc/pipedal/react -port 80 \n"
"\n"
@@ -398,6 +402,7 @@ int main(int argc, char *argv[])
});
model.Init(configuration);
model.SetDriverType(driverOption);
// Get heavy IO out of the way before letting dependent (Jack/ALSA) services run.
model.LoadLv2PluginInfo();
@@ -447,6 +452,18 @@ int main(int argc, char *argv[])
model.Load();
// Initialize Band-in-a-Box mixer engine
{
auto mixerEngine = std::make_shared<MixerEngine>();
// Configure with current audio settings
auto jackSettings = model.GetJackServerSettings();
mixerEngine->setSampleRate((uint32_t)jackSettings.GetSampleRate());
mixerEngine->setMaxBufferSize((size_t)jackSettings.GetBufferSize());
mixerEngine->Activate();
model.SetMixerEngine(mixerEngine);
Lv2Log::info("MixerEngine initialized (BB-6)");
}
auto pipedalSocketFactory = MakePiPedalSocketFactory(model);
server->AddSocketFactory(pipedalSocketFactory);
@@ -1,6 +1,6 @@
// grant NetworkManager dbus permissions (no-prompts) to the pipedald systemd service.
// grant NetworkManager dbus permissions (no-prompts) to the oppedald systemd service.
polkit.addRule(function(action, subject) {
if (action.id.indexOf("org.freedesktop.NetworkManager.") == 0 && subject.isInGroup("pipedal_d")) {
if (action.id.indexOf("org.freedesktop.NetworkManager.") == 0 && subject.isInGroup("oppedal_d")) {
return polkit.Result.YES;
}
});
+1 -1
View File
@@ -5,7 +5,7 @@ interface=p2p-${WLAN}-*
port=0
dhcp-range=192.168.60.20,192.168.60.127,15m
domain=local
address=/pipedal-p2p.local/192.168.60.2
address=/oppedal-p2p.local/192.168.60.2
except-interface=eth0
except-interface=${WLAN}
+1 -1
View File
@@ -5,7 +5,7 @@ server=8.8.4.4
interface=p2p-wlan0-0
dhcp-range=192.168.60.3,192.168.60.127,15m
domain=local
address=/pipedal.local/192.168.60.2
address=/oppedal.local/192.168.60.2
except-interface=eth0
except-interface=wlan0
+2 -2
View File
@@ -1,9 +1,9 @@
# PiPedal dnsmasq settings for p2p
# OP-Pedal dnsmasq settings for p2p
#disable dns, dhcp only."
port=0
dhcp-range=192.168.60.3,192.168.60.127,15m
domain=local
address=/pipedal-p2p.local/192.168.60.2
address=/oppedal-p2p.local/192.168.60.2
# defensive. Not actually required.
except-interface=eth0
+1 -1
View File
@@ -3,7 +3,7 @@ Description=Jack Audio Daemon
After=sound.target
After=local-fs.target
After=dbus.socket
After=pipedald
After=oppedald
[Service]
@@ -1,5 +1,5 @@
[Unit]
Description=PiPedal P2P Session Manager
Description=OP-Pedal P2P Session Manager
After=network.target
After=nss-lookup.target
After=wpa_supplicant.service
@@ -11,7 +11,7 @@ StartLimitBurst=8
[Service]
ExecStart=${COMMAND} --systemd --loglevel info
Type=notify
WorkingDirectory=/var/pipedal
WorkingDirectory=/var/oppedal
Restart=on-failure
TimeoutStopSec=5
RestartSec=5s
+3 -3
View File
@@ -21,9 +21,9 @@ wifiChannel=${WIFI_CHANNEL}
# UI use only. service state is authoritative.
enabled=${ENABLED}
# P2P Device info
p2p_model_name=PiPedal
p2p_model_name=OP-Pedal
p2p_model_number=1
p2p_manufacturer="The PiPedal Project"
p2p_manufacturer="The OP-Pedal Project"
p2p_serial_number=1
p2p_device_type=1-0050F204-1
@@ -39,6 +39,6 @@ p2p_go_he=0
service_guid_file=${INSTANCE_ID_FILE}
# GUID identifying the PiPedal service
# GUID identifying the OP-Pedal service
# (if service_guid_file is not provided.)
service_guid=
+2 -2
View File
@@ -1,5 +1,5 @@
[Unit]
Description=PiPedal P2P Session Manager
Description=OP-Pedal P2P Session Manager
After=network.target
After=nss-lookup.target
After=wpa_supplicant.service
@@ -10,7 +10,7 @@ StartLimitBurst=8
[Service]
ExecStart=${COMMAND} --systemd --log-level info
Type=notify
WorkingDirectory=/var/pipedal
WorkingDirectory=/var/oppedal
TimeoutStopSec=5
Restart=on-failure
RestartSec=10s
+1 -1
View File
@@ -6,7 +6,7 @@ After=network.target
ExecStart=${COMMAND}
Restart=always
RestartSec=60
WorkingDirectory=/var/pipedal
WorkingDirectory=/var/oppedal
TimeoutStopSec=15
KillMode=process
+3 -3
View File
@@ -13,14 +13,14 @@ LimitMEMLOCK=infinity
LimitRTPRIO=95
Nice=-9
ExecStart=${COMMAND}
User=pipedal_d
Group=pipedal_d
User=oppedal_d
Group=oppedal_d
Restart=always
TimeoutStartSec=60
NotifyAccess=all
RestartSec=5
TimeoutStopSec=15
WorkingDirectory=/var/pipedal
WorkingDirectory=/var/oppedal
[Install]
+4 -4
View File
@@ -2,9 +2,9 @@
# CMake doesn't have an uninstall option, so remove files manually.
export PREFIX=/usr
rm $PREFIX/bin/pipedal*
rm $PREFIX/sbin/pipedal*
rm -rf /etc/pipedal/
rm -rf /var/pipedal/
rm $PREFIX/bin/oppedal*
rm $PREFIX/sbin/oppedal*
rm -rf /etc/oppedal/
rm -rf /var/oppedal/
rm -rf /usr/lib/lv2/ToobAmp.lv2/
+3 -2
View File
@@ -3,11 +3,12 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-visual" />
<meta name="theme-color" content="#000000" />
<meta name="color-scheme" content="dark light" /> <!-- uses media queries in web views. -->
<meta name="description" content="PiPedal Guitar Stomp Box for Raspberry Pi" />
<meta name="description" content="OP-Pedal Guitar Stomp Box for Raspberry Pi" />
<link rel="stylesheet" href="/css/roboto.css" />
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
@@ -120,7 +121,7 @@
</script>
<title>PiPedal</title>
<title>OP-Pedal</title>
</head>
<body>

Some files were not shown because too many files have changed in this diff Show More