// ES-5 PipeWire test — outputs encoded gate+CV pattern via PipeWire // Tests if PipeWire introduces corruption (like Bitwig does) // Usage: es5_pw_test #include #include #include #include #include #include #include #include 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(0xFFFFFF & (-bits)) / -8388608.0f; else return static_cast(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(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(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(userdata); pw_buffer *b = pw_stream_dequeue_buffer(app->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) { 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_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; }