Compare commits

1 Commits

Author SHA1 Message Date
sub 4d0ad127b5 Add shared memory daemon architecture and systemd service
Move ES-5 encoding from the CLAP plugin to a standalone PipeWire
daemon (es5d) communicating via POSIX shared memory. The plugin now
acts as a parameter/state frontend while es5d outputs directly to
the ES-9. Includes systemd user service for autostart after reboot
and an install script for local deployment.
2026-03-27 22:01:04 +01:00
10 changed files with 696 additions and 73 deletions
+13
View File
@@ -53,7 +53,20 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
) )
endif() endif()
# ES-5 daemon (PipeWire output)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
find_package(PkgConfig REQUIRED)
pkg_check_modules(PIPEWIRE REQUIRED libpipewire-0.3)
add_executable(es5d tools/es5d.cpp)
target_include_directories(es5d PRIVATE ${PIPEWIRE_INCLUDE_DIRS})
target_link_libraries(es5d PRIVATE ${PIPEWIRE_LIBRARIES} rt)
target_compile_options(es5d PRIVATE ${PIPEWIRE_CFLAGS_OTHER})
endif()
# Install to standard CLAP paths # Install to standard CLAP paths
if(CMAKE_SYSTEM_NAME STREQUAL "Linux") if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
install(TARGETS ${PROJECT_NAME} LIBRARY DESTINATION lib/clap) install(TARGETS ${PROJECT_NAME} LIBRARY DESTINATION lib/clap)
install(TARGETS es5d RUNTIME DESTINATION bin)
install(FILES dist/es5d.service DESTINATION lib/systemd/user)
endif() endif()
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=ES-5 Encoder Daemon (PipeWire)
Documentation=https://git.sub-net.at/submodular/es5-clap
After=pipewire.service
Requires=pipewire.service
[Service]
Type=simple
ExecStart=%h/.local/bin/es5d
Restart=on-failure
RestartSec=3
[Install]
WantedBy=default.target
Executable
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="${SCRIPT_DIR}/build"
# Build if needed
if [[ ! -f "${BUILD_DIR}/es5d" ]] || [[ ! -f "${BUILD_DIR}/es5-clap.clap" ]]; then
echo "Building..."
cmake -S "${SCRIPT_DIR}" -B "${BUILD_DIR}" -DCMAKE_BUILD_TYPE=Release
cmake --build "${BUILD_DIR}" -j"$(nproc)"
fi
# Install CLAP plugin
CLAP_DIR="${HOME}/.clap"
mkdir -p "${CLAP_DIR}"
install -m 755 "${BUILD_DIR}/es5-clap.clap" "${CLAP_DIR}/"
echo "Installed plugin to ${CLAP_DIR}/es5-clap.clap"
# Install daemon binary
install -Dm 755 "${BUILD_DIR}/es5d" "${HOME}/.local/bin/es5d"
echo "Installed daemon to ${HOME}/.local/bin/es5d"
# Install and enable systemd user service
SYSTEMD_DIR="${HOME}/.config/systemd/user"
mkdir -p "${SYSTEMD_DIR}"
install -m 644 "${SCRIPT_DIR}/dist/es5d.service" "${SYSTEMD_DIR}/"
systemctl --user daemon-reload
systemctl --user enable --now es5d.service
echo "Enabled and started es5d.service"
echo "Done. Check status with: systemctl --user status es5d"
+1 -1
View File
@@ -17,7 +17,7 @@ static const clap_plugin_descriptor_t s_desc = {
.description = "Encoder for Expert Sleepers ES-5 + ESX-8CV/ESX-8GT expanders. " .description = "Encoder for Expert Sleepers ES-5 + ESX-8CV/ESX-8GT expanders. "
"Generates encoded stereo audio for the ES-9 ADAT output.", "Generates encoded stereo audio for the ES-9 ADAT output.",
.features = (const char *[]){ .features = (const char *[]){
CLAP_PLUGIN_FEATURE_AUDIO_EFFECT, CLAP_PLUGIN_FEATURE_INSTRUMENT,
CLAP_PLUGIN_FEATURE_UTILITY, CLAP_PLUGIN_FEATURE_UTILITY,
CLAP_PLUGIN_FEATURE_STEREO, CLAP_PLUGIN_FEATURE_STEREO,
nullptr nullptr
+23
View File
@@ -0,0 +1,23 @@
// Shared memory layout between CLAP plugin and ES-5 daemon
// Copyright (c) 2026 Sub-Net e.U. — MIT License
#pragma once
#include <cstdint>
#include <atomic>
#define ES5_SHM_NAME "/es5_state"
#define ES5_NUM_HEADERS 6
#define ES5_CHANNELS_PER_HEADER 8
struct ES5SharedState {
// Header types: 0=off, 1=GT, 2=CV
std::atomic<uint8_t> header_types[ES5_NUM_HEADERS];
// Gate/CV values per header per channel
// GT: >0.5 = on, CV: 0.01.0 maps to full DAC range
std::atomic<float> values[ES5_NUM_HEADERS][ES5_CHANNELS_PER_HEADER];
// Flag: plugin is active
std::atomic<uint8_t> active;
};
+53 -66
View File
@@ -5,6 +5,10 @@
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <cmath> #include <cmath>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
ES5Plugin::ES5Plugin(const clap_plugin_descriptor *desc, const clap_host *host) ES5Plugin::ES5Plugin(const clap_plugin_descriptor *desc, const clap_host *host)
: Plugin(desc, host) : Plugin(desc, host)
@@ -12,12 +16,32 @@ ES5Plugin::ES5Plugin(const clap_plugin_descriptor *desc, const clap_host *host)
headerTypes_.fill(0.0); headerTypes_.fill(0.0);
for (auto &v : values_) for (auto &v : values_)
v.fill(0.0); v.fill(0.0);
// Open shared memory for daemon communication
int fd = shm_open(ES5_SHM_NAME, O_CREAT | O_RDWR, 0644);
if (fd >= 0) {
ftruncate(fd, sizeof(ES5SharedState));
shm_ = static_cast<ES5SharedState *>(
mmap(nullptr, sizeof(ES5SharedState), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0));
close(fd);
if (shm_ == MAP_FAILED)
shm_ = nullptr;
else
shm_->active.store(1, std::memory_order_relaxed);
}
}
ES5Plugin::~ES5Plugin() {
if (shm_) {
shm_->active.store(0, std::memory_order_relaxed);
munmap(shm_, sizeof(ES5SharedState));
}
} }
// --- Audio Ports --- // --- Audio Ports ---
uint32_t ES5Plugin::audioPortsCount(bool isInput) const noexcept { uint32_t ES5Plugin::audioPortsCount(bool isInput) const noexcept {
return isInput ? 0 : 1; // No input, 1 stereo output return isInput ? 0 : 1;
} }
bool ES5Plugin::audioPortsInfo(uint32_t index, bool isInput, bool ES5Plugin::audioPortsInfo(uint32_t index, bool isInput,
@@ -34,11 +58,10 @@ bool ES5Plugin::audioPortsInfo(uint32_t index, bool isInput,
// --- Parameters --- // --- Parameters ---
// Build a flat index → (paramId, header, channel) mapping
struct ParamEntry { struct ParamEntry {
clap_id id; clap_id id;
int header; // 05, or -1 for N/A int header;
int channel; // 07, or -1 for header type params int channel;
}; };
static ParamEntry paramEntryForIndex(uint32_t index) { static ParamEntry paramEntryForIndex(uint32_t index) {
@@ -63,7 +86,6 @@ bool ES5Plugin::paramsInfo(uint32_t paramIndex, clap_param_info *info) const noe
info->cookie = nullptr; info->cookie = nullptr;
if (entry.channel < 0) { if (entry.channel < 0) {
// Header type parameter
info->flags = CLAP_PARAM_IS_AUTOMATABLE | CLAP_PARAM_IS_STEPPED; info->flags = CLAP_PARAM_IS_AUTOMATABLE | CLAP_PARAM_IS_STEPPED;
snprintf(info->name, sizeof(info->name), "Header %d Type", entry.header + 1); snprintf(info->name, sizeof(info->name), "Header %d Type", entry.header + 1);
snprintf(info->module, sizeof(info->module), "Header %d", entry.header + 1); snprintf(info->module, sizeof(info->module), "Header %d", entry.header + 1);
@@ -71,7 +93,6 @@ bool ES5Plugin::paramsInfo(uint32_t paramIndex, clap_param_info *info) const noe
info->max_value = 2.0; info->max_value = 2.0;
info->default_value = 0.0; info->default_value = 0.0;
} else { } else {
// Value parameter (gate or CV depending on header type)
info->flags = CLAP_PARAM_IS_AUTOMATABLE; info->flags = CLAP_PARAM_IS_AUTOMATABLE;
snprintf(info->name, sizeof(info->name), "H%d Ch%d", entry.header + 1, entry.channel + 1); snprintf(info->name, sizeof(info->name), "H%d Ch%d", entry.header + 1, entry.channel + 1);
snprintf(info->module, sizeof(info->module), "Header %d", entry.header + 1); snprintf(info->module, sizeof(info->module), "Header %d", entry.header + 1);
@@ -84,12 +105,10 @@ bool ES5Plugin::paramsInfo(uint32_t paramIndex, clap_param_info *info) const noe
} }
bool ES5Plugin::paramsValue(clap_id paramId, double *value) noexcept { bool ES5Plugin::paramsValue(clap_id paramId, double *value) noexcept {
// Header type params: IDs 16
if (paramId >= 1 && paramId <= 6) { if (paramId >= 1 && paramId <= 6) {
*value = headerTypes_[paramId - 1]; *value = headerTypes_[paramId - 1];
return true; return true;
} }
// Value params: IDs x01x08 where x = 16
uint32_t h = (paramId / 100) - 1; uint32_t h = (paramId / 100) - 1;
uint32_t c = (paramId % 100) - 1; uint32_t c = (paramId % 100) - 1;
if (h < NUM_HEADERS && c < CHANNELS_PER_HEADER) { if (h < NUM_HEADERS && c < CHANNELS_PER_HEADER) {
@@ -137,12 +156,12 @@ void ES5Plugin::paramsFlush(const clap_input_events *in,
const clap_output_events *) noexcept { const clap_output_events *) noexcept {
for (uint32_t i = 0; i < in->size(in); ++i) for (uint32_t i = 0; i < in->size(in); ++i)
handleEvent(in->get(in, i)); handleEvent(in->get(in, i));
syncToSharedMemory();
} }
// --- State --- // --- State ---
bool ES5Plugin::stateSave(const clap_ostream *stream) noexcept { bool ES5Plugin::stateSave(const clap_ostream *stream) noexcept {
// Simple binary format: header types then all values
for (uint32_t h = 0; h < NUM_HEADERS; ++h) { for (uint32_t h = 0; h < NUM_HEADERS; ++h) {
double v = headerTypes_[h]; double v = headerTypes_[h];
if (stream->write(stream, &v, sizeof(v)) != sizeof(v)) return false; if (stream->write(stream, &v, sizeof(v)) != sizeof(v)) return false;
@@ -169,9 +188,7 @@ bool ES5Plugin::stateLoad(const clap_istream *stream) noexcept {
values_[h][c] = v; values_[h][c] = v;
} }
} }
// Reset encoder state on preset load syncToSharedMemory();
for (auto &enc : cvEncoders_)
enc.reset();
return true; return true;
} }
@@ -184,12 +201,10 @@ void ES5Plugin::handleEvent(const clap_event_header *hdr) {
auto ev = reinterpret_cast<const clap_event_param_value *>(hdr); auto ev = reinterpret_cast<const clap_event_param_value *>(hdr);
clap_id id = ev->param_id; clap_id id = ev->param_id;
// Header type
if (id >= 1 && id <= 6) { if (id >= 1 && id <= 6) {
headerTypes_[id - 1] = ev->value; headerTypes_[id - 1] = ev->value;
return; return;
} }
// Value
uint32_t h = (id / 100) - 1; uint32_t h = (id / 100) - 1;
uint32_t c = (id % 100) - 1; uint32_t c = (id % 100) - 1;
if (h < NUM_HEADERS && c < CHANNELS_PER_HEADER) { if (h < NUM_HEADERS && c < CHANNELS_PER_HEADER) {
@@ -197,64 +212,36 @@ void ES5Plugin::handleEvent(const clap_event_header *hdr) {
} }
} }
void ES5Plugin::syncToSharedMemory() {
if (!shm_) return;
for (uint32_t h = 0; h < NUM_HEADERS; ++h) {
shm_->header_types[h].store(
static_cast<uint8_t>(headerTypes_[h] + 0.5),
std::memory_order_relaxed);
for (uint32_t c = 0; c < CHANNELS_PER_HEADER; ++c) {
shm_->values[h][c].store(
static_cast<float>(values_[h][c]),
std::memory_order_relaxed);
}
}
}
clap_process_status ES5Plugin::process(const clap_process *process) noexcept { clap_process_status ES5Plugin::process(const clap_process *process) noexcept {
const uint32_t nframes = process->frames_count; const uint32_t nframes = process->frames_count;
const uint32_t nev = process->in_events->size(process->in_events); const uint32_t nev = process->in_events->size(process->in_events);
uint32_t ev_index = 0;
uint32_t next_ev_frame = (nev > 0) ? 0 : nframes;
// Process parameter events
for (uint32_t i = 0; i < nev; ++i)
handleEvent(process->in_events->get(process->in_events, i));
// Sync state to shared memory for the daemon
syncToSharedMemory();
// Output silence — the daemon handles actual audio output via PipeWire
float *outL = process->audio_outputs[0].data32[0]; float *outL = process->audio_outputs[0].data32[0];
float *outR = process->audio_outputs[0].data32[1]; float *outR = process->audio_outputs[0].data32[1];
memset(outL, 0, nframes * sizeof(float));
for (uint32_t i = 0; i < nframes;) { memset(outR, 0, nframes * sizeof(float));
// Process events at this sample
while (ev_index < nev && next_ev_frame == i) {
auto hdr = process->in_events->get(process->in_events, ev_index);
if (hdr->time != i) {
next_ev_frame = hdr->time;
break;
}
handleEvent(hdr);
++ev_index;
if (ev_index == nev) {
next_ev_frame = nframes;
break;
}
}
// Generate encoded audio
for (; i < next_ev_frame; ++i) {
uint8_t headers[6] = {};
for (uint32_t h = 0; h < NUM_HEADERS; ++h) {
auto type = static_cast<HeaderType>(
static_cast<int>(headerTypes_[h] + 0.5));
switch (type) {
case HeaderType::GT: {
bool gates[8];
for (int c = 0; c < 8; ++c)
gates[c] = values_[h][c] > 0.5;
headers[h] = gtEncoders_[h].encode(gates);
break;
}
case HeaderType::CV: {
// Map 0.01.0 → -2048..+2047
double cvs[8];
for (int c = 0; c < 8; ++c)
cvs[c] = values_[h][c] * 4095.0 - 2048.0;
headers[h] = cvEncoders_[h].encode(cvs);
break;
}
default:
headers[h] = 0;
break;
}
}
es5Encoder_.encode(headers, outL[i], outR[i]);
}
}
return CLAP_PROCESS_CONTINUE; return CLAP_PROCESS_CONTINUE;
} }
+20 -6
View File
@@ -9,6 +9,7 @@
#include <clap/helpers/host-proxy.hxx> #include <clap/helpers/host-proxy.hxx>
#include <array> #include <array>
#include "encoders.h" #include "encoders.h"
#include "es5_shared.h"
// Header type enum (matches parameter stepped values) // Header type enum (matches parameter stepped values)
enum class HeaderType : int { Off = 0, GT = 1, CV = 2 }; enum class HeaderType : int { Off = 0, GT = 1, CV = 2 };
@@ -35,6 +36,7 @@ class ES5Plugin : public clap::helpers::Plugin<
{ {
public: public:
ES5Plugin(const clap_plugin_descriptor *desc, const clap_host *host); ES5Plugin(const clap_plugin_descriptor *desc, const clap_host *host);
~ES5Plugin();
// --- Audio Ports --- // --- Audio Ports ---
bool implementsAudioPorts() const noexcept override { return true; } bool implementsAudioPorts() const noexcept override { return true; }
@@ -42,6 +44,19 @@ public:
bool audioPortsInfo(uint32_t index, bool isInput, bool audioPortsInfo(uint32_t index, bool isInput,
clap_audio_port_info *info) const noexcept override; clap_audio_port_info *info) const noexcept override;
// --- Note Ports (required for Bitwig to treat as instrument) ---
bool implementsNotePorts() const noexcept override { return true; }
uint32_t notePortsCount(bool isInput) const noexcept override { return isInput ? 1 : 0; }
bool notePortsInfo(uint32_t index, bool isInput,
clap_note_port_info *info) const noexcept override {
if (!isInput || index != 0) return false;
info->id = 0;
info->supported_dialects = CLAP_NOTE_DIALECT_MIDI;
info->preferred_dialect = CLAP_NOTE_DIALECT_MIDI;
snprintf(info->name, sizeof(info->name), "MIDI In");
return true;
}
// --- Parameters --- // --- Parameters ---
bool implementsParams() const noexcept override { return true; } bool implementsParams() const noexcept override { return true; }
uint32_t paramsCount() const noexcept override; uint32_t paramsCount() const noexcept override;
@@ -64,13 +79,12 @@ public:
private: private:
void handleEvent(const clap_event_header *hdr); void handleEvent(const clap_event_header *hdr);
void syncToSharedMemory();
// Parameter storage // Parameter storage
std::array<double, NUM_HEADERS> headerTypes_{}; // 0=off, 1=gt, 2=cv std::array<double, NUM_HEADERS> headerTypes_{};
std::array<std::array<double, CHANNELS_PER_HEADER>, NUM_HEADERS> values_{}; // 0.01.0 std::array<std::array<double, CHANNELS_PER_HEADER>, NUM_HEADERS> values_{};
// Encoder instances // Shared memory for daemon communication
es5::ES5Encoder es5Encoder_; ES5SharedState *shm_ = nullptr;
std::array<es5::ESX8GTEncoder, NUM_HEADERS> gtEncoders_;
std::array<es5::ESX8CVEncoder, NUM_HEADERS> cvEncoders_;
}; };
+171
View File
@@ -0,0 +1,171 @@
// ES-5 PipeWire test — outputs encoded gate+CV pattern via PipeWire
// Tests if PipeWire introduces corruption (like Bitwig does)
// Usage: es5_pw_test
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <cmath>
#include <csignal>
#include <algorithm>
#include <pipewire/pipewire.h>
#include <spa/param/audio/format-utils.h>
static volatile bool running = true;
static void sighandler(int) { running = false; }
// --- ES-5 encoder (from Expert Sleepers MIT source) ---
static float bitsToFloat(int32_t bits) {
if (bits & 0x800000)
return static_cast<float>(0xFFFFFF & (-bits)) / -8388608.0f;
else
return static_cast<float>(bits) / 8388608.0f;
}
struct ESX8CVEncoder {
int phase = 0;
uint32_t value = 0;
uint8_t encode(const double values[8]) {
int p = phase & ~1;
int state = (p >> 1) & 3;
int dac = (p >> 3) & 7;
if (state == 0)
value = 2048 + static_cast<int32_t>(std::max(-2048.0, std::min(2047.0, values[dac])));
phase += 2;
if ((phase & 7) == 6) phase += 2;
phase &= 63;
uint32_t out;
if (state == 0) out = 0x80 | (value & 0x1F);
else if (state == 1) out = (value >> 5) & 0x1F;
else out = ((dac > 3) ? 0x40 : 0x20) | (value >> 10) | ((dac & 3) << 2);
return static_cast<uint8_t>(out);
}
};
struct AppData {
pw_main_loop *loop;
pw_stream *stream;
uint32_t sample_count;
uint32_t total_samples;
bool gates_on;
ESX8CVEncoder cv_encoder;
};
static void on_process(void *userdata) {
auto *app = static_cast<AppData *>(userdata);
pw_buffer *b = pw_stream_dequeue_buffer(app->stream);
if (!b) return;
spa_buffer *buf = b->buffer;
float *dst = static_cast<float *>(buf->datas[0].data);
if (!dst) return;
uint32_t n_frames = buf->datas[0].maxsize / (sizeof(float) * 2);
if (b->requested && b->requested < n_frames)
n_frames = b->requested;
for (uint32_t f = 0; f < n_frames; ++f) {
if (++app->sample_count >= 48000) {
app->sample_count = 0;
app->gates_on = !app->gates_on;
fprintf(stderr, " Header 1 gates: %s\n", app->gates_on ? "ALL ON " : "ALL OFF");
}
app->total_samples++;
uint8_t h1 = app->gates_on ? 0xFF : 0x00;
double cvs[8];
for (int c = 0; c < 8; ++c) {
double t = (app->total_samples + c * 48000) / (double)(48000 * 8);
cvs[c] = sin(t * 3.14159265) * 2047.0;
}
uint8_t h6 = app->cv_encoder.encode(cvs);
int32_t bitsL = h1 << 16;
int32_t bitsR = h6; // headers 4,5 = 0, header 6 = cv byte
dst[f * 2 + 0] = bitsToFloat(bitsL);
dst[f * 2 + 1] = bitsToFloat(bitsR);
}
buf->datas[0].chunk->offset = 0;
buf->datas[0].chunk->stride = sizeof(float) * 2;
buf->datas[0].chunk->size = n_frames * sizeof(float) * 2;
pw_stream_queue_buffer(app->stream, b);
}
static const pw_stream_events stream_events = {
.version = PW_VERSION_STREAM_EVENTS,
.process = on_process,
};
int main(int argc, char *argv[]) {
setbuf(stderr, nullptr);
signal(SIGINT, sighandler);
pw_init(&argc, &argv);
AppData app{};
app.loop = pw_main_loop_new(nullptr);
auto *props = pw_properties_new(
PW_KEY_MEDIA_TYPE, "Audio",
PW_KEY_MEDIA_CATEGORY, "Playback",
PW_KEY_MEDIA_ROLE, "Production",
PW_KEY_NODE_NAME, "es5-encoder-test",
PW_KEY_NODE_DESCRIPTION, "ES-5 Encoder Test",
// Target the ES-9 multichannel output
PW_KEY_TARGET_OBJECT, "alsa_output.usb-Expert_Sleepers_Ltd_ES-9-01.multichannel-output",
PW_KEY_NODE_WANT_DRIVER, "true",
nullptr
);
app.stream = pw_stream_new_simple(
pw_main_loop_get_loop(app.loop),
"es5-test",
props,
&stream_events,
&app
);
// Set up format: stereo F32, 48kHz
// Channel positions: AUX12, AUX13 (ES-5 L/R on the ES-9)
uint8_t format_buf[1024];
spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(format_buf, sizeof(format_buf));
spa_audio_info_raw audio_info = {};
audio_info.format = SPA_AUDIO_FORMAT_F32;
audio_info.rate = 48000;
audio_info.channels = 2;
audio_info.position[0] = SPA_AUDIO_CHANNEL_AUX12;
audio_info.position[1] = SPA_AUDIO_CHANNEL_AUX13;
const spa_pod *params[1];
params[0] = spa_format_audio_raw_build(&pod_builder, SPA_PARAM_EnumFormat, &audio_info);
pw_stream_connect(
app.stream,
PW_DIRECTION_OUTPUT,
PW_ID_ANY,
static_cast<pw_stream_flags>(
PW_STREAM_FLAG_AUTOCONNECT |
PW_STREAM_FLAG_MAP_BUFFERS |
PW_STREAM_FLAG_RT_PROCESS),
params, 1
);
fprintf(stderr, "ES-5 PipeWire test: targeting ES-9 AUX12/AUX13\n");
fprintf(stderr, "Header 1: GT (toggling), Header 6: CV (sweep)\n");
fprintf(stderr, "Headers 2-5: must stay OFF. Ctrl+C to stop.\n\n");
pw_main_loop_run(app.loop);
pw_stream_destroy(app.stream);
pw_main_loop_destroy(app.loop);
pw_deinit();
return 0;
}
+151
View File
@@ -0,0 +1,151 @@
// Direct ES-5 test — outputs encoded gate pattern via ALSA, bypassing Bitwig
// Tests header 1 (GT, all gates toggling) + header 6 (CV encoder running)
// Headers 2-5 should remain silent — any activity = encoding leak
// Usage: es5_test [alsa_device] [channel_offset]
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <cmath>
#include <csignal>
#include <algorithm>
#include <alsa/asoundlib.h>
static volatile bool running = true;
static void sighandler(int) { running = false; }
// --- ESX-8CV encoder (from Expert Sleepers MIT-licensed source) ---
struct ESX8CVEncoder {
int phase = 0;
uint32_t value = 0;
uint8_t encode(const double values[8]) {
int p = phase & ~1;
int state = (p >> 1) & 3;
int dac = (p >> 3) & 7;
if (state == 0) {
double s = values[dac];
value = 2048 + static_cast<int32_t>(
std::max(-2048.0, std::min(2047.0, s)));
}
phase += 2;
if ((phase & 7) == 6)
phase += 2;
phase &= 63;
uint32_t out;
if (state == 0)
out = 0x80 | (value & 0x1F);
else if (state == 1)
out = (value >> 5) & 0x1F;
else
out = ((dac > 3) ? 0x40 : 0x20) | (value >> 10) | ((dac & 3) << 2);
return static_cast<uint8_t>(out);
}
};
// Pack 24-bit integer into 3 bytes (little-endian)
static void pack_s24_3le(uint8_t *dst, int32_t val) {
uint32_t u = static_cast<uint32_t>(val) & 0xFFFFFF;
dst[0] = u & 0xFF;
dst[1] = (u >> 8) & 0xFF;
dst[2] = (u >> 16) & 0xFF;
}
int main(int argc, char *argv[]) {
setbuf(stdout, nullptr);
const char *device = argc > 1 ? argv[1] : "hw:7";
int ch_offset = argc > 2 ? atoi(argv[2]) : 12;
const unsigned rate = 48000;
const unsigned channels = 16;
signal(SIGINT, sighandler);
snd_pcm_t *pcm = nullptr;
int err = snd_pcm_open(&pcm, device, SND_PCM_STREAM_PLAYBACK, 0);
if (err < 0) {
fprintf(stderr, "Cannot open %s: %s\n", device, snd_strerror(err));
return 1;
}
snd_pcm_hw_params_t *hw;
snd_pcm_hw_params_alloca(&hw);
snd_pcm_hw_params_any(pcm, hw);
snd_pcm_hw_params_set_access(pcm, hw, SND_PCM_ACCESS_RW_INTERLEAVED);
snd_pcm_hw_params_set_format(pcm, hw, SND_PCM_FORMAT_S24_3LE);
snd_pcm_hw_params_set_channels(pcm, hw, channels);
unsigned actual_rate = rate;
snd_pcm_hw_params_set_rate_near(pcm, hw, &actual_rate, 0);
snd_pcm_uframes_t period = 1024;
snd_pcm_hw_params_set_period_size_near(pcm, hw, &period, 0);
if ((err = snd_pcm_hw_params(pcm, hw)) < 0) {
fprintf(stderr, "Cannot set hw params: %s\n", snd_strerror(err));
return 1;
}
printf("ES-5 test: device=%s rate=%u channels=%u ES-5_L/R=ch%d/%d\n",
device, actual_rate, channels, ch_offset, ch_offset + 1);
printf("Header 1: GT (toggling all gates every second)\n");
printf("Header 6: CV (sweeping all 8 CVs)\n");
printf("Headers 2-5: should be OFF (any activity = leak!)\n");
printf("Ctrl+C to stop.\n\n");
int buf_frame_bytes = channels * 3;
uint8_t *buf = new uint8_t[period * buf_frame_bytes]();
uint32_t sample_count = 0;
uint32_t total_samples = 0;
bool gates_on = false;
ESX8CVEncoder cv_encoder;
while (running) {
for (unsigned f = 0; f < period; ++f) {
if (++sample_count >= rate) {
sample_count = 0;
gates_on = !gates_on;
printf(" Header 1 gates: %s | CV sweep: %.0f%%\n",
gates_on ? "ALL ON " : "ALL OFF",
fmod(total_samples / (double)rate, 8.0) / 8.0 * 100.0);
}
total_samples++;
// Header 1: GT
uint8_t h1 = gates_on ? 0xFF : 0x00;
// Header 6: CV — sweep each channel slowly
double cvs[8];
for (int c = 0; c < 8; ++c) {
double t = (total_samples + c * rate) / (double)(rate * 8);
cvs[c] = sin(t * 3.14159265) * 2047.0; // ±2047 sweep
}
uint8_t h6 = cv_encoder.encode(cvs);
// Headers 2-5: OFF (must stay 0x00)
uint8_t h2 = 0, h3 = 0, h4 = 0, h5 = 0;
int32_t bitsL = (h1 << 16) | (h2 << 8) | h3;
int32_t bitsR = (h4 << 16) | (h5 << 8) | h6;
uint8_t *frame = &buf[f * buf_frame_bytes];
memset(frame, 0, buf_frame_bytes);
pack_s24_3le(&frame[ch_offset * 3], bitsL);
pack_s24_3le(&frame[(ch_offset + 1) * 3], bitsR);
}
snd_pcm_sframes_t written = snd_pcm_writei(pcm, buf, period);
if (written < 0) {
fprintf(stderr, " Write error: %s, recovering...\n", snd_strerror(written));
snd_pcm_recover(pcm, written, 0);
}
}
delete[] buf;
snd_pcm_close(pcm);
printf("\nDone.\n");
return 0;
}
+218
View File
@@ -0,0 +1,218 @@
// ES-5 Daemon — reads gate/CV state from shared memory, outputs encoded
// audio via PipeWire directly to the ES-9, bypassing Bitwig's audio engine.
// Copyright (c) 2026 Sub-Net e.U. — MIT License
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <cmath>
#include <csignal>
#include <algorithm>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <pipewire/pipewire.h>
#include <spa/param/audio/format-utils.h>
#include "../src/es5_shared.h"
#include "../src/encoders.h"
static pw_main_loop *loop = nullptr;
static void sighandler(int) {
if (loop) pw_main_loop_quit(loop);
}
struct AppData {
ES5SharedState *shm;
es5::ES5Encoder es5_enc;
es5::ESX8GTEncoder gt_enc[ES5_NUM_HEADERS];
es5::ESX8CVEncoder cv_enc[ES5_NUM_HEADERS];
};
static void on_process(void *userdata) {
auto *app = static_cast<AppData *>(userdata);
auto *shm = app->shm;
pw_buffer *b = pw_stream_dequeue_buffer(static_cast<pw_stream *>(
// stored in userdata via closure — we'll use a different approach
nullptr));
// This won't work — need stream reference. Use struct instead.
}
// We need the stream in the callback, so use a full struct
struct DaemonData {
pw_main_loop *loop;
pw_stream *stream;
ES5SharedState *shm;
es5::ES5Encoder es5_enc;
es5::ESX8GTEncoder gt_enc[ES5_NUM_HEADERS];
es5::ESX8CVEncoder cv_enc[ES5_NUM_HEADERS];
uint32_t debug_count = 0;
FILE *debug_file = nullptr;
};
static void daemon_process(void *userdata) {
auto *d = static_cast<DaemonData *>(userdata);
pw_buffer *b = pw_stream_dequeue_buffer(d->stream);
if (!b) return;
spa_buffer *buf = b->buffer;
float *dst = static_cast<float *>(buf->datas[0].data);
if (!dst) return;
uint32_t n_frames = buf->datas[0].maxsize / (sizeof(float) * 2);
if (b->requested && b->requested < n_frames)
n_frames = b->requested;
for (uint32_t f = 0; f < n_frames; ++f) {
uint8_t headers[6] = {};
for (int h = 0; h < ES5_NUM_HEADERS; ++h) {
uint8_t type = d->shm->header_types[h].load(std::memory_order_relaxed);
if (type == 1) { // GT
bool gates[8];
for (int c = 0; c < 8; ++c)
gates[c] = d->shm->values[h][c].load(std::memory_order_relaxed) > 0.5f;
headers[h] = d->gt_enc[h].encode(gates);
} else if (type == 2) { // CV
double cvs[8];
for (int c = 0; c < 8; ++c)
cvs[c] = d->shm->values[h][c].load(std::memory_order_relaxed) * 4095.0 - 2048.0;
headers[h] = d->cv_enc[h].encode(cvs);
}
}
float L, R;
d->es5_enc.encode(headers, L, R);
dst[f * 2 + 0] = L;
dst[f * 2 + 1] = R;
// Debug: dump first 48000 samples
if (d->debug_count < 48000) {
if (!d->debug_file)
d->debug_file = fopen("/tmp/es5d_debug.raw", "wb");
if (d->debug_file) {
fwrite(&L, 4, 1, d->debug_file);
fwrite(&R, 4, 1, d->debug_file);
fwrite(headers, 1, 6, d->debug_file);
// Also write shm header types
uint8_t types[6];
for (int i = 0; i < 6; ++i)
types[i] = d->shm->header_types[i].load(std::memory_order_relaxed);
fwrite(types, 1, 6, d->debug_file);
d->debug_count++;
if (d->debug_count == 48000) {
fclose(d->debug_file);
d->debug_file = nullptr;
fprintf(stderr, "es5d: debug dump written to /tmp/es5d_debug.raw\n");
}
}
}
}
buf->datas[0].chunk->offset = 0;
buf->datas[0].chunk->stride = sizeof(float) * 2;
buf->datas[0].chunk->size = n_frames * sizeof(float) * 2;
pw_stream_queue_buffer(d->stream, b);
}
static const pw_stream_events stream_events = {
.version = PW_VERSION_STREAM_EVENTS,
.process = daemon_process,
};
int main(int argc, char *argv[]) {
setbuf(stderr, nullptr);
// Open or create shared memory
int fd = shm_open(ES5_SHM_NAME, O_CREAT | O_RDWR, 0644);
if (fd < 0) {
perror("shm_open");
return 1;
}
ftruncate(fd, sizeof(ES5SharedState));
auto *shm = static_cast<ES5SharedState *>(
mmap(nullptr, sizeof(ES5SharedState), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0));
close(fd);
if (shm == MAP_FAILED) {
perror("mmap");
return 1;
}
// Initialize shared memory
memset(shm, 0, sizeof(ES5SharedState));
pw_init(&argc, &argv);
DaemonData data{};
data.shm = shm;
data.loop = pw_main_loop_new(nullptr);
loop = data.loop;
signal(SIGINT, sighandler);
signal(SIGTERM, sighandler);
auto *props = pw_properties_new(
PW_KEY_MEDIA_TYPE, "Audio",
PW_KEY_MEDIA_CATEGORY, "Playback",
PW_KEY_MEDIA_ROLE, "Production",
PW_KEY_NODE_NAME, "es5-encoder",
PW_KEY_NODE_DESCRIPTION, "ES-5 Encoder (Direct)",
PW_KEY_TARGET_OBJECT, "alsa_output.usb-Expert_Sleepers_Ltd_ES-9-01.multichannel-output",
PW_KEY_NODE_WANT_DRIVER, "true",
PW_KEY_NODE_ALWAYS_PROCESS, "true",
nullptr
);
data.stream = pw_stream_new_simple(
pw_main_loop_get_loop(data.loop),
"es5-encoder",
props,
&stream_events,
&data
);
uint8_t format_buf[1024];
spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(format_buf, sizeof(format_buf));
spa_audio_info_raw audio_info = {};
audio_info.format = SPA_AUDIO_FORMAT_F32;
audio_info.rate = 48000;
audio_info.channels = 2;
audio_info.position[0] = SPA_AUDIO_CHANNEL_AUX12;
audio_info.position[1] = SPA_AUDIO_CHANNEL_AUX13;
const spa_pod *params[1];
params[0] = spa_format_audio_raw_build(&pod_builder, SPA_PARAM_EnumFormat, &audio_info);
pw_stream_connect(
data.stream,
PW_DIRECTION_OUTPUT,
PW_ID_ANY,
static_cast<pw_stream_flags>(
PW_STREAM_FLAG_AUTOCONNECT |
PW_STREAM_FLAG_MAP_BUFFERS |
PW_STREAM_FLAG_RT_PROCESS),
params, 1
);
fprintf(stderr, "es5d: running — PipeWire → ES-9 AUX12/AUX13\n");
fprintf(stderr, "es5d: shared memory at " ES5_SHM_NAME "\n");
fprintf(stderr, "es5d: waiting for CLAP plugin to set header types...\n");
pw_main_loop_run(data.loop);
fprintf(stderr, "\nes5d: shutting down\n");
pw_stream_destroy(data.stream);
pw_main_loop_destroy(data.loop);
pw_deinit();
munmap(shm, sizeof(ES5SharedState));
return 0;
}