#![cfg_attr(
not(bela_device),
allow(
dead_code,
reason = "only the fallback main is reachable off-device; the probe code should still compile and lint"
)
)]
use core::fmt::{self, Write as _};
#[cfg(bela_device)]
use core::num::NonZeroU32;
use core::str;
use std::process::ExitCode;
use bela::{
BelaApplication, BlockContext, PinMode, RenderContext, SetupContext, ThreadInfo, rt_println,
};
const OUT_CHANNEL: usize = 0;
const IN_CHANNEL: usize = 1;
const LED_CHANNEL: usize = 2;
const WRITE_FRAME: usize = 4;
const WRITE_PERIOD_BLOCKS: u32 = 8;
const REPORT_SECONDS: f32 = 1.0;
const MAX_EDGE_FRAMES: usize = 16;
#[allow(
clippy::struct_excessive_bools,
reason = "each one is an independent one-bit fact about the loopback, and grouping them into a state enum would only hide which are true together"
)]
struct Loopback {
input_only: bool,
split: bool,
repeat: bool,
configured: bool,
frame_clock: u64,
write_at: u64,
awaiting: bool,
level: bool,
last_seen: bool,
blocks: u32,
blocks_per_report: u32,
blocks_since_write: u32,
edges: u32,
misses: u32,
unexpected: u32,
min_latency: u64,
max_latency: u64,
out_readback: bool,
edge_frames: [usize; MAX_EDGE_FRAMES],
edge_frame_count: usize,
edge_frames_overflowed: bool,
led: bool,
}
struct List {
bytes: [u8; List::CAPACITY],
len: usize,
truncated: bool,
}
impl List {
const CAPACITY: usize = 96;
const fn new() -> Self {
Self {
bytes: [0; Self::CAPACITY],
len: 0,
truncated: false,
}
}
fn push(&mut self, value: usize) {
let mark = self.len;
if self.len > 0 && write!(self, ",").is_err() {
return;
}
if write!(self, "{value}").is_err() {
self.len = mark;
}
}
const fn mark_truncated(&mut self) {
self.truncated = true;
}
fn as_str(&self) -> &str {
if self.len == 0 {
return if self.truncated { "…" } else { "none" };
}
str::from_utf8(&self.bytes[..self.len]).unwrap_or("?")
}
const fn suffix(&self) -> &'static str {
if self.truncated && self.len > 0 {
",…"
} else {
""
}
}
}
impl fmt::Write for List {
fn write_str(&mut self, text: &str) -> fmt::Result {
let room = Self::CAPACITY - self.len;
if text.len() > room {
self.truncated = true;
return Err(fmt::Error);
}
self.bytes[self.len..self.len + text.len()].copy_from_slice(text.as_bytes());
self.len += text.len();
Ok(())
}
}
impl Loopback {
const fn new(input_only: bool, split: bool, repeat: bool) -> Self {
Self {
input_only,
split,
repeat,
configured: false,
frame_clock: 0,
write_at: 0,
awaiting: false,
level: false,
last_seen: false,
blocks: 0,
blocks_per_report: 0,
blocks_since_write: 0,
edges: 0,
misses: 0,
unexpected: 0,
min_latency: u64::MAX,
max_latency: 0,
out_readback: false,
edge_frames: [0; MAX_EDGE_FRAMES],
edge_frame_count: 0,
edge_frames_overflowed: false,
led: false,
}
}
fn record_edge_frame(&mut self, frame: usize) {
if self.edge_frames[..self.edge_frame_count].contains(&frame) {
return;
}
if self.edge_frame_count == MAX_EDGE_FRAMES {
self.edge_frames_overflowed = true;
return;
}
self.edge_frames[self.edge_frame_count] = frame;
self.edge_frame_count += 1;
}
const fn reset(&mut self) {
self.blocks = 0;
self.edges = 0;
self.misses = 0;
self.unexpected = 0;
self.min_latency = u64::MAX;
self.max_latency = 0;
self.edge_frame_count = 0;
self.edge_frames_overflowed = false;
}
fn report(&mut self, context: &mut BlockContext) {
if self.input_only {
let mut positions = List::new();
for index in 0..self.edge_frame_count {
positions.push(self.edge_frames[index]);
}
if self.edge_frames_overflowed {
positions.mark_truncated();
}
rt_println!(
"digital: input edges:{} in-now:{} edge-frames:{}{}",
self.edges,
self.last_seen,
positions.as_str(),
positions.suffix()
);
self.blink(context);
self.reset();
return;
}
if self.split {
self.report_split(context);
self.reset();
return;
}
if self.edges == 0 {
rt_println!(
"digital: edges:0 misses:{} unexpected:{} latency-frames:none",
self.misses,
self.unexpected
);
} else {
rt_println!(
"digital: edges:{} misses:{} unexpected:{} latency-frames:{}..{}",
self.edges,
self.misses,
self.unexpected,
self.min_latency,
self.max_latency
);
}
rt_println!(
"digital: out-readback:{} level:{} in-now:{}",
self.out_readback,
self.level,
self.last_seen
);
self.blink(context);
self.reset();
}
fn report_split(&mut self, context: &mut BlockContext) {
let threads = context.thread_count();
let frames = context.digital_frames();
let mut boundaries = List::new();
for thread in 1..threads {
boundaries.push(frames * thread / threads);
}
let mut positions = List::new();
for index in 0..self.edge_frame_count {
positions.push(self.edge_frames[index]);
}
if self.edge_frames_overflowed {
positions.mark_truncated();
}
let per_block = if self.blocks == 0 {
0
} else {
u64::from(self.edges) * 100 / u64::from(self.blocks)
};
rt_println!(
"digital: split threads:{} frames:{} boundaries:{}{}",
threads,
frames,
boundaries.as_str(),
boundaries.suffix()
);
rt_println!(
"digital: split edges:{} per-block:{}.{:02} edge-frames:{}{}",
self.edges,
per_block / 100,
per_block % 100,
positions.as_str(),
positions.suffix()
);
self.blink(context);
}
fn blink(&mut self, context: &mut BlockContext) {
self.led = !self.led;
context.digital_write(0, LED_CHANNEL, self.led);
}
fn configure(context: &mut BlockContext, input_only: bool) {
if !input_only {
context.pin_mode(0, OUT_CHANNEL, PinMode::Output);
}
context.pin_mode(0, IN_CHANNEL, PinMode::Input);
context.pin_mode(0, LED_CHANNEL, PinMode::Output);
}
}
impl BelaApplication for Loopback {
type RenderState = ();
fn setup(&mut self, context: &SetupContext) -> bool {
println!(
"digital: shape=channels:{},frames:{},rate:{},audio-frames:{}",
context.digital_channels(),
context.digital_frames(),
context.digital_sample_rate(),
context.audio_frames()
);
let needed = LED_CHANNEL + 1;
if context.digital_channels() < needed || context.digital_frames() == 0 {
println!("digital: need {needed} digital channels and a frame to use them in");
return false;
}
#[allow(
clippy::cast_precision_loss,
reason = "a period size is a few hundred frames at most"
)]
let blocks_per_second = context.audio_sample_rate() / context.audio_frames() as f32;
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "blocks per second times a one-second window is a small positive number"
)]
let blocks_per_report = (blocks_per_second * REPORT_SECONDS) as u32;
self.blocks_per_report = blocks_per_report.max(1);
true
}
fn create_render_state(&mut self, _thread: ThreadInfo, _context: &SetupContext) {}
fn render_pre(&mut self, _states: &mut [()], context: &mut BlockContext) {
let frames = context.digital_frames();
let write_frame = WRITE_FRAME.min(frames.saturating_sub(1));
if !self.configured {
rt_println!("digital: initial-word=0x{:08x}", context.digital()[0]);
Self::configure(context, self.input_only);
rt_println!("digital: after-pin-mode=0x{:08x}", context.digital()[0]);
self.last_seen = context.digital_read(0, IN_CHANNEL);
self.configured = true;
} else if self.repeat {
Self::configure(context, self.input_only);
}
for frame in 0..frames {
let value = context.digital_read(frame, IN_CHANNEL);
if value == self.last_seen {
continue;
}
self.last_seen = value;
if self.input_only || self.split {
self.record_edge_frame(frame);
self.edges += 1;
} else if self.awaiting {
let latency = self.frame_clock + frame as u64 - self.write_at;
self.min_latency = self.min_latency.min(latency);
self.max_latency = self.max_latency.max(latency);
self.edges += 1;
self.awaiting = false;
} else {
self.unexpected += 1;
}
}
if !self.input_only {
self.blocks_since_write += 1;
}
if !self.input_only && !self.split && self.repeat {
context.digital_write(0, OUT_CHANNEL, self.level);
context.digital_write(0, LED_CHANNEL, self.led);
}
if !self.input_only && !self.split && self.blocks_since_write >= WRITE_PERIOD_BLOCKS {
if self.awaiting {
self.misses += 1;
}
self.level = !self.level;
self.write_at = self.frame_clock + write_frame as u64;
self.awaiting = true;
self.blocks_since_write = 0;
context.digital_write(write_frame, OUT_CHANNEL, self.level);
self.out_readback = context.digital_read(write_frame, OUT_CHANNEL);
}
self.frame_clock += frames as u64;
self.blocks += 1;
if self.blocks >= self.blocks_per_report {
self.report(context);
}
}
fn render(&self, _state: &mut (), context: &mut RenderContext) {
if !self.split {
return;
}
let range = context.digital_frame_range();
if range.is_empty() {
return;
}
let level = context.this_thread() % 2 == 1;
context.digital_write(range.start, OUT_CHANNEL, level);
}
}
#[cfg(bela_device)]
fn main() -> ExitCode {
use std::env::args_os;
use std::ffi::OsString;
let mut input_only = false;
let mut split = false;
let mut repeat = false;
let mut threads: Option<NonZeroU32> = None;
let mut want_threads = false;
let mut args: Vec<OsString> = Vec::new();
for argument in args_os() {
if want_threads {
want_threads = false;
threads = argument.to_str().and_then(|value| value.parse().ok());
if threads.is_none() {
eprintln!("--threads wants a non-zero number");
return ExitCode::FAILURE;
}
continue;
}
match argument.to_str() {
Some("--input-only") => input_only = true,
Some("--split") => split = true,
Some("--repeat") => repeat = true,
Some("--threads") => want_threads = true,
_ => args.push(argument),
}
}
if want_threads {
eprintln!("--threads wants a non-zero number");
return ExitCode::FAILURE;
}
if split && repeat {
eprintln!("--repeat cannot be combined with --split");
return ExitCode::FAILURE;
}
if input_only && (split || repeat) {
eprintln!("--input-only cannot be combined with --split or --repeat");
return ExitCode::FAILURE;
}
let mut settings = bela::Settings::new();
if let Some(threads) = threads {
settings = settings.thread_count(threads);
}
match bela::Bela::run_with_args(Loopback::new(input_only, split, repeat), &settings, args) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("Error: {error}");
ExitCode::FAILURE
}
}
}
#[cfg(not(bela_device))]
fn main() -> ExitCode {
eprintln!("This example must be cross-compiled for Bela Gem (aarch64-unknown-linux-gnu).");
ExitCode::FAILURE
}