// ES-5 CLAP Plugin implementation // Copyright (c) 2026 Sub-Net e.U. — MIT License #include "plugin.h" #include #include #include 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(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(h), static_cast(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(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(static_cast(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(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( static_cast(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; }