// 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 #include #include #include #include #include #include #include #include #include #include #include #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(userdata); auto *shm = app->shm; pw_buffer *b = pw_stream_dequeue_buffer(static_cast( // 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(userdata); pw_buffer *b = pw_stream_dequeue_buffer(d->stream); if (!b) return; spa_buffer *buf = b->buffer; float *dst = static_cast(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( 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_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; }