6a117a9ba9
Implements the full encoding pipeline for Expert Sleepers ES-5 with ESX-8GT (gate) and ESX-8CV (CV) expanders as a native CLAP plugin. Encoder logic ported from Expert Sleepers' MIT-licensed Pure Data externals (https://github.com/expertsleepersltd/externals). - ES-5 encoder: packs 6 × 8-bit header values into stereo 24-bit audio - ESX-8GT encoder: 8 boolean gates → 1 byte - ESX-8CV encoder: 8 × 12-bit CV values via 64-phase state machine - 54 automatable parameters (6 header types + 48 gate/CV values) - State save/load for preset support - Builds on Linux (tested), macOS, and Windows
261 lines
8.4 KiB
C++
261 lines
8.4 KiB
C++
// ES-5 CLAP Plugin implementation
|
||
// Copyright (c) 2026 Sub-Net e.U. — MIT License
|
||
|
||
#include "plugin.h"
|
||
#include <cstdio>
|
||
#include <cstring>
|
||
#include <cmath>
|
||
|
||
ES5Plugin::ES5Plugin(const clap_plugin_descriptor *desc, const clap_host *host)
|
||
: Plugin(desc, host)
|
||
{
|
||
headerTypes_.fill(0.0);
|
||
for (auto &v : values_)
|
||
v.fill(0.0);
|
||
}
|
||
|
||
// --- Audio Ports ---
|
||
|
||
uint32_t ES5Plugin::audioPortsCount(bool isInput) const noexcept {
|
||
return isInput ? 0 : 1; // No input, 1 stereo output
|
||
}
|
||
|
||
bool ES5Plugin::audioPortsInfo(uint32_t index, bool isInput,
|
||
clap_audio_port_info *info) const noexcept {
|
||
if (isInput || index != 0) return false;
|
||
info->id = 0;
|
||
snprintf(info->name, sizeof(info->name), "ES-5 Output");
|
||
info->channel_count = 2;
|
||
info->flags = CLAP_AUDIO_PORT_IS_MAIN;
|
||
info->port_type = CLAP_PORT_STEREO;
|
||
info->in_place_pair = CLAP_INVALID_ID;
|
||
return true;
|
||
}
|
||
|
||
// --- Parameters ---
|
||
|
||
// Build a flat index → (paramId, header, channel) mapping
|
||
struct ParamEntry {
|
||
clap_id id;
|
||
int header; // 0–5, or -1 for N/A
|
||
int channel; // 0–7, or -1 for header type params
|
||
};
|
||
|
||
static ParamEntry paramEntryForIndex(uint32_t index) {
|
||
if (index < NUM_HEADERS) {
|
||
return {headerTypeParamId(index), static_cast<int>(index), -1};
|
||
}
|
||
uint32_t vi = index - NUM_HEADERS;
|
||
uint32_t h = vi / CHANNELS_PER_HEADER;
|
||
uint32_t c = vi % CHANNELS_PER_HEADER;
|
||
return {headerValueParamId(h, c), static_cast<int>(h), static_cast<int>(c)};
|
||
}
|
||
|
||
uint32_t ES5Plugin::paramsCount() const noexcept {
|
||
return TOTAL_PARAMS;
|
||
}
|
||
|
||
bool ES5Plugin::paramsInfo(uint32_t paramIndex, clap_param_info *info) const noexcept {
|
||
if (paramIndex >= TOTAL_PARAMS) return false;
|
||
|
||
auto entry = paramEntryForIndex(paramIndex);
|
||
info->id = entry.id;
|
||
info->cookie = nullptr;
|
||
|
||
if (entry.channel < 0) {
|
||
// Header type parameter
|
||
info->flags = CLAP_PARAM_IS_AUTOMATABLE | CLAP_PARAM_IS_STEPPED;
|
||
snprintf(info->name, sizeof(info->name), "Header %d Type", entry.header + 1);
|
||
snprintf(info->module, sizeof(info->module), "Header %d", entry.header + 1);
|
||
info->min_value = 0.0;
|
||
info->max_value = 2.0;
|
||
info->default_value = 0.0;
|
||
} else {
|
||
// Value parameter (gate or CV depending on header type)
|
||
info->flags = CLAP_PARAM_IS_AUTOMATABLE;
|
||
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);
|
||
info->min_value = 0.0;
|
||
info->max_value = 1.0;
|
||
info->default_value = 0.0;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
bool ES5Plugin::paramsValue(clap_id paramId, double *value) noexcept {
|
||
// Header type params: IDs 1–6
|
||
if (paramId >= 1 && paramId <= 6) {
|
||
*value = headerTypes_[paramId - 1];
|
||
return true;
|
||
}
|
||
// Value params: IDs x01–x08 where x = 1–6
|
||
uint32_t h = (paramId / 100) - 1;
|
||
uint32_t c = (paramId % 100) - 1;
|
||
if (h < NUM_HEADERS && c < CHANNELS_PER_HEADER) {
|
||
*value = values_[h][c];
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool ES5Plugin::paramsValueToText(clap_id paramId, double value,
|
||
char *display, uint32_t size) noexcept {
|
||
if (paramId >= 1 && paramId <= 6) {
|
||
int v = static_cast<int>(value + 0.5);
|
||
const char *names[] = {"Off", "GT (Gate)", "CV"};
|
||
snprintf(display, size, "%s", names[std::max(0, std::min(2, v))]);
|
||
return true;
|
||
}
|
||
|
||
uint32_t h = (paramId / 100) - 1;
|
||
if (h < NUM_HEADERS) {
|
||
auto type = static_cast<HeaderType>(static_cast<int>(headerTypes_[h] + 0.5));
|
||
if (type == HeaderType::GT)
|
||
snprintf(display, size, "%s", value > 0.5 ? "ON" : "OFF");
|
||
else if (type == HeaderType::CV)
|
||
snprintf(display, size, "%.1f%%", value * 100.0);
|
||
else
|
||
snprintf(display, size, "%.2f", value);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool ES5Plugin::paramsTextToValue(clap_id paramId, const char *display,
|
||
double *value) noexcept {
|
||
if (paramId >= 1 && paramId <= 6) {
|
||
if (!strcmp(display, "Off") || !strcmp(display, "off")) { *value = 0; return true; }
|
||
if (!strcmp(display, "GT") || !strcmp(display, "gt")) { *value = 1; return true; }
|
||
if (!strcmp(display, "CV") || !strcmp(display, "cv")) { *value = 2; return true; }
|
||
}
|
||
*value = atof(display);
|
||
return true;
|
||
}
|
||
|
||
void ES5Plugin::paramsFlush(const clap_input_events *in,
|
||
const clap_output_events *) noexcept {
|
||
for (uint32_t i = 0; i < in->size(in); ++i)
|
||
handleEvent(in->get(in, i));
|
||
}
|
||
|
||
// --- State ---
|
||
|
||
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) {
|
||
double v = headerTypes_[h];
|
||
if (stream->write(stream, &v, sizeof(v)) != sizeof(v)) return false;
|
||
}
|
||
for (uint32_t h = 0; h < NUM_HEADERS; ++h) {
|
||
for (uint32_t c = 0; c < CHANNELS_PER_HEADER; ++c) {
|
||
double v = values_[h][c];
|
||
if (stream->write(stream, &v, sizeof(v)) != sizeof(v)) return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
bool ES5Plugin::stateLoad(const clap_istream *stream) noexcept {
|
||
for (uint32_t h = 0; h < NUM_HEADERS; ++h) {
|
||
double v;
|
||
if (stream->read(stream, &v, sizeof(v)) != sizeof(v)) return false;
|
||
headerTypes_[h] = v;
|
||
}
|
||
for (uint32_t h = 0; h < NUM_HEADERS; ++h) {
|
||
for (uint32_t c = 0; c < CHANNELS_PER_HEADER; ++c) {
|
||
double v;
|
||
if (stream->read(stream, &v, sizeof(v)) != sizeof(v)) return false;
|
||
values_[h][c] = v;
|
||
}
|
||
}
|
||
// Reset encoder state on preset load
|
||
for (auto &enc : cvEncoders_)
|
||
enc.reset();
|
||
return true;
|
||
}
|
||
|
||
// --- Process ---
|
||
|
||
void ES5Plugin::handleEvent(const clap_event_header *hdr) {
|
||
if (hdr->space_id != CLAP_CORE_EVENT_SPACE_ID) return;
|
||
if (hdr->type != CLAP_EVENT_PARAM_VALUE) return;
|
||
|
||
auto ev = reinterpret_cast<const clap_event_param_value *>(hdr);
|
||
clap_id id = ev->param_id;
|
||
|
||
// Header type
|
||
if (id >= 1 && id <= 6) {
|
||
headerTypes_[id - 1] = ev->value;
|
||
return;
|
||
}
|
||
// Value
|
||
uint32_t h = (id / 100) - 1;
|
||
uint32_t c = (id % 100) - 1;
|
||
if (h < NUM_HEADERS && c < CHANNELS_PER_HEADER) {
|
||
values_[h][c] = ev->value;
|
||
}
|
||
}
|
||
|
||
clap_process_status ES5Plugin::process(const clap_process *process) noexcept {
|
||
const uint32_t nframes = process->frames_count;
|
||
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;
|
||
|
||
float *outL = process->audio_outputs[0].data32[0];
|
||
float *outR = process->audio_outputs[0].data32[1];
|
||
|
||
for (uint32_t i = 0; i < nframes;) {
|
||
// 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.0–1.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;
|
||
}
|