#![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::{AUDIO_PRIORITY, AuxiliaryTask, BelaApplication, Context, CpuTimer, 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: i32 = AUDIO_PRIORITY - 20;
#[derive(Debug, Default)]
struct Published {
thread: AtomicU32,
section: AtomicU32,
}
struct Load {
timer: CpuTimer,
published: Arc<Published>,
task: Option<AuxiliaryTask>,
phases: [f32; OSCILLATORS],
phase_increments: [f32; OSCILLATORS],
blocks: u64,
blocks_per_report: u64,
}
impl Load {
fn new() -> Self {
Self {
timer: CpuTimer::new(cycle()),
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")
}
unsafe impl BelaApplication for Load {
fn setup(&mut self, context: &mut Context) -> 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, reporting every {} blocks",
self.blocks_per_report
);
true
}
Err(error) => {
rt_println!("setup: could not create the task: {error}");
false
}
}
}
fn render(&mut self, context: &mut Context) {
{
let _oscillators = self.timer.measure();
let channels = context.audio_out_channels();
for frame in 0..context.audio_frames() {
let mut sample = 0.0;
for (phase, increment) in self.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);
}
}
}
self.blocks += 1;
if self.blocks % self.blocks_per_report != 0 {
return;
}
let thread = context.cpu_usage().map_or(0.0, |usage| usage.percentage());
self.published
.thread
.store(thread.to_bits(), Ordering::Relaxed);
self.published
.section
.store(self.timer.usage().percentage().to_bits(), Ordering::Relaxed);
if let Some(task) = &self.task {
task.schedule(context);
}
}
fn cleanup(&mut self, context: &mut Context) {
if let Some(usage) = context.cpu_usage() {
rt_println!("cleanup: audio thread {usage}");
}
rt_println!(
"cleanup: oscillators {}; {} blocks rendered",
self.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.unsigned_abs() * 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
}