#![cfg_attr(
not(bela_device),
allow(
dead_code,
reason = "only the fallback main is reachable off-device; the application code should still compile and lint"
)
)]
use core::f32::consts::TAU;
use core::num::NonZeroU32;
#[cfg(not(bela_device))]
use std::process::ExitCode;
use bela::{
BelaApplication, BlockContext, CleanupContext, CpuTimer, RenderContext, SetupContext,
ThreadInfo, rt_println,
};
const OSCILLATORS: usize = 192;
const BASE_FREQUENCY: f32 = 55.0;
const AMPLITUDE: f32 = 0.2;
const DEFAULT_THREADS: u32 = 4;
const MEASUREMENTS_PER_CYCLE: u32 = 2000;
const UNWRITTEN: f32 = f32::NAN;
const fn cycle() -> NonZeroU32 {
NonZeroU32::new(MEASUREMENTS_PER_CYCLE).expect("the cycle length is a non-zero constant")
}
#[cfg(bela_device)]
fn thread_id() -> i64 {
i64::from(unsafe { libc::gettid() })
}
#[cfg(not(bela_device))]
const fn thread_id() -> i64 {
-1
}
#[cfg(bela_device)]
fn current_cpu() -> i32 {
unsafe { libc::sched_getcpu() }
}
#[cfg(not(bela_device))]
const fn current_cpu() -> i32 {
-1
}
struct Parallel {
phases: [f32; OSCILLATORS],
phase_increments: [f32; OSCILLATORS],
started: u64,
finished: u64,
uncovered: u64,
abandoned: u64,
}
struct Voice {
thread: usize,
first_frame: usize,
last_frame: usize,
phases: [f32; OSCILLATORS],
calls: u64,
frames: u64,
thread_id: i64,
cpu: i32,
timer: CpuTimer,
}
impl Parallel {
const fn new() -> Self {
Self {
phases: [0.0; OSCILLATORS],
phase_increments: [0.0; OSCILLATORS],
started: 0,
finished: 0,
uncovered: 0,
abandoned: 0,
}
}
}
impl BelaApplication for Parallel {
type RenderState = Voice;
fn setup(&mut self, context: &SetupContext) -> bool {
let sample_rate = context.audio_sample_rate();
for (index, increment) in self.phase_increments.iter_mut().enumerate() {
#[allow(
clippy::cast_precision_loss,
reason = "the oscillator index is far below f32's exact integer range"
)]
let harmonic = index as f32 + 1.0;
*increment = TAU * BASE_FREQUENCY * harmonic / sample_rate;
}
rt_println!(
"parallel: setup threads={} frames={} oscillators={OSCILLATORS} rate={sample_rate}",
context.thread_count(),
context.audio_frames()
);
true
}
fn create_render_state(&mut self, thread: ThreadInfo, context: &SetupContext) -> Voice {
let frames = thread.frame_range(context.audio_frames());
Voice {
thread: thread.index(),
first_frame: frames.start,
last_frame: frames.end,
phases: [0.0; OSCILLATORS],
calls: 0,
frames: 0,
thread_id: -1,
cpu: -1,
timer: CpuTimer::new(cycle()),
}
}
fn render_pre(&mut self, states: &mut [Voice], context: &mut BlockContext) {
for state in states.iter_mut() {
#[allow(
clippy::cast_precision_loss,
reason = "a frame index within a block is far below f32's exact integer range"
)]
let offset = state.first_frame as f32;
for (phase, (block_phase, increment)) in state
.phases
.iter_mut()
.zip(self.phases.iter().zip(&self.phase_increments))
{
*phase = block_phase + offset * increment;
}
}
self.started += 1;
let channels = context.audio_out_channels();
for sample in context.audio_out().iter_mut().step_by(channels.max(1)) {
*sample = UNWRITTEN;
}
}
fn render(&self, state: &mut Voice, context: &mut RenderContext) {
if state.calls == 0 {
state.thread_id = thread_id();
state.cpu = current_cpu();
}
state.calls += 1;
let _oscillators = state.timer.measure();
let channels = context.audio_out_channels();
for frame in context.audio_frame_range() {
let mut sample = 0.0;
for (phase, increment) in state.phases.iter_mut().zip(&self.phase_increments) {
sample += phase.sin();
*phase += increment;
if *phase >= TAU {
*phase -= TAU;
}
}
#[allow(
clippy::cast_precision_loss,
reason = "the oscillator count is far below f32's exact integer range"
)]
let sample = AMPLITUDE * sample / OSCILLATORS as f32;
for channel in 0..channels {
context.audio_write(frame, channel, sample);
}
state.frames += 1;
}
}
fn render_post(&mut self, _states: &mut [Voice], context: &mut BlockContext) {
#[allow(
clippy::cast_precision_loss,
reason = "a block's frame count is far below f32's exact integer range"
)]
let frames = context.audio_frames() as f32;
for (phase, increment) in self.phases.iter_mut().zip(&self.phase_increments) {
*phase = (*phase + frames * increment) % TAU;
}
let channels = context.audio_out_channels();
let mut uncovered = 0;
for sample in context.audio_out().iter_mut().step_by(channels.max(1)) {
if sample.is_nan() {
*sample = 0.0;
uncovered += 1;
}
}
if uncovered != 0 {
self.uncovered += uncovered;
self.abandoned += 1;
}
self.finished += 1;
}
fn cleanup(&mut self, states: &mut [Voice], context: &CleanupContext) {
let mut rendered = 0;
for state in states.iter() {
rt_println!(
"parallel: thread={} tid={} cpu={} range={}..{} calls={} frames={} section={:.1}%",
state.thread,
state.thread_id,
state.cpu,
state.first_frame,
state.last_frame,
state.calls,
state.frames,
state.timer.usage().percentage()
);
rendered += state.frames;
}
let frames = context.audio_frames() as u64;
let expected = self.started * frames;
rt_println!(
"parallel: blocks={} frames={frames} rendered={rendered} expected={expected}",
self.started
);
rt_println!(
"parallel: uncovered={} abandoned={} unfinished={}",
self.uncovered,
self.abandoned,
self.started - self.finished
);
let busy = context.cpu_usage().map_or(0.0, |usage| usage.percentage());
rt_println!("parallel: audio-thread={busy:.1}%");
}
}
fn requested_threads() -> u32 {
use std::env;
env::args()
.nth(1)
.and_then(|argument| argument.parse().ok())
.unwrap_or(DEFAULT_THREADS)
}
#[cfg(bela_device)]
fn main() -> Result<(), bela::Error> {
let settings = bela::Settings::new()
.thread_count(requested_threads())
.cpu_monitoring(cycle());
match bela::Bela::run(Parallel::new(), &settings) {
Ok(()) => {
println!("parallel: faults=0");
Ok(())
}
Err(bela::Error::CallbackFaults(faults)) => {
println!("parallel: faults={faults}");
Err(bela::Error::CallbackFaults(faults))
}
Err(error) => Err(error),
}
}
#[cfg(not(bela_device))]
fn main() -> ExitCode {
eprintln!("This example must be cross-compiled for Bela Gem (aarch64-unknown-linux-gnu).");
ExitCode::FAILURE
}