feat(reactive): multichannel I/O, CV/Gate routing and timing correctness#13
Open
LeoFabre wants to merge 2 commits into
Open
feat(reactive): multichannel I/O, CV/Gate routing and timing correctness#13LeoFabre wants to merge 2 commits into
LeoFabre wants to merge 2 commits into
Conversation
Extends the Reactive (embedded host) frontend beyond stereo and fixes
timestamp correctness in the RealTimeController.
Frontend & configuration
- ReactiveFrontendConfiguration takes audio_inputs, audio_outputs and
output_latency_us (defaults stay at stereo / 0 us for backwards compat).
- MAX_FRONTEND_CHANNELS raised from 8 to 16; over-max returns
INVALID_N_CHANNELS.
- ReactiveFrontend::process_audio() now runs xrun detection, pause-ramp
handling, and set_flush_denormals_to_zero().
- SushiOptions exposes reactive_audio_inputs, reactive_audio_outputs,
reactive_output_latency_us.
CV / Gate routing
- RtController gains set_cv_input / cv_output / set_gate_input /
gate_output, forwarded to the frontend's ControlBuffer.
Timestamp correctness
- calculate_timestamp_from_start() anchors to the host's real clock when
a hardware timestamp is supplied via increment_samples_since_start();
prevents float-precision drift in long sessions and fixes Link sync
(int64_t + double math).
Sample-rate propagation
- ConcreteSushi::set_sample_rate() propagates to the frontend through a
new BaseAudioFrontend::update_sample_rate() hook.
Tests
- reactive_frontend_test.cpp: channel bounds, output latency, CV/Gate
round-trip, update_sample_rate, notify_interrupted_audio forwarding.
- reactive_controller_test.cpp: clock-anchor behaviour and long-session
precision guards.
|
Looks like a solid and useful extension of the Reactive frontend.
The multichannel I/O, CV/Gate routing, sample-rate propagation and
clock-anchored timestamp calculation all make sense for embedded/Bela-style
hosts. The timestamp fix is especially important for long-running sessions
and Link sync stability.
Before merging, I would double-check:
- that all process paths still enforce AUDIO_CHUNK_SIZE correctly,
- that output_latency_us is applied consistently and not only stored,
- that the 16-channel limit does not introduce ABI/config compatibility
issues for existing reactive users.
The included unit tests and Bela manual test coverage are a good sign.
śr., 10 cze 2026, 16:42 użytkownik Leo Fabre ***@***.***>
napisał:
… Extends the Reactive (embedded host) frontend beyond stereo and fixes
timestamp correctness in the RealTimeController.
Frontend & configuration
- ReactiveFrontendConfiguration takes audio_inputs, audio_outputs and
output_latency_us (defaults stay at stereo / 0 us for backwards compat).
- MAX_FRONTEND_CHANNELS raised from 8 to 16; over-max returns
INVALID_N_CHANNELS.
- ReactiveFrontend::process_audio() now runs xrun detection,
pause-ramp handling, and set_flush_denormals_to_zero().
- SushiOptions exposes reactive_audio_inputs, reactive_audio_outputs,
reactive_output_latency_us.
CV / Gate routing
- RtController gains set_cv_input / cv_output / set_gate_input /
gate_output, forwarded to the frontend's ControlBuffer.
Timestamp correctness
- calculate_timestamp_from_start() anchors to the host's real clock
when a hardware timestamp is supplied via increment_samples_since_start();
prevents float-precision drift in long sessions and fixes Link sync
(int64_t + double math).
Sample-rate propagation
- ConcreteSushi::set_sample_rate() propagates to the frontend through
a new BaseAudioFrontend::update_sample_rate() hook.
Tests
- reactive_frontend_test.cpp: channel bounds, output latency, CV/Gate
round-trip, update_sample_rate, notify_interrupted_audio forwarding.
- reactive_controller_test.cpp: clock-anchor behaviour and
long-session precision guards.
Manually tested on a Bela Gem Multi through a minimal Bela C++ project
that uses Sushi Reactive frontend.
Sample Bela C++ code :
#include <Bela.h>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <string>
#define TWINE_EXPOSE_INTERNALS // init_xenomai() is host-only API, hidden by default
#include <twine/twine.h>
#include <sushi/constants.h>
#include <sushi/reactive_factory.h>
#include <sushi/rt_controller.h>
#include <sushi/sample_buffer.h>
#include <sushi/sushi.h>
static std::unique_ptr<sushi::Sushi> g_sushi;static std::unique_ptr<sushi::RtController> g_rt;
// Channel mapping: sushi channels 0/1 <-> Bela audio codec, channels 2..9 <->// Bela analog I/O 0..7 (capelet, unipolar DAC: signals are re-biased 0.5+/-0.5// on output). SUSHI_IO_CHANNELS env (2..10, default 2) sets the count at// startup — the default is bit-identical to the historical stereo behaviour.static int g_channels = 2;static sushi::ChunkSampleBuffer g_in;static sushi::ChunkSampleBuffer g_out;
bool setup(BelaContext* context, void* /*userData*/)
{
// Arm twine's realtime flag so WorkerPool::create_worker_pool() returns the
// EVL out-of-band pool (otherwise it silently falls back to plain pthreads
// pinned to cores 0..N-1, ignoring isolcpus).
twine::init_xenomai();
sushi::ReactiveFactory factory;
sushi::SushiOptions options;
options.frontend_type = sushi::FrontendType::REACTIVE;
options.config_source = sushi::ConfigurationSource::FILE;
const char* config_env = std::getenv("SUSHI_CONFIG");
const char* plugin_env = std::getenv("SUSHI_PLUGIN_PATH");
const char* log_env = std::getenv("SUSHI_LOG_LEVEL");
const char* cores_env = std::getenv("SUSHI_RT_CORES");
const char* chans_env = std::getenv("SUSHI_IO_CHANNELS");
int rt_cores = cores_env ? std::atoi(cores_env) : 1;
if (rt_cores < 1 || rt_cores > 3)
{
rt_cores = 1;
}
g_channels = chans_env ? std::atoi(chans_env) : 2;
if (g_channels < 2 || g_channels > 10)
{
g_channels = 2;
}
options.config_filename = config_env ? config_env : "/root/sushi-config.json";
options.base_plugin_path = plugin_env ? plugin_env : "/usr/lib/vst3";
options.use_osc = false;
options.use_grpc = true;
options.log_level = log_env ? log_env : "warning";
options.log_file = "/tmp/sushi.log";
options.rt_cpu_cores = rt_cores;
options.enable_timings = true;
options.reactive_audio_inputs = g_channels;
options.reactive_audio_outputs = g_channels;
auto [sushi, status] = factory.new_instance(options);
if (status != sushi::Status::OK)
{
rt_fprintf(stderr, "nexus-preamp: sushi init failed: %s\n",
sushi::to_string(status).c_str());
return false;
}
g_rt = factory.rt_controller();
if (!g_rt)
{
rt_fprintf(stderr, "nexus-preamp: failed to get RtController\n");
return false;
}
sushi->set_sample_rate(context->audioSampleRate);
auto start_status = sushi->start();
if (start_status != sushi::Status::OK)
{
rt_fprintf(stderr, "nexus-preamp: sushi start failed: %s\n",
sushi::to_string(start_status).c_str());
return false;
}
g_sushi = std::move(sushi);
g_in = sushi::ChunkSampleBuffer(g_channels);
g_out = sushi::ChunkSampleBuffer(g_channels);
rt_fprintf(stderr,
"nexus-preamp: bela ctx — audio %u in/%u out @%g Hz (%u frames), analog %u in/%u out (%u frames)\n",
context->audioInChannels, context->audioOutChannels,
context->audioSampleRate, context->audioFrames,
context->analogInChannels, context->analogOutChannels,
context->analogFrames);
rt_fprintf(stderr, "nexus-preamp: sushi started (gRPC on :51051, rt_cores=%d, chunk=%d, io_channels=%d)\n",
rt_cores, sushi::AUDIO_CHUNK_SIZE, g_channels);
return true;
}
void render(BelaContext* context, void* /*userData*/)
{
const int frames = context->audioFrames;
constexpr int chunk = sushi::AUDIO_CHUNK_SIZE;
// The engine consumes exactly AUDIO_CHUNK_SIZE frames per process_audio()
// call; feeding it any other amount corrupts memory or desyncs time.
if (frames % chunk != 0)
{
static bool warned = false;
if (!warned)
{
rt_fprintf(stderr, "nexus-preamp: period %d incompatible with sushi chunk %d — muting\n",
frames, chunk);
warned = true;
}
for (int ch = 0; ch < 2; ch++)
{
for (int n = 0; n < frames; n++)
{
audioWrite(context, n, ch, 0.0f);
}
}
return;
}
// Two possible Bela layouts for the extra channels:
// - classic: separate analog context (analogRead/Write, often half rate)
// - PB2 multichannel codec: extra channels folded into the AUDIO context
// (audioOutChannels > 2) — then audioWrite reaches them directly.
const int audio_in_ch = static_cast<int>(context->audioInChannels);
const int audio_out_ch = static_cast<int>(context->audioOutChannels);
const int analog_frames = context->analogFrames;
const int analog_ratio = (analog_frames > 0) ? frames / analog_frames : 1;
const int analog_in = static_cast<int>(context->analogInChannels);
const int analog_out = static_cast<int>(context->analogOutChannels);
for (int offset = 0; offset < frames; offset += chunk)
{
for (int ch = 0; ch < g_channels; ch++)
{
float* dst = g_in.channel(ch);
if (ch < audio_in_ch)
{
for (int n = 0; n < chunk; n++)
{
dst[n] = audioRead(context, offset + n, ch);
}
}
else if (ch - 2 < analog_in)
{
// Unipolar ADC 0..1 -> bipolar
for (int n = 0; n < chunk; n++)
{
dst[n] = 2.0f * analogRead(context, (offset + n) / analog_ratio, ch - 2) - 1.0f;
}
}
else
{
std::memset(dst, 0, chunk * sizeof(float));
}
}
auto timestamp = g_rt->calculate_timestamp_from_start(context->audioSampleRate);
g_rt->process_audio(g_in, g_out, timestamp);
g_rt->increment_samples_since_start(chunk, timestamp);
for (int ch = 0; ch < g_channels; ch++)
{
const float* src = g_out.channel(ch);
if (ch < audio_out_ch)
{
for (int n = 0; n < chunk; n++)
{
audioWrite(context, offset + n, ch, src[n]);
}
}
else if (ch - 2 < analog_out)
{
// Bipolar -> unipolar DAC, clamped to 0..1
for (int n = 0; n < chunk; n++)
{
float v = 0.5f + 0.5f * src[n];
v = (v < 0.0f) ? 0.0f : (v > 1.0f ? 1.0f : v);
analogWriteOnce(context, (offset + n) / analog_ratio, ch - 2, v);
}
}
}
}
}
void cleanup(BelaContext* /*context*/, void* /*userData*/)
{
if (g_sushi)
{
g_sushi->stop();
g_sushi.reset();
}
g_rt.reset();
}
------------------------------
You can view, comment on, or merge this pull request online at:
#13
Commit Summary
- a64273e
<a64273e>
feat(reactive): multichannel I/O, CV/Gate routing and timing correctness
File Changes
(14 files <https://github.com/elk-audio/sushi/pull/13/files>)
- *M* docs/LIBRARY.md
<https://github.com/elk-audio/sushi/pull/13/files#diff-41862bfac01bdf28ee55d1331c291cec32cb4f633b95ea03ae467ec34fb9e65a>
(19)
- *M* include/sushi/rt_controller.h
<https://github.com/elk-audio/sushi/pull/13/files#diff-e3ea9d0b5cc0581646510da76965f5efa3748ff983cf1e787d5126018a877e82>
(35)
- *M* include/sushi/sushi.h
<https://github.com/elk-audio/sushi/pull/13/files#diff-ea2c9d4ae426546b13d3ec91262d36d38d5d155a14ebf3bdecb2018b23d4eecc>
(25)
- *M* src/audio_frontends/base_audio_frontend.h
<https://github.com/elk-audio/sushi/pull/13/files#diff-ed401d3907cb3116eec8417dbec25fcad29df64fc44e6f619f8068e3f2c04fec>
(15)
- *M* src/audio_frontends/reactive_frontend.cpp
<https://github.com/elk-audio/sushi/pull/13/files#diff-56c595ce41e332cac3a30d678bc6823d699654e998eef5f5a0d3bf021522656c>
(38)
- *M* src/audio_frontends/reactive_frontend.h
<https://github.com/elk-audio/sushi/pull/13/files#diff-2456e3272c793268be403c7c3987f0967cb3ed747df66aedde5e88dd72582ba7>
(60)
- *M* src/concrete_sushi.cpp
<https://github.com/elk-audio/sushi/pull/13/files#diff-f867f6753859addec14b425d1aa814b99e5668ebd86cd85942724db556fcc37f>
(4)
- *M* src/engine/controller/real_time_controller.cpp
<https://github.com/elk-audio/sushi/pull/13/files#diff-bdd84a3fede6ba205b99a70b0c644bb779bb0f0995fcf6a14ce9e1d632d909c3>
(50)
- *M* src/engine/controller/real_time_controller.h
<https://github.com/elk-audio/sushi/pull/13/files#diff-f745edc38116fcdb0686dbc2b97244072ce1f39a814a85193f0d412c028f57ba>
(17)
- *M* src/factories/reactive_factory_implementation.cpp
<https://github.com/elk-audio/sushi/pull/13/files#diff-ddbe1e8fdfdc192e328f85fdd6acc42c135831212801c8028f72342be68a1da5>
(9)
- *M* src/factories/reactive_factory_implementation.h
<https://github.com/elk-audio/sushi/pull/13/files#diff-2fefcfb4db179a5b0c3db966bedfeb1553b4373c83e18ed09a344ebc74f72d07>
(2)
- *M* test/CMakeLists.txt
<https://github.com/elk-audio/sushi/pull/13/files#diff-33394812ba204689144fd2f80832db83853ba1cb32403edb4e15fe4893e675fd>
(2)
- *A* test/unittests/audio_frontends/reactive_frontend_test.cpp
<https://github.com/elk-audio/sushi/pull/13/files#diff-d615272020767c116b238ca2fb7de726ef25820ac44dfdee756131dfd7030d20>
(240)
- *M* test/unittests/engine/controllers/reactive_controller_test.cpp
<https://github.com/elk-audio/sushi/pull/13/files#diff-7562b228ce07956f7e26bdaecd0d580286f4852a47599fcde6021439c7ecadef>
(93)
Patch Links:
- https://github.com/elk-audio/sushi/pull/13.patch
- https://github.com/elk-audio/sushi/pull/13.diff
—
Reply to this email directly, view it on GitHub
<#13?email_source=notifications&email_token=BUHB4E4P6VXD67XQS5YFJNT47FXXBA5CNFSNUABEM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UF4ZTQNBQGM3DANZYGOTHEZLBONXW5KTTOVRHGY3SNFRGKZFFMV3GK3TUVRTG633UMVZF6Y3MNFRWW>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/BUHB4EYKQ3GEF6B7B62MXC347FXXBAVCNFSNUABFKJSXA33TNF2G64TZHMZDENRTGU3TOMJUHNEXG43VMU5TINRTGIZDSMBUGQY2C5QC>
.
You are receiving this because you are subscribed to this thread.Message
ID: ***@***.***>
|
MAX_FRONTEND_CHANNELS goes back to 8 so the other frontends keep their existing channel/port behaviour (the JACK frontend registers MAX_FRONTEND_CHANNELS ports and the CoreAudio frontend clamps to it). The Reactive frontend uses its own MAX_REACTIVE_CHANNELS = 16.
Author
|
@Karen86Tonoyan Looks like an AI automatic comment, with all due respect -- but why not, thanks for the review. Note that I chose 16 as it is the maximum inputs the Bela Gem Multi supports regarding PDM mic inputs but in the end the host asks for its own number of channels. The two other points are OK. Regards |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Extends the Reactive (embedded host) frontend beyond stereo and fixes timestamp correctness in the RealTimeController.
Frontend & configuration
CV / Gate routing
Timestamp correctness
Sample-rate propagation
Tests
Manually tested on a Bela Gem Multi through a minimal Bela C++ project that uses Sushi Reactive frontend.
Sample Bela C++ code :