bela 0.2.0

Safe Rust API for real-time audio on Bela Gem
Documentation
//! Hardware checks for the CPU monitoring rules that cannot be tested
//! on the host, because they need a real audio system — and, in one
//! case, because only libbela knows the answer.
//!
//! Run by `scripts/smoke-test.sh`, which invokes it once per check and
//! asserts on what that run prints. It covers three things:
//!
//! - **the period size limit is the right one** (`fifo-probe`). Above a
//!   hardware-dependent period size libbela renders on a thread of its
//!   own (`fifoLoop`) while the CPU counters stay with the core audio
//!   thread, which is why `Settings::cpu_monitoring` is refused past
//!   `MAX_MONITORED_PERIOD_SIZE`. That constant is a measured board
//!   fact, so it is measured again here: libbela prints `gFifoFactor`
//!   under `Settings::verbose`, and it has to be 1 at the limit and
//!   more than 1 just above it. Without this the limit could drift from
//!   the hardware and nothing would notice — the refusal itself would
//!   keep passing, since it only consults the constant.
//! - **only one audio system exists at a time** (`second-new`). A
//!   second `Bela::new` has to be refused, because setting an audio
//!   system up reaches into libbela's globals — including the CPU
//!   counters, before `Bela_initAudio` gets a chance to object.
//! - **an unset `cpu_monitoring` means off** (`monitoring`), not
//!   "whatever the last audio system left behind". Stale counters would
//!   report a reading nobody asked for, and would skip the period size
//!   check that goes with asking for one.
//!
//! # One check per run
//!
//! Each invocation brings up at most one audio system and exits. That
//! is not tidiness: this board does not survive several of them in one
//! process. Bringing four or five up and tearing them down again ends
//! in a bus error, and an initialisation aborted from `setup` leaves
//! libbela holding hardware it will not take back — the next one fails
//! with `Mcasp::start() called while already running` and then
//! segfaults. Separate processes are also what makes the output
//! unambiguous, since libbela's C `printf` and Rust's `println!`
//! buffer independently.
//!
//! Cross-compile and run on the board (see docs/cross-compile.md):
//!
//! ```sh
//! cargo build -p bela --release --target aarch64-unknown-linux-gnu --example monitoring_rules
//! ```

#![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::num::NonZeroU32;
use core::sync::atomic::{AtomicBool, Ordering};
use std::process::ExitCode;
use std::sync::Arc;

use bela::{BelaApplication, Context};

const MEASUREMENTS_PER_CYCLE: u32 = 2000;

const fn cycle() -> NonZeroU32 {
    NonZeroU32::new(MEASUREMENTS_PER_CYCLE).expect("the cycle length is a non-zero constant")
}

/// Records whether monitoring was on when `setup` ran — the earliest a
/// callback can look — and then stops the audio system from coming up,
/// since the answer is already in hand.
struct Observe {
    monitored: Arc<AtomicBool>,
}

unsafe impl BelaApplication for Observe {
    fn setup(&mut self, context: &mut Context) -> bool {
        self.monitored
            .store(context.cpu_usage().is_some(), Ordering::Relaxed);
        false
    }

    fn render(&mut self, _context: &mut Context) {}
}

/// Stops an audio system from coming up, for the probe that only needs
/// libbela to have got as far as choosing a `gFifoFactor`.
struct Abort;

unsafe impl BelaApplication for Abort {
    fn setup(&mut self, _context: &mut Context) -> bool {
        false
    }

    fn render(&mut self, _context: &mut Context) {}
}

/// Does nothing, for the check that only needs an audio system to
/// exist.
struct Idle;

unsafe impl BelaApplication for Idle {
    fn render(&mut self, _context: &mut Context) {}
}

#[cfg(bela_device)]
mod checks {
    use core::sync::atomic::{AtomicBool, Ordering};
    use std::sync::Arc;

    use bela::{Bela, Error, Settings};

    use super::{Abort, Idle, Observe, cycle};

    /// Starts initialising audio at `period_size` with libbela's own
    /// logging on, so that it prints the `gFifoFactor` it chose, and
    /// aborts in `setup` once it has.
    ///
    /// Deliberately without `cpu_monitoring`: asking for it above the
    /// limit would be refused before `Bela_initAudio` ran, and libbela
    /// would never print the number being checked.
    ///
    /// Prints nothing itself — the reading is libbela's own output, and
    /// this run is the only thing producing it.
    pub fn fifo_probe(period_size: u32) {
        let settings = Settings::new().period_size(period_size).verbose(true);
        // `Err` is expected: `Abort` refuses in `setup`.
        drop(Bela::new(Abort, &settings));
    }

    /// A second audio system must be refused while the first is alive.
    pub fn second_new() {
        let outcome = match Bela::new(Idle, &Settings::new()) {
            Err(error) => format!("first-new-failed-{error}"),
            Ok(first) => {
                let second = match Bela::new(Idle, &Settings::new()) {
                    Err(Error::AudioSystemExists) => "refused",
                    Err(_) => "failed-otherwise",
                    Ok(_) => "created",
                };
                // Only now, so the second attempt really did overlap it.
                drop(first);
                second.to_owned()
            }
        };
        println!("rules: second-new={outcome}");
    }

    /// Reports whether `setup` saw monitoring on, with it either asked
    /// for or left unset.
    pub fn monitoring(requested: bool) {
        let settings = if requested {
            Settings::new().cpu_monitoring(cycle())
        } else {
            Settings::new()
        };
        let monitored = Arc::new(AtomicBool::new(false));
        let app = Observe {
            monitored: Arc::clone(&monitored),
        };
        // `Observe` aborts in `setup`, so a failed init is the expected
        // outcome and means the observation was taken.
        let observed = match Bela::new(app, &settings) {
            Err(Error::Init(_)) => true,
            Err(_) => false,
            Ok(bela) => {
                drop(bela);
                true
            }
        };
        let seen = if !observed {
            "setup-not-reached"
        } else if monitored.load(Ordering::Relaxed) {
            "some"
        } else {
            "none"
        };
        println!("rules: monitoring={seen}");
    }
}

#[cfg(bela_device)]
fn main() -> ExitCode {
    use std::env::args;

    let arguments: Vec<String> = args().skip(1).collect();
    let check: Vec<&str> = arguments.iter().map(String::as_str).collect();
    match check.as_slice() {
        ["fifo-probe", frames] => {
            let Ok(frames) = frames.parse() else {
                eprintln!("fifo-probe takes a period size in frames, not {frames:?}");
                return ExitCode::FAILURE;
            };
            checks::fifo_probe(frames);
        }
        ["second-new"] => checks::second_new(),
        ["monitoring", "on"] => checks::monitoring(true),
        ["monitoring", "off"] => checks::monitoring(false),
        _ => {
            eprintln!(
                "usage: monitoring_rules (fifo-probe <frames> | second-new | monitoring on|off)\n\
                 one check per run: this board does not survive several audio systems in one process"
            );
            return ExitCode::FAILURE;
        }
    }
    ExitCode::SUCCESS
}

#[cfg(not(bela_device))]
fn main() -> ExitCode {
    eprintln!("This example must be cross-compiled for Bela Gem (aarch64-unknown-linux-gnu).");
    ExitCode::FAILURE
}