#![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;
use core::sync::atomic::{AtomicU32, Ordering};
#[cfg(not(bela_device))]
use std::process::ExitCode;
use std::sync::Arc;
use bela::{
AuxiliaryTask, BelaApplication, BlockContext, CleanupContext, CpuTimer, Priority,
RenderContext, SetupContext, ThreadInfo, rt_println,
};
const OSCILLATORS: usize = 64;
const BASE_FREQUENCY: f32 = 110.0;
const AMPLITUDE: f32 = 0.2;
const MEASUREMENTS_PER_CYCLE: u32 = 2000;
const TASK_PRIORITY: Priority = Priority::new(75).expect("75 is within Bela's priority range");
#[derive(Debug, Default)]
struct Published {
thread: AtomicU32,
section: AtomicU32,
}
struct Load {
published: Arc<Published>,
task: Option<AuxiliaryTask>,
phases: [f32; OSCILLATORS],
phase_increments: [f32; OSCILLATORS],
blocks: u64,
blocks_per_report: u64,
}
struct Bank {
first_frame: usize,
phases: [f32; OSCILLATORS],
timer: CpuTimer,
}
impl Load {
fn new() -> Self {
Self {
published: Arc::new(Published::default()),
task: None,
phases: [0.0; OSCILLATORS],
phase_increments: [0.0; OSCILLATORS],
blocks: 0,
blocks_per_report: 1,
}
}
}
const fn cycle() -> NonZeroU32 {
NonZeroU32::new(MEASUREMENTS_PER_CYCLE).expect("the cycle length is a non-zero constant")
}
impl BelaApplication for Load {
type RenderState = Bank;
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;
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "the sample rate is a small positive number"
)]
let sample_rate_hz = sample_rate as u64;
self.blocks_per_report = (sample_rate_hz / context.audio_frames().max(1) as u64).max(1);
if context.cpu_usage().is_none() {
rt_println!("setup: CPU monitoring is off; run with Settings::cpu_monitoring");
return false;
}
let published = Arc::clone(&self.published);
let task = AuxiliaryTask::new("bela-rs-cpu", TASK_PRIORITY, move || {
let thread = f32::from_bits(published.thread.load(Ordering::Relaxed));
let section = f32::from_bits(published.section.load(Ordering::Relaxed));
rt_println!("cpu: audio thread {thread:.1}%; oscillators {section:.1}%");
});
match task {
Ok(task) => {
self.task = Some(task);
rt_println!(
"setup: {OSCILLATORS} oscillators over {} render thread(s), \
reporting every {} blocks",
context.thread_count(),
self.blocks_per_report
);
true
}
Err(error) => {
rt_println!("setup: could not create the task: {error}");
false
}
}
}
fn create_render_state(&mut self, thread: ThreadInfo, context: &SetupContext) -> Bank {
Bank {
first_frame: thread.frame_range(context.audio_frames()).start,
phases: [0.0; OSCILLATORS],
timer: CpuTimer::new(cycle()),
}
}
fn render_pre(&mut self, states: &mut [Bank], _context: &mut BlockContext) {
for state in states {
#[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;
}
}
}
fn render(&self, state: &mut Bank, context: &mut RenderContext) {
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);
}
}
}
fn render_post(&mut self, states: &mut [Bank], 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;
}
self.blocks += 1;
if self.blocks % self.blocks_per_report != 0 {
return;
}
let thread = context.cpu_usage().map_or(0.0, |usage| usage.percentage());
let section = states
.first()
.map_or(0.0, |bank| bank.timer.usage().percentage());
self.published
.thread
.store(thread.to_bits(), Ordering::Relaxed);
self.published
.section
.store(section.to_bits(), Ordering::Relaxed);
if let Some(task) = &self.task {
task.schedule(context);
}
}
fn cleanup(&mut self, states: &mut [Bank], context: &CleanupContext) {
if let Some(usage) = context.cpu_usage() {
rt_println!("cleanup: audio thread {usage}");
}
if let Some(bank) = states.first() {
rt_println!(
"cleanup: oscillators {}; {} blocks rendered",
bank.timer.usage(),
self.blocks
);
}
}
}
#[cfg(bela_device)]
fn report_fifo_guard() {
let settings = bela::Settings::new()
.cpu_monitoring(cycle())
.period_size(bela::MAX_MONITORED_PERIOD_SIZE * 2);
let outcome = match bela::Bela::new(Load::new(), &settings) {
Err(bela::Error::CpuMonitoringPeriodSize(frames)) => format!("refused at {frames} frames"),
Err(error) => format!("other-error {error}"),
Ok(_) => "accepted".to_owned(),
};
println!("fifo-guard: {outcome}");
}
#[cfg(bela_device)]
fn main() -> Result<(), bela::Error> {
report_fifo_guard();
bela::Bela::run(Load::new(), &bela::Settings::new().cpu_monitoring(cycle()))
}
#[cfg(not(bela_device))]
fn main() -> ExitCode {
eprintln!("This example must be cross-compiled for Bela Gem (aarch64-unknown-linux-gnu).");
ExitCode::FAILURE
}