Files
es5-clap/src/plugin.cpp
T
sub 6a117a9ba9 Initial CLAP plugin scaffold for ES-5 encoder
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
2026-03-21 01:43:32 +01:00

261 lines
8.4 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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; // 05, or -1 for N/A
int channel; // 07, 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 16
if (paramId >= 1 && paramId <= 6) {
*value = headerTypes_[paramId - 1];
return true;
}
// Value params: IDs x01x08 where x = 16
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.01.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;
}