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
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
|||||||
|
build/
|
||||||
|
cmake-build-*/
|
||||||
|
.cache/
|
||||||
|
compile_commands.json
|
||||||
|
*.clap
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.dll
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.17)
|
||||||
|
project(es5-clap VERSION 0.1.0 LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||||
|
|
||||||
|
include(FetchContent)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
clap
|
||||||
|
GIT_REPOSITORY https://github.com/free-audio/clap.git
|
||||||
|
GIT_TAG 1.2.7
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(clap)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
clap-helpers
|
||||||
|
GIT_REPOSITORY https://github.com/free-audio/clap-helpers.git
|
||||||
|
GIT_TAG main
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(clap-helpers)
|
||||||
|
|
||||||
|
add_library(${PROJECT_NAME} MODULE
|
||||||
|
src/plugin.cpp
|
||||||
|
src/entry.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(${PROJECT_NAME} PRIVATE clap-helpers)
|
||||||
|
|
||||||
|
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||||
|
set_target_properties(${PROJECT_NAME} PROPERTIES
|
||||||
|
PREFIX ""
|
||||||
|
SUFFIX ".clap"
|
||||||
|
)
|
||||||
|
target_link_options(${PROJECT_NAME} PRIVATE
|
||||||
|
-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/src/linux-es5-clap.version
|
||||||
|
-Wl,-z,defs
|
||||||
|
)
|
||||||
|
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||||
|
set_target_properties(${PROJECT_NAME} PROPERTIES
|
||||||
|
BUNDLE TRUE
|
||||||
|
BUNDLE_EXTENSION clap
|
||||||
|
MACOSX_BUNDLE_GUI_IDENTIFIER at.sub-net.es5-clap
|
||||||
|
MACOSX_BUNDLE_BUNDLE_NAME "ES-5 Encoder"
|
||||||
|
MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}"
|
||||||
|
MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}"
|
||||||
|
)
|
||||||
|
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||||
|
set_target_properties(${PROJECT_NAME} PROPERTIES
|
||||||
|
PREFIX ""
|
||||||
|
SUFFIX ".clap"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Install to standard CLAP paths
|
||||||
|
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||||
|
install(TARGETS ${PROJECT_NAME} LIBRARY DESTINATION lib/clap)
|
||||||
|
endif()
|
||||||
@@ -1,3 +1,63 @@
|
|||||||
# es5-clap
|
# es5-clap
|
||||||
|
|
||||||
Native CLAP plugin for Expert Sleepers ES-5/ESX-8CV/ESX-8GT encoder — Bitwig-friendly replacement for Silent Way
|
Native [CLAP](https://cleveraudio.org/) plugin for encoding gate and CV data for the
|
||||||
|
[Expert Sleepers ES-5](https://www.expert-sleepers.co.uk/es5.html) module via the
|
||||||
|
[ES-9](https://www.expert-sleepers.co.uk/es9.html) ADAT output.
|
||||||
|
Replaces Silent Way's ES-5 Controller for DAWs with CLAP support (Bitwig, REAPER, etc.).
|
||||||
|
|
||||||
|
## Supported Hardware
|
||||||
|
|
||||||
|
- **ES-5 mk3** — 6 expansion headers, encoded via stereo ADAT pair
|
||||||
|
- **ESX-8GT** — 8 gate outputs per header (accent/velocity via PWM planned)
|
||||||
|
- **ESX-8CV** — 8 CV outputs per header (12-bit DAC, ±10V range)
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -B build -DCMAKE_BUILD_TYPE=Release
|
||||||
|
cmake --build build -j$(nproc)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Copy the built plugin to your CLAP plugin directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux
|
||||||
|
cp build/es5-clap.clap ~/.clap/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
cp -r build/es5-clap.clap ~/Library/Audio/Plug-Ins/CLAP/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage in Bitwig
|
||||||
|
|
||||||
|
1. Add **ES-5 Encoder** as an audio effect on a track
|
||||||
|
2. Route the track's audio output to the ES-9 ADAT channel pair feeding your ES-5
|
||||||
|
(default: ADAT channels 7/8, configurable via jumper on the ES-5 PCB)
|
||||||
|
3. Set each header's type to match your connected expander (GT or CV)
|
||||||
|
4. Automate the gate/CV parameters from Bitwig's modulation system
|
||||||
|
|
||||||
|
### Parameter Layout
|
||||||
|
|
||||||
|
| Parameter | ID | Range | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Header N Type | 1–6 | Off / GT / CV | Expander type on each header |
|
||||||
|
| HN Ch1–8 | N01–N08 | 0.0–1.0 | Gate (>0.5 = on) or CV value |
|
||||||
|
|
||||||
|
## Encoder Protocol
|
||||||
|
|
||||||
|
Based on the [MIT-licensed Pure Data externals](https://github.com/expertsleepersltd/externals)
|
||||||
|
by Expert Sleepers Ltd.
|
||||||
|
|
||||||
|
The ES-5 receives a stereo audio stream where each 24-bit sample packs 3 bytes
|
||||||
|
(L = headers 1–3, R = headers 4–6). Each byte drives one expansion header:
|
||||||
|
|
||||||
|
- **ESX-8GT**: each bit = one gate output (8 gates per byte)
|
||||||
|
- **ESX-8CV**: 12-bit values serialized via a 3-state, 64-phase protocol
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — see [LICENSE](LICENSE).
|
||||||
|
|
||||||
|
Encoder logic derived from Expert Sleepers Ltd MIT-licensed source code.
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// ES-5 / ESX-8CV / ESX-8GT encoder logic
|
||||||
|
// Ported from Expert Sleepers MIT-licensed Pure Data externals:
|
||||||
|
// https://github.com/expertsleepersltd/externals
|
||||||
|
//
|
||||||
|
// Copyright (c) 2014 Expert Sleepers Ltd — MIT License
|
||||||
|
// CLAP adaptation Copyright (c) 2026 Sub-Net e.U. — MIT License
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace es5 {
|
||||||
|
|
||||||
|
// --- Bit ↔ Float conversion (from es5encoder~/main.cpp) ---
|
||||||
|
|
||||||
|
inline float bitsToFloat(int32_t bits) {
|
||||||
|
if (bits & 0x800000)
|
||||||
|
return static_cast<float>(0xFFFFFF & (-bits)) / -8388608.0f;
|
||||||
|
else
|
||||||
|
return static_cast<float>(bits) / 8388608.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ES-5 Encoder: packs 6 × byte (0–255) into stereo float ---
|
||||||
|
|
||||||
|
struct ES5Encoder {
|
||||||
|
// Pack 6 header byte values into L/R audio samples
|
||||||
|
void encode(const uint8_t headers[6], float &outL, float &outR) {
|
||||||
|
int32_t bitsL = (headers[0] << 16) | (headers[1] << 8) | headers[2];
|
||||||
|
int32_t bitsR = (headers[3] << 16) | (headers[4] << 8) | headers[5];
|
||||||
|
outL = bitsToFloat(bitsL);
|
||||||
|
outR = bitsToFloat(bitsR);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- ESX-8GT Encoder: 8 gate booleans → 1 byte ---
|
||||||
|
|
||||||
|
struct ESX8GTEncoder {
|
||||||
|
uint8_t encode(const bool gates[8]) {
|
||||||
|
uint8_t out = 0;
|
||||||
|
for (int i = 0; i < 8; ++i) {
|
||||||
|
if (gates[i])
|
||||||
|
out |= (1 << i);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- ESX-8CV Encoder: 8 × 12-bit CV values → 1 byte per sample (stateful) ---
|
||||||
|
// Ported from esx8cvencoder~/main.cpp
|
||||||
|
// Input CV range: -2048 to +2047 (maps to ≈ ±10V on the hardware)
|
||||||
|
|
||||||
|
struct ESX8CVEncoder {
|
||||||
|
int phase = 0;
|
||||||
|
uint32_t value = 0;
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
phase = 0;
|
||||||
|
value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// values[0..7]: CV inputs in range -2048..+2047
|
||||||
|
// Returns encoded byte (0–255) for this sample
|
||||||
|
uint8_t encode(const double values[8]) {
|
||||||
|
// Non-SMUX mode (standard for ES-9 ADAT)
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance phase (increment by 2 in non-SMUX mode)
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace es5
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// ES-5 CLAP Plugin entry point
|
||||||
|
// Copyright (c) 2026 Sub-Net e.U. — MIT License
|
||||||
|
|
||||||
|
#include <clap/clap.h>
|
||||||
|
#include <cstring>
|
||||||
|
#include "plugin.h"
|
||||||
|
|
||||||
|
static const clap_plugin_descriptor_t s_desc = {
|
||||||
|
.clap_version = CLAP_VERSION_INIT,
|
||||||
|
.id = "at.sub-net.es5-encoder",
|
||||||
|
.name = "ES-5 Encoder",
|
||||||
|
.vendor = "Sub-Net e.U.",
|
||||||
|
.url = "https://git.sub-net.at/submodular/es5-clap",
|
||||||
|
.manual_url = "https://git.sub-net.at/submodular/es5-clap",
|
||||||
|
.support_url = "https://git.sub-net.at/submodular/es5-clap/issues",
|
||||||
|
.version = "0.1.0",
|
||||||
|
.description = "Encoder for Expert Sleepers ES-5 + ESX-8CV/ESX-8GT expanders. "
|
||||||
|
"Generates encoded stereo audio for the ES-9 ADAT output.",
|
||||||
|
.features = (const char *[]){
|
||||||
|
CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
|
||||||
|
CLAP_PLUGIN_FEATURE_UTILITY,
|
||||||
|
CLAP_PLUGIN_FEATURE_STEREO,
|
||||||
|
nullptr
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const clap_plugin_t *create_plugin(
|
||||||
|
const clap_plugin_factory *,
|
||||||
|
const clap_host_t *host,
|
||||||
|
const char *plugin_id)
|
||||||
|
{
|
||||||
|
if (!clap_version_is_compatible(host->clap_version))
|
||||||
|
return nullptr;
|
||||||
|
if (strcmp(plugin_id, s_desc.id) != 0)
|
||||||
|
return nullptr;
|
||||||
|
auto p = new ES5Plugin(&s_desc, host);
|
||||||
|
return p->clapPlugin();
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t get_plugin_count(const clap_plugin_factory *) { return 1; }
|
||||||
|
|
||||||
|
static const clap_plugin_descriptor_t *get_plugin_descriptor(
|
||||||
|
const clap_plugin_factory *, uint32_t index)
|
||||||
|
{
|
||||||
|
return index == 0 ? &s_desc : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const clap_plugin_factory_t s_factory = {
|
||||||
|
.get_plugin_count = get_plugin_count,
|
||||||
|
.get_plugin_descriptor = get_plugin_descriptor,
|
||||||
|
.create_plugin = create_plugin,
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool entry_init(const char *) { return true; }
|
||||||
|
static void entry_deinit() {}
|
||||||
|
static const void *entry_get_factory(const char *factory_id) {
|
||||||
|
if (!strcmp(factory_id, CLAP_PLUGIN_FACTORY_ID))
|
||||||
|
return &s_factory;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
CLAP_EXPORT const clap_plugin_entry_t clap_entry = {
|
||||||
|
.clap_version = CLAP_VERSION_INIT,
|
||||||
|
.init = entry_init,
|
||||||
|
.deinit = entry_deinit,
|
||||||
|
.get_factory = entry_get_factory,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
global:
|
||||||
|
clap_entry;
|
||||||
|
local:
|
||||||
|
*;
|
||||||
|
};
|
||||||
+260
@@ -0,0 +1,260 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// ES-5 CLAP Plugin — Bitwig-native encoder for Expert Sleepers ES-5 + ESX expanders
|
||||||
|
// Copyright (c) 2026 Sub-Net e.U. — MIT License
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <clap/helpers/plugin.hh>
|
||||||
|
#include <clap/helpers/plugin.hxx>
|
||||||
|
#include <clap/helpers/host-proxy.hh>
|
||||||
|
#include <clap/helpers/host-proxy.hxx>
|
||||||
|
#include <array>
|
||||||
|
#include "encoders.h"
|
||||||
|
|
||||||
|
// Header type enum (matches parameter stepped values)
|
||||||
|
enum class HeaderType : int { Off = 0, GT = 1, CV = 2 };
|
||||||
|
|
||||||
|
// Parameter ID scheme:
|
||||||
|
// 1–6: Header type (Off/GT/CV)
|
||||||
|
// 101–108: Header 1 values
|
||||||
|
// 201–208: Header 2 values
|
||||||
|
// ...
|
||||||
|
// 601–608: Header 6 values
|
||||||
|
|
||||||
|
constexpr uint32_t NUM_HEADERS = 6;
|
||||||
|
constexpr uint32_t CHANNELS_PER_HEADER = 8;
|
||||||
|
constexpr uint32_t TOTAL_PARAMS = NUM_HEADERS + NUM_HEADERS * CHANNELS_PER_HEADER; // 54
|
||||||
|
|
||||||
|
constexpr clap_id headerTypeParamId(uint32_t header) { return header + 1; }
|
||||||
|
constexpr clap_id headerValueParamId(uint32_t header, uint32_t ch) {
|
||||||
|
return (header + 1) * 100 + ch + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ES5Plugin : public clap::helpers::Plugin<
|
||||||
|
clap::helpers::MisbehaviourHandler::Terminate,
|
||||||
|
clap::helpers::CheckingLevel::Maximal>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ES5Plugin(const clap_plugin_descriptor *desc, const clap_host *host);
|
||||||
|
|
||||||
|
// --- Audio Ports ---
|
||||||
|
bool implementsAudioPorts() const noexcept override { return true; }
|
||||||
|
uint32_t audioPortsCount(bool isInput) const noexcept override;
|
||||||
|
bool audioPortsInfo(uint32_t index, bool isInput,
|
||||||
|
clap_audio_port_info *info) const noexcept override;
|
||||||
|
|
||||||
|
// --- Parameters ---
|
||||||
|
bool implementsParams() const noexcept override { return true; }
|
||||||
|
uint32_t paramsCount() const noexcept override;
|
||||||
|
bool paramsInfo(uint32_t paramIndex, clap_param_info *info) const noexcept override;
|
||||||
|
bool paramsValue(clap_id paramId, double *value) noexcept override;
|
||||||
|
bool paramsValueToText(clap_id paramId, double value,
|
||||||
|
char *display, uint32_t size) noexcept override;
|
||||||
|
bool paramsTextToValue(clap_id paramId, const char *display,
|
||||||
|
double *value) noexcept override;
|
||||||
|
void paramsFlush(const clap_input_events *in,
|
||||||
|
const clap_output_events *out) noexcept override;
|
||||||
|
|
||||||
|
// --- State ---
|
||||||
|
bool implementsState() const noexcept override { return true; }
|
||||||
|
bool stateSave(const clap_ostream *stream) noexcept override;
|
||||||
|
bool stateLoad(const clap_istream *stream) noexcept override;
|
||||||
|
|
||||||
|
// --- Process ---
|
||||||
|
clap_process_status process(const clap_process *process) noexcept override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void handleEvent(const clap_event_header *hdr);
|
||||||
|
|
||||||
|
// Parameter storage
|
||||||
|
std::array<double, NUM_HEADERS> headerTypes_{}; // 0=off, 1=gt, 2=cv
|
||||||
|
std::array<std::array<double, CHANNELS_PER_HEADER>, NUM_HEADERS> values_{}; // 0.0–1.0
|
||||||
|
|
||||||
|
// Encoder instances
|
||||||
|
es5::ES5Encoder es5Encoder_;
|
||||||
|
std::array<es5::ESX8GTEncoder, NUM_HEADERS> gtEncoders_;
|
||||||
|
std::array<es5::ESX8CVEncoder, NUM_HEADERS> cvEncoders_;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user