Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d0ad127b5 |
@@ -53,7 +53,20 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
|||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# ES-5 daemon (PipeWire output)
|
||||||
|
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(PIPEWIRE REQUIRED libpipewire-0.3)
|
||||||
|
|
||||||
|
add_executable(es5d tools/es5d.cpp)
|
||||||
|
target_include_directories(es5d PRIVATE ${PIPEWIRE_INCLUDE_DIRS})
|
||||||
|
target_link_libraries(es5d PRIVATE ${PIPEWIRE_LIBRARIES} rt)
|
||||||
|
target_compile_options(es5d PRIVATE ${PIPEWIRE_CFLAGS_OTHER})
|
||||||
|
endif()
|
||||||
|
|
||||||
# Install to standard CLAP paths
|
# Install to standard CLAP paths
|
||||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||||
install(TARGETS ${PROJECT_NAME} LIBRARY DESTINATION lib/clap)
|
install(TARGETS ${PROJECT_NAME} LIBRARY DESTINATION lib/clap)
|
||||||
|
install(TARGETS es5d RUNTIME DESTINATION bin)
|
||||||
|
install(FILES dist/es5d.service DESTINATION lib/systemd/user)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
Vendored
+14
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=ES-5 Encoder Daemon (PipeWire)
|
||||||
|
Documentation=https://git.sub-net.at/submodular/es5-clap
|
||||||
|
After=pipewire.service
|
||||||
|
Requires=pipewire.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=%h/.local/bin/es5d
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
Executable
+32
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BUILD_DIR="${SCRIPT_DIR}/build"
|
||||||
|
|
||||||
|
# Build if needed
|
||||||
|
if [[ ! -f "${BUILD_DIR}/es5d" ]] || [[ ! -f "${BUILD_DIR}/es5-clap.clap" ]]; then
|
||||||
|
echo "Building..."
|
||||||
|
cmake -S "${SCRIPT_DIR}" -B "${BUILD_DIR}" -DCMAKE_BUILD_TYPE=Release
|
||||||
|
cmake --build "${BUILD_DIR}" -j"$(nproc)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install CLAP plugin
|
||||||
|
CLAP_DIR="${HOME}/.clap"
|
||||||
|
mkdir -p "${CLAP_DIR}"
|
||||||
|
install -m 755 "${BUILD_DIR}/es5-clap.clap" "${CLAP_DIR}/"
|
||||||
|
echo "Installed plugin to ${CLAP_DIR}/es5-clap.clap"
|
||||||
|
|
||||||
|
# Install daemon binary
|
||||||
|
install -Dm 755 "${BUILD_DIR}/es5d" "${HOME}/.local/bin/es5d"
|
||||||
|
echo "Installed daemon to ${HOME}/.local/bin/es5d"
|
||||||
|
|
||||||
|
# Install and enable systemd user service
|
||||||
|
SYSTEMD_DIR="${HOME}/.config/systemd/user"
|
||||||
|
mkdir -p "${SYSTEMD_DIR}"
|
||||||
|
install -m 644 "${SCRIPT_DIR}/dist/es5d.service" "${SYSTEMD_DIR}/"
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now es5d.service
|
||||||
|
echo "Enabled and started es5d.service"
|
||||||
|
|
||||||
|
echo "Done. Check status with: systemctl --user status es5d"
|
||||||
+1
-1
@@ -17,7 +17,7 @@ static const clap_plugin_descriptor_t s_desc = {
|
|||||||
.description = "Encoder for Expert Sleepers ES-5 + ESX-8CV/ESX-8GT expanders. "
|
.description = "Encoder for Expert Sleepers ES-5 + ESX-8CV/ESX-8GT expanders. "
|
||||||
"Generates encoded stereo audio for the ES-9 ADAT output.",
|
"Generates encoded stereo audio for the ES-9 ADAT output.",
|
||||||
.features = (const char *[]){
|
.features = (const char *[]){
|
||||||
CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
|
CLAP_PLUGIN_FEATURE_INSTRUMENT,
|
||||||
CLAP_PLUGIN_FEATURE_UTILITY,
|
CLAP_PLUGIN_FEATURE_UTILITY,
|
||||||
CLAP_PLUGIN_FEATURE_STEREO,
|
CLAP_PLUGIN_FEATURE_STEREO,
|
||||||
nullptr
|
nullptr
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Shared memory layout between CLAP plugin and ES-5 daemon
|
||||||
|
// Copyright (c) 2026 Sub-Net e.U. — MIT License
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
#define ES5_SHM_NAME "/es5_state"
|
||||||
|
#define ES5_NUM_HEADERS 6
|
||||||
|
#define ES5_CHANNELS_PER_HEADER 8
|
||||||
|
|
||||||
|
struct ES5SharedState {
|
||||||
|
// Header types: 0=off, 1=GT, 2=CV
|
||||||
|
std::atomic<uint8_t> header_types[ES5_NUM_HEADERS];
|
||||||
|
|
||||||
|
// Gate/CV values per header per channel
|
||||||
|
// GT: >0.5 = on, CV: 0.0–1.0 maps to full DAC range
|
||||||
|
std::atomic<float> values[ES5_NUM_HEADERS][ES5_CHANNELS_PER_HEADER];
|
||||||
|
|
||||||
|
// Flag: plugin is active
|
||||||
|
std::atomic<uint8_t> active;
|
||||||
|
};
|
||||||
+53
-66
@@ -5,6 +5,10 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
ES5Plugin::ES5Plugin(const clap_plugin_descriptor *desc, const clap_host *host)
|
ES5Plugin::ES5Plugin(const clap_plugin_descriptor *desc, const clap_host *host)
|
||||||
: Plugin(desc, host)
|
: Plugin(desc, host)
|
||||||
@@ -12,12 +16,32 @@ ES5Plugin::ES5Plugin(const clap_plugin_descriptor *desc, const clap_host *host)
|
|||||||
headerTypes_.fill(0.0);
|
headerTypes_.fill(0.0);
|
||||||
for (auto &v : values_)
|
for (auto &v : values_)
|
||||||
v.fill(0.0);
|
v.fill(0.0);
|
||||||
|
|
||||||
|
// Open shared memory for daemon communication
|
||||||
|
int fd = shm_open(ES5_SHM_NAME, O_CREAT | O_RDWR, 0644);
|
||||||
|
if (fd >= 0) {
|
||||||
|
ftruncate(fd, sizeof(ES5SharedState));
|
||||||
|
shm_ = static_cast<ES5SharedState *>(
|
||||||
|
mmap(nullptr, sizeof(ES5SharedState), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0));
|
||||||
|
close(fd);
|
||||||
|
if (shm_ == MAP_FAILED)
|
||||||
|
shm_ = nullptr;
|
||||||
|
else
|
||||||
|
shm_->active.store(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ES5Plugin::~ES5Plugin() {
|
||||||
|
if (shm_) {
|
||||||
|
shm_->active.store(0, std::memory_order_relaxed);
|
||||||
|
munmap(shm_, sizeof(ES5SharedState));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Audio Ports ---
|
// --- Audio Ports ---
|
||||||
|
|
||||||
uint32_t ES5Plugin::audioPortsCount(bool isInput) const noexcept {
|
uint32_t ES5Plugin::audioPortsCount(bool isInput) const noexcept {
|
||||||
return isInput ? 0 : 1; // No input, 1 stereo output
|
return isInput ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ES5Plugin::audioPortsInfo(uint32_t index, bool isInput,
|
bool ES5Plugin::audioPortsInfo(uint32_t index, bool isInput,
|
||||||
@@ -34,11 +58,10 @@ bool ES5Plugin::audioPortsInfo(uint32_t index, bool isInput,
|
|||||||
|
|
||||||
// --- Parameters ---
|
// --- Parameters ---
|
||||||
|
|
||||||
// Build a flat index → (paramId, header, channel) mapping
|
|
||||||
struct ParamEntry {
|
struct ParamEntry {
|
||||||
clap_id id;
|
clap_id id;
|
||||||
int header; // 0–5, or -1 for N/A
|
int header;
|
||||||
int channel; // 0–7, or -1 for header type params
|
int channel;
|
||||||
};
|
};
|
||||||
|
|
||||||
static ParamEntry paramEntryForIndex(uint32_t index) {
|
static ParamEntry paramEntryForIndex(uint32_t index) {
|
||||||
@@ -63,7 +86,6 @@ bool ES5Plugin::paramsInfo(uint32_t paramIndex, clap_param_info *info) const noe
|
|||||||
info->cookie = nullptr;
|
info->cookie = nullptr;
|
||||||
|
|
||||||
if (entry.channel < 0) {
|
if (entry.channel < 0) {
|
||||||
// Header type parameter
|
|
||||||
info->flags = CLAP_PARAM_IS_AUTOMATABLE | CLAP_PARAM_IS_STEPPED;
|
info->flags = CLAP_PARAM_IS_AUTOMATABLE | CLAP_PARAM_IS_STEPPED;
|
||||||
snprintf(info->name, sizeof(info->name), "Header %d Type", entry.header + 1);
|
snprintf(info->name, sizeof(info->name), "Header %d Type", entry.header + 1);
|
||||||
snprintf(info->module, sizeof(info->module), "Header %d", entry.header + 1);
|
snprintf(info->module, sizeof(info->module), "Header %d", entry.header + 1);
|
||||||
@@ -71,7 +93,6 @@ bool ES5Plugin::paramsInfo(uint32_t paramIndex, clap_param_info *info) const noe
|
|||||||
info->max_value = 2.0;
|
info->max_value = 2.0;
|
||||||
info->default_value = 0.0;
|
info->default_value = 0.0;
|
||||||
} else {
|
} else {
|
||||||
// Value parameter (gate or CV depending on header type)
|
|
||||||
info->flags = CLAP_PARAM_IS_AUTOMATABLE;
|
info->flags = CLAP_PARAM_IS_AUTOMATABLE;
|
||||||
snprintf(info->name, sizeof(info->name), "H%d Ch%d", entry.header + 1, entry.channel + 1);
|
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);
|
snprintf(info->module, sizeof(info->module), "Header %d", entry.header + 1);
|
||||||
@@ -84,12 +105,10 @@ bool ES5Plugin::paramsInfo(uint32_t paramIndex, clap_param_info *info) const noe
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ES5Plugin::paramsValue(clap_id paramId, double *value) noexcept {
|
bool ES5Plugin::paramsValue(clap_id paramId, double *value) noexcept {
|
||||||
// Header type params: IDs 1–6
|
|
||||||
if (paramId >= 1 && paramId <= 6) {
|
if (paramId >= 1 && paramId <= 6) {
|
||||||
*value = headerTypes_[paramId - 1];
|
*value = headerTypes_[paramId - 1];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Value params: IDs x01–x08 where x = 1–6
|
|
||||||
uint32_t h = (paramId / 100) - 1;
|
uint32_t h = (paramId / 100) - 1;
|
||||||
uint32_t c = (paramId % 100) - 1;
|
uint32_t c = (paramId % 100) - 1;
|
||||||
if (h < NUM_HEADERS && c < CHANNELS_PER_HEADER) {
|
if (h < NUM_HEADERS && c < CHANNELS_PER_HEADER) {
|
||||||
@@ -137,12 +156,12 @@ void ES5Plugin::paramsFlush(const clap_input_events *in,
|
|||||||
const clap_output_events *) noexcept {
|
const clap_output_events *) noexcept {
|
||||||
for (uint32_t i = 0; i < in->size(in); ++i)
|
for (uint32_t i = 0; i < in->size(in); ++i)
|
||||||
handleEvent(in->get(in, i));
|
handleEvent(in->get(in, i));
|
||||||
|
syncToSharedMemory();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- State ---
|
// --- State ---
|
||||||
|
|
||||||
bool ES5Plugin::stateSave(const clap_ostream *stream) noexcept {
|
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) {
|
for (uint32_t h = 0; h < NUM_HEADERS; ++h) {
|
||||||
double v = headerTypes_[h];
|
double v = headerTypes_[h];
|
||||||
if (stream->write(stream, &v, sizeof(v)) != sizeof(v)) return false;
|
if (stream->write(stream, &v, sizeof(v)) != sizeof(v)) return false;
|
||||||
@@ -169,9 +188,7 @@ bool ES5Plugin::stateLoad(const clap_istream *stream) noexcept {
|
|||||||
values_[h][c] = v;
|
values_[h][c] = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Reset encoder state on preset load
|
syncToSharedMemory();
|
||||||
for (auto &enc : cvEncoders_)
|
|
||||||
enc.reset();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,12 +201,10 @@ void ES5Plugin::handleEvent(const clap_event_header *hdr) {
|
|||||||
auto ev = reinterpret_cast<const clap_event_param_value *>(hdr);
|
auto ev = reinterpret_cast<const clap_event_param_value *>(hdr);
|
||||||
clap_id id = ev->param_id;
|
clap_id id = ev->param_id;
|
||||||
|
|
||||||
// Header type
|
|
||||||
if (id >= 1 && id <= 6) {
|
if (id >= 1 && id <= 6) {
|
||||||
headerTypes_[id - 1] = ev->value;
|
headerTypes_[id - 1] = ev->value;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Value
|
|
||||||
uint32_t h = (id / 100) - 1;
|
uint32_t h = (id / 100) - 1;
|
||||||
uint32_t c = (id % 100) - 1;
|
uint32_t c = (id % 100) - 1;
|
||||||
if (h < NUM_HEADERS && c < CHANNELS_PER_HEADER) {
|
if (h < NUM_HEADERS && c < CHANNELS_PER_HEADER) {
|
||||||
@@ -197,64 +212,36 @@ void ES5Plugin::handleEvent(const clap_event_header *hdr) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ES5Plugin::syncToSharedMemory() {
|
||||||
|
if (!shm_) return;
|
||||||
|
for (uint32_t h = 0; h < NUM_HEADERS; ++h) {
|
||||||
|
shm_->header_types[h].store(
|
||||||
|
static_cast<uint8_t>(headerTypes_[h] + 0.5),
|
||||||
|
std::memory_order_relaxed);
|
||||||
|
for (uint32_t c = 0; c < CHANNELS_PER_HEADER; ++c) {
|
||||||
|
shm_->values[h][c].store(
|
||||||
|
static_cast<float>(values_[h][c]),
|
||||||
|
std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
clap_process_status ES5Plugin::process(const clap_process *process) noexcept {
|
clap_process_status ES5Plugin::process(const clap_process *process) noexcept {
|
||||||
const uint32_t nframes = process->frames_count;
|
const uint32_t nframes = process->frames_count;
|
||||||
const uint32_t nev = process->in_events->size(process->in_events);
|
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;
|
|
||||||
|
|
||||||
|
// Process parameter events
|
||||||
|
for (uint32_t i = 0; i < nev; ++i)
|
||||||
|
handleEvent(process->in_events->get(process->in_events, i));
|
||||||
|
|
||||||
|
// Sync state to shared memory for the daemon
|
||||||
|
syncToSharedMemory();
|
||||||
|
|
||||||
|
// Output silence — the daemon handles actual audio output via PipeWire
|
||||||
float *outL = process->audio_outputs[0].data32[0];
|
float *outL = process->audio_outputs[0].data32[0];
|
||||||
float *outR = process->audio_outputs[0].data32[1];
|
float *outR = process->audio_outputs[0].data32[1];
|
||||||
|
memset(outL, 0, nframes * sizeof(float));
|
||||||
for (uint32_t i = 0; i < nframes;) {
|
memset(outR, 0, nframes * sizeof(float));
|
||||||
// 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;
|
return CLAP_PROCESS_CONTINUE;
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-6
@@ -9,6 +9,7 @@
|
|||||||
#include <clap/helpers/host-proxy.hxx>
|
#include <clap/helpers/host-proxy.hxx>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include "encoders.h"
|
#include "encoders.h"
|
||||||
|
#include "es5_shared.h"
|
||||||
|
|
||||||
// Header type enum (matches parameter stepped values)
|
// Header type enum (matches parameter stepped values)
|
||||||
enum class HeaderType : int { Off = 0, GT = 1, CV = 2 };
|
enum class HeaderType : int { Off = 0, GT = 1, CV = 2 };
|
||||||
@@ -35,6 +36,7 @@ class ES5Plugin : public clap::helpers::Plugin<
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ES5Plugin(const clap_plugin_descriptor *desc, const clap_host *host);
|
ES5Plugin(const clap_plugin_descriptor *desc, const clap_host *host);
|
||||||
|
~ES5Plugin();
|
||||||
|
|
||||||
// --- Audio Ports ---
|
// --- Audio Ports ---
|
||||||
bool implementsAudioPorts() const noexcept override { return true; }
|
bool implementsAudioPorts() const noexcept override { return true; }
|
||||||
@@ -42,6 +44,19 @@ public:
|
|||||||
bool audioPortsInfo(uint32_t index, bool isInput,
|
bool audioPortsInfo(uint32_t index, bool isInput,
|
||||||
clap_audio_port_info *info) const noexcept override;
|
clap_audio_port_info *info) const noexcept override;
|
||||||
|
|
||||||
|
// --- Note Ports (required for Bitwig to treat as instrument) ---
|
||||||
|
bool implementsNotePorts() const noexcept override { return true; }
|
||||||
|
uint32_t notePortsCount(bool isInput) const noexcept override { return isInput ? 1 : 0; }
|
||||||
|
bool notePortsInfo(uint32_t index, bool isInput,
|
||||||
|
clap_note_port_info *info) const noexcept override {
|
||||||
|
if (!isInput || index != 0) return false;
|
||||||
|
info->id = 0;
|
||||||
|
info->supported_dialects = CLAP_NOTE_DIALECT_MIDI;
|
||||||
|
info->preferred_dialect = CLAP_NOTE_DIALECT_MIDI;
|
||||||
|
snprintf(info->name, sizeof(info->name), "MIDI In");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Parameters ---
|
// --- Parameters ---
|
||||||
bool implementsParams() const noexcept override { return true; }
|
bool implementsParams() const noexcept override { return true; }
|
||||||
uint32_t paramsCount() const noexcept override;
|
uint32_t paramsCount() const noexcept override;
|
||||||
@@ -64,13 +79,12 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void handleEvent(const clap_event_header *hdr);
|
void handleEvent(const clap_event_header *hdr);
|
||||||
|
void syncToSharedMemory();
|
||||||
|
|
||||||
// Parameter storage
|
// Parameter storage
|
||||||
std::array<double, NUM_HEADERS> headerTypes_{}; // 0=off, 1=gt, 2=cv
|
std::array<double, NUM_HEADERS> headerTypes_{};
|
||||||
std::array<std::array<double, CHANNELS_PER_HEADER>, NUM_HEADERS> values_{}; // 0.0–1.0
|
std::array<std::array<double, CHANNELS_PER_HEADER>, NUM_HEADERS> values_{};
|
||||||
|
|
||||||
// Encoder instances
|
// Shared memory for daemon communication
|
||||||
es5::ES5Encoder es5Encoder_;
|
ES5SharedState *shm_ = nullptr;
|
||||||
std::array<es5::ESX8GTEncoder, NUM_HEADERS> gtEncoders_;
|
|
||||||
std::array<es5::ESX8CVEncoder, NUM_HEADERS> cvEncoders_;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
// ES-5 PipeWire test — outputs encoded gate+CV pattern via PipeWire
|
||||||
|
// Tests if PipeWire introduces corruption (like Bitwig does)
|
||||||
|
// Usage: es5_pw_test
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cmath>
|
||||||
|
#include <csignal>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <pipewire/pipewire.h>
|
||||||
|
#include <spa/param/audio/format-utils.h>
|
||||||
|
|
||||||
|
static volatile bool running = true;
|
||||||
|
static void sighandler(int) { running = false; }
|
||||||
|
|
||||||
|
// --- ES-5 encoder (from Expert Sleepers MIT source) ---
|
||||||
|
static float bitsToFloat(int32_t bits) {
|
||||||
|
if (bits & 0x800000)
|
||||||
|
return static_cast<float>(0xFFFFFF & (-bits)) / -8388608.0f;
|
||||||
|
else
|
||||||
|
return static_cast<float>(bits) / 8388608.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
value = 2048 + static_cast<int32_t>(std::max(-2048.0, std::min(2047.0, values[dac])));
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AppData {
|
||||||
|
pw_main_loop *loop;
|
||||||
|
pw_stream *stream;
|
||||||
|
uint32_t sample_count;
|
||||||
|
uint32_t total_samples;
|
||||||
|
bool gates_on;
|
||||||
|
ESX8CVEncoder cv_encoder;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void on_process(void *userdata) {
|
||||||
|
auto *app = static_cast<AppData *>(userdata);
|
||||||
|
|
||||||
|
pw_buffer *b = pw_stream_dequeue_buffer(app->stream);
|
||||||
|
if (!b) return;
|
||||||
|
|
||||||
|
spa_buffer *buf = b->buffer;
|
||||||
|
float *dst = static_cast<float *>(buf->datas[0].data);
|
||||||
|
if (!dst) return;
|
||||||
|
|
||||||
|
uint32_t n_frames = buf->datas[0].maxsize / (sizeof(float) * 2);
|
||||||
|
if (b->requested && b->requested < n_frames)
|
||||||
|
n_frames = b->requested;
|
||||||
|
|
||||||
|
for (uint32_t f = 0; f < n_frames; ++f) {
|
||||||
|
if (++app->sample_count >= 48000) {
|
||||||
|
app->sample_count = 0;
|
||||||
|
app->gates_on = !app->gates_on;
|
||||||
|
fprintf(stderr, " Header 1 gates: %s\n", app->gates_on ? "ALL ON " : "ALL OFF");
|
||||||
|
}
|
||||||
|
app->total_samples++;
|
||||||
|
|
||||||
|
uint8_t h1 = app->gates_on ? 0xFF : 0x00;
|
||||||
|
|
||||||
|
double cvs[8];
|
||||||
|
for (int c = 0; c < 8; ++c) {
|
||||||
|
double t = (app->total_samples + c * 48000) / (double)(48000 * 8);
|
||||||
|
cvs[c] = sin(t * 3.14159265) * 2047.0;
|
||||||
|
}
|
||||||
|
uint8_t h6 = app->cv_encoder.encode(cvs);
|
||||||
|
|
||||||
|
int32_t bitsL = h1 << 16;
|
||||||
|
int32_t bitsR = h6; // headers 4,5 = 0, header 6 = cv byte
|
||||||
|
|
||||||
|
dst[f * 2 + 0] = bitsToFloat(bitsL);
|
||||||
|
dst[f * 2 + 1] = bitsToFloat(bitsR);
|
||||||
|
}
|
||||||
|
|
||||||
|
buf->datas[0].chunk->offset = 0;
|
||||||
|
buf->datas[0].chunk->stride = sizeof(float) * 2;
|
||||||
|
buf->datas[0].chunk->size = n_frames * sizeof(float) * 2;
|
||||||
|
|
||||||
|
pw_stream_queue_buffer(app->stream, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const pw_stream_events stream_events = {
|
||||||
|
.version = PW_VERSION_STREAM_EVENTS,
|
||||||
|
.process = on_process,
|
||||||
|
};
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
setbuf(stderr, nullptr);
|
||||||
|
signal(SIGINT, sighandler);
|
||||||
|
|
||||||
|
pw_init(&argc, &argv);
|
||||||
|
|
||||||
|
AppData app{};
|
||||||
|
app.loop = pw_main_loop_new(nullptr);
|
||||||
|
|
||||||
|
auto *props = pw_properties_new(
|
||||||
|
PW_KEY_MEDIA_TYPE, "Audio",
|
||||||
|
PW_KEY_MEDIA_CATEGORY, "Playback",
|
||||||
|
PW_KEY_MEDIA_ROLE, "Production",
|
||||||
|
PW_KEY_NODE_NAME, "es5-encoder-test",
|
||||||
|
PW_KEY_NODE_DESCRIPTION, "ES-5 Encoder Test",
|
||||||
|
// Target the ES-9 multichannel output
|
||||||
|
PW_KEY_TARGET_OBJECT, "alsa_output.usb-Expert_Sleepers_Ltd_ES-9-01.multichannel-output",
|
||||||
|
PW_KEY_NODE_WANT_DRIVER, "true",
|
||||||
|
nullptr
|
||||||
|
);
|
||||||
|
|
||||||
|
app.stream = pw_stream_new_simple(
|
||||||
|
pw_main_loop_get_loop(app.loop),
|
||||||
|
"es5-test",
|
||||||
|
props,
|
||||||
|
&stream_events,
|
||||||
|
&app
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set up format: stereo F32, 48kHz
|
||||||
|
// Channel positions: AUX12, AUX13 (ES-5 L/R on the ES-9)
|
||||||
|
uint8_t format_buf[1024];
|
||||||
|
spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(format_buf, sizeof(format_buf));
|
||||||
|
|
||||||
|
spa_audio_info_raw audio_info = {};
|
||||||
|
audio_info.format = SPA_AUDIO_FORMAT_F32;
|
||||||
|
audio_info.rate = 48000;
|
||||||
|
audio_info.channels = 2;
|
||||||
|
audio_info.position[0] = SPA_AUDIO_CHANNEL_AUX12;
|
||||||
|
audio_info.position[1] = SPA_AUDIO_CHANNEL_AUX13;
|
||||||
|
|
||||||
|
const spa_pod *params[1];
|
||||||
|
params[0] = spa_format_audio_raw_build(&pod_builder, SPA_PARAM_EnumFormat, &audio_info);
|
||||||
|
|
||||||
|
pw_stream_connect(
|
||||||
|
app.stream,
|
||||||
|
PW_DIRECTION_OUTPUT,
|
||||||
|
PW_ID_ANY,
|
||||||
|
static_cast<pw_stream_flags>(
|
||||||
|
PW_STREAM_FLAG_AUTOCONNECT |
|
||||||
|
PW_STREAM_FLAG_MAP_BUFFERS |
|
||||||
|
PW_STREAM_FLAG_RT_PROCESS),
|
||||||
|
params, 1
|
||||||
|
);
|
||||||
|
|
||||||
|
fprintf(stderr, "ES-5 PipeWire test: targeting ES-9 AUX12/AUX13\n");
|
||||||
|
fprintf(stderr, "Header 1: GT (toggling), Header 6: CV (sweep)\n");
|
||||||
|
fprintf(stderr, "Headers 2-5: must stay OFF. Ctrl+C to stop.\n\n");
|
||||||
|
|
||||||
|
pw_main_loop_run(app.loop);
|
||||||
|
|
||||||
|
pw_stream_destroy(app.stream);
|
||||||
|
pw_main_loop_destroy(app.loop);
|
||||||
|
pw_deinit();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
+218
@@ -0,0 +1,218 @@
|
|||||||
|
// ES-5 Daemon — reads gate/CV state from shared memory, outputs encoded
|
||||||
|
// audio via PipeWire directly to the ES-9, bypassing Bitwig's audio engine.
|
||||||
|
// Copyright (c) 2026 Sub-Net e.U. — MIT License
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cmath>
|
||||||
|
#include <csignal>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <pipewire/pipewire.h>
|
||||||
|
#include <spa/param/audio/format-utils.h>
|
||||||
|
|
||||||
|
#include "../src/es5_shared.h"
|
||||||
|
#include "../src/encoders.h"
|
||||||
|
|
||||||
|
static pw_main_loop *loop = nullptr;
|
||||||
|
|
||||||
|
static void sighandler(int) {
|
||||||
|
if (loop) pw_main_loop_quit(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AppData {
|
||||||
|
ES5SharedState *shm;
|
||||||
|
es5::ES5Encoder es5_enc;
|
||||||
|
es5::ESX8GTEncoder gt_enc[ES5_NUM_HEADERS];
|
||||||
|
es5::ESX8CVEncoder cv_enc[ES5_NUM_HEADERS];
|
||||||
|
};
|
||||||
|
|
||||||
|
static void on_process(void *userdata) {
|
||||||
|
auto *app = static_cast<AppData *>(userdata);
|
||||||
|
auto *shm = app->shm;
|
||||||
|
|
||||||
|
pw_buffer *b = pw_stream_dequeue_buffer(static_cast<pw_stream *>(
|
||||||
|
// stored in userdata via closure — we'll use a different approach
|
||||||
|
nullptr));
|
||||||
|
// This won't work — need stream reference. Use struct instead.
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need the stream in the callback, so use a full struct
|
||||||
|
struct DaemonData {
|
||||||
|
pw_main_loop *loop;
|
||||||
|
pw_stream *stream;
|
||||||
|
ES5SharedState *shm;
|
||||||
|
es5::ES5Encoder es5_enc;
|
||||||
|
es5::ESX8GTEncoder gt_enc[ES5_NUM_HEADERS];
|
||||||
|
es5::ESX8CVEncoder cv_enc[ES5_NUM_HEADERS];
|
||||||
|
uint32_t debug_count = 0;
|
||||||
|
FILE *debug_file = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void daemon_process(void *userdata) {
|
||||||
|
auto *d = static_cast<DaemonData *>(userdata);
|
||||||
|
|
||||||
|
pw_buffer *b = pw_stream_dequeue_buffer(d->stream);
|
||||||
|
if (!b) return;
|
||||||
|
|
||||||
|
spa_buffer *buf = b->buffer;
|
||||||
|
float *dst = static_cast<float *>(buf->datas[0].data);
|
||||||
|
if (!dst) return;
|
||||||
|
|
||||||
|
uint32_t n_frames = buf->datas[0].maxsize / (sizeof(float) * 2);
|
||||||
|
if (b->requested && b->requested < n_frames)
|
||||||
|
n_frames = b->requested;
|
||||||
|
|
||||||
|
for (uint32_t f = 0; f < n_frames; ++f) {
|
||||||
|
uint8_t headers[6] = {};
|
||||||
|
|
||||||
|
for (int h = 0; h < ES5_NUM_HEADERS; ++h) {
|
||||||
|
uint8_t type = d->shm->header_types[h].load(std::memory_order_relaxed);
|
||||||
|
|
||||||
|
if (type == 1) { // GT
|
||||||
|
bool gates[8];
|
||||||
|
for (int c = 0; c < 8; ++c)
|
||||||
|
gates[c] = d->shm->values[h][c].load(std::memory_order_relaxed) > 0.5f;
|
||||||
|
headers[h] = d->gt_enc[h].encode(gates);
|
||||||
|
} else if (type == 2) { // CV
|
||||||
|
double cvs[8];
|
||||||
|
for (int c = 0; c < 8; ++c)
|
||||||
|
cvs[c] = d->shm->values[h][c].load(std::memory_order_relaxed) * 4095.0 - 2048.0;
|
||||||
|
headers[h] = d->cv_enc[h].encode(cvs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float L, R;
|
||||||
|
d->es5_enc.encode(headers, L, R);
|
||||||
|
dst[f * 2 + 0] = L;
|
||||||
|
dst[f * 2 + 1] = R;
|
||||||
|
|
||||||
|
// Debug: dump first 48000 samples
|
||||||
|
if (d->debug_count < 48000) {
|
||||||
|
if (!d->debug_file)
|
||||||
|
d->debug_file = fopen("/tmp/es5d_debug.raw", "wb");
|
||||||
|
if (d->debug_file) {
|
||||||
|
fwrite(&L, 4, 1, d->debug_file);
|
||||||
|
fwrite(&R, 4, 1, d->debug_file);
|
||||||
|
fwrite(headers, 1, 6, d->debug_file);
|
||||||
|
// Also write shm header types
|
||||||
|
uint8_t types[6];
|
||||||
|
for (int i = 0; i < 6; ++i)
|
||||||
|
types[i] = d->shm->header_types[i].load(std::memory_order_relaxed);
|
||||||
|
fwrite(types, 1, 6, d->debug_file);
|
||||||
|
d->debug_count++;
|
||||||
|
if (d->debug_count == 48000) {
|
||||||
|
fclose(d->debug_file);
|
||||||
|
d->debug_file = nullptr;
|
||||||
|
fprintf(stderr, "es5d: debug dump written to /tmp/es5d_debug.raw\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf->datas[0].chunk->offset = 0;
|
||||||
|
buf->datas[0].chunk->stride = sizeof(float) * 2;
|
||||||
|
buf->datas[0].chunk->size = n_frames * sizeof(float) * 2;
|
||||||
|
|
||||||
|
pw_stream_queue_buffer(d->stream, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const pw_stream_events stream_events = {
|
||||||
|
.version = PW_VERSION_STREAM_EVENTS,
|
||||||
|
.process = daemon_process,
|
||||||
|
};
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
setbuf(stderr, nullptr);
|
||||||
|
|
||||||
|
// Open or create shared memory
|
||||||
|
int fd = shm_open(ES5_SHM_NAME, O_CREAT | O_RDWR, 0644);
|
||||||
|
if (fd < 0) {
|
||||||
|
perror("shm_open");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
ftruncate(fd, sizeof(ES5SharedState));
|
||||||
|
auto *shm = static_cast<ES5SharedState *>(
|
||||||
|
mmap(nullptr, sizeof(ES5SharedState), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0));
|
||||||
|
close(fd);
|
||||||
|
if (shm == MAP_FAILED) {
|
||||||
|
perror("mmap");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize shared memory
|
||||||
|
memset(shm, 0, sizeof(ES5SharedState));
|
||||||
|
|
||||||
|
pw_init(&argc, &argv);
|
||||||
|
|
||||||
|
DaemonData data{};
|
||||||
|
data.shm = shm;
|
||||||
|
data.loop = pw_main_loop_new(nullptr);
|
||||||
|
loop = data.loop;
|
||||||
|
|
||||||
|
signal(SIGINT, sighandler);
|
||||||
|
signal(SIGTERM, sighandler);
|
||||||
|
|
||||||
|
auto *props = pw_properties_new(
|
||||||
|
PW_KEY_MEDIA_TYPE, "Audio",
|
||||||
|
PW_KEY_MEDIA_CATEGORY, "Playback",
|
||||||
|
PW_KEY_MEDIA_ROLE, "Production",
|
||||||
|
PW_KEY_NODE_NAME, "es5-encoder",
|
||||||
|
PW_KEY_NODE_DESCRIPTION, "ES-5 Encoder (Direct)",
|
||||||
|
PW_KEY_TARGET_OBJECT, "alsa_output.usb-Expert_Sleepers_Ltd_ES-9-01.multichannel-output",
|
||||||
|
PW_KEY_NODE_WANT_DRIVER, "true",
|
||||||
|
PW_KEY_NODE_ALWAYS_PROCESS, "true",
|
||||||
|
nullptr
|
||||||
|
);
|
||||||
|
|
||||||
|
data.stream = pw_stream_new_simple(
|
||||||
|
pw_main_loop_get_loop(data.loop),
|
||||||
|
"es5-encoder",
|
||||||
|
props,
|
||||||
|
&stream_events,
|
||||||
|
&data
|
||||||
|
);
|
||||||
|
|
||||||
|
uint8_t format_buf[1024];
|
||||||
|
spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(format_buf, sizeof(format_buf));
|
||||||
|
|
||||||
|
spa_audio_info_raw audio_info = {};
|
||||||
|
audio_info.format = SPA_AUDIO_FORMAT_F32;
|
||||||
|
audio_info.rate = 48000;
|
||||||
|
audio_info.channels = 2;
|
||||||
|
audio_info.position[0] = SPA_AUDIO_CHANNEL_AUX12;
|
||||||
|
audio_info.position[1] = SPA_AUDIO_CHANNEL_AUX13;
|
||||||
|
|
||||||
|
const spa_pod *params[1];
|
||||||
|
params[0] = spa_format_audio_raw_build(&pod_builder, SPA_PARAM_EnumFormat, &audio_info);
|
||||||
|
|
||||||
|
pw_stream_connect(
|
||||||
|
data.stream,
|
||||||
|
PW_DIRECTION_OUTPUT,
|
||||||
|
PW_ID_ANY,
|
||||||
|
static_cast<pw_stream_flags>(
|
||||||
|
PW_STREAM_FLAG_AUTOCONNECT |
|
||||||
|
PW_STREAM_FLAG_MAP_BUFFERS |
|
||||||
|
PW_STREAM_FLAG_RT_PROCESS),
|
||||||
|
params, 1
|
||||||
|
);
|
||||||
|
|
||||||
|
fprintf(stderr, "es5d: running — PipeWire → ES-9 AUX12/AUX13\n");
|
||||||
|
fprintf(stderr, "es5d: shared memory at " ES5_SHM_NAME "\n");
|
||||||
|
fprintf(stderr, "es5d: waiting for CLAP plugin to set header types...\n");
|
||||||
|
|
||||||
|
pw_main_loop_run(data.loop);
|
||||||
|
|
||||||
|
fprintf(stderr, "\nes5d: shutting down\n");
|
||||||
|
pw_stream_destroy(data.stream);
|
||||||
|
pw_main_loop_destroy(data.loop);
|
||||||
|
pw_deinit();
|
||||||
|
munmap(shm, sizeof(ES5SharedState));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user