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.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user