4d0ad127b5
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.
152 lines
4.8 KiB
C++
152 lines
4.8 KiB
C++
// 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;
|
|
}
|