Skip to main content

aux_task/
aux_task.rs

1//! Moves non-real-time work off the audio thread with an auxiliary
2//! task: the audio thread counts blocks and, once a second, asks a
3//! lower-priority task to report — including work that allocates,
4//! which the audio thread itself must never do.
5//!
6//! The counting and the scheduling happen in `render_post`, which runs
7//! once per block on the main audio thread: a block is one block
8//! however many threads rendered it. A task can equally be scheduled
9//! from `render` — the handle is shared as `&self`, so every render
10//! thread reaches it — but then a block asks for one report per thread.
11//!
12//! Cross-compile and run on the board (see docs/cross-compile.md):
13//!
14//! ```sh
15//! cargo build -p bela --release --target aarch64-unknown-linux-gnu --example aux_task
16//! ```
17
18#![cfg_attr(
19    not(bela_device),
20    allow(
21        dead_code,
22        reason = "only the fallback main is reachable off-device; the application code should still compile and lint"
23    )
24)]
25
26use core::sync::atomic::{AtomicU64, Ordering};
27#[cfg(not(bela_device))]
28use std::process::ExitCode;
29use std::sync::Arc;
30
31use bela::{
32    AuxiliaryTask, BelaApplication, BlockContext, CleanupContext, Priority, RenderContext,
33    SetupContext, ThreadInfo, rt_println,
34};
35
36/// The task runs below the audio thread, so a slow report can never
37/// delay rendering.
38const TASK_PRIORITY: Priority = Priority::new(75).expect("75 is within Bela's priority range");
39
40struct Report {
41    task: Option<AuxiliaryTask>,
42    /// Written by the audio thread, read by the task: the only thing
43    /// the two threads share.
44    blocks: Arc<AtomicU64>,
45    /// How many blocks between reports; set in `setup`.
46    interval: u64,
47    /// Counted by the task, so `cleanup` can compare it with the
48    /// number of requests: a request that arrives while the task is
49    /// still running is silently lost.
50    runs: Arc<AtomicU64>,
51    /// Counted in `render_post`, which is single-threaded, so it needs
52    /// no synchronisation.
53    requests: u64,
54}
55
56impl Report {
57    fn new() -> Self {
58        Self {
59            task: None,
60            blocks: Arc::new(AtomicU64::new(0)),
61            interval: 1,
62            runs: Arc::new(AtomicU64::new(0)),
63            requests: 0,
64        }
65    }
66}
67
68impl BelaApplication for Report {
69    type RenderState = ();
70
71    fn setup(&mut self, context: &SetupContext) -> bool {
72        #[allow(
73            clippy::cast_possible_truncation,
74            clippy::cast_sign_loss,
75            reason = "the sample rate is a small positive number"
76        )]
77        let sample_rate_hz = context.audio_sample_rate() as u64;
78        self.interval = (sample_rate_hz / context.audio_frames().max(1) as u64).max(1);
79
80        // The callback owns everything it touches: it cannot borrow
81        // from the application, which the audio thread is using while
82        // the task runs.
83        let blocks = Arc::clone(&self.blocks);
84        let runs = Arc::clone(&self.runs);
85        let task = AuxiliaryTask::new("bela-rs-report", TASK_PRIORITY, move || {
86            runs.fetch_add(1, Ordering::Relaxed);
87            let count = blocks.load(Ordering::Relaxed);
88            // Allocating here is the point of the exercise: this is a
89            // normal thread, so it may do what the audio thread may not.
90            let bar = "#".repeat((count / 10_000) as usize + 1);
91            rt_println!("task: {count} blocks {bar}");
92        });
93
94        match task {
95            Ok(task) => {
96                self.task = Some(task);
97                rt_println!("setup: reporting every {} blocks", self.interval);
98                true
99            }
100            Err(error) => {
101                rt_println!("setup: could not create the task: {error}");
102                false
103            }
104        }
105    }
106
107    fn create_render_state(&mut self, _thread: ThreadInfo, _context: &SetupContext) {}
108
109    fn render(&self, _state: &mut (), _context: &mut RenderContext) {}
110
111    // Real-time safe: a counter, an atomic store and a schedule, all
112    // of which return immediately.
113    fn render_post(&mut self, _states: &mut [()], context: &mut BlockContext) {
114        let blocks = self.blocks.fetch_add(1, Ordering::Relaxed) + 1;
115        if blocks % self.interval != 0 {
116            return;
117        }
118        if let Some(task) = &self.task {
119            task.schedule(context);
120            self.requests += 1;
121        }
122    }
123
124    fn cleanup(&mut self, _states: &mut [()], _context: &CleanupContext) {
125        rt_println!(
126            "cleanup: {} blocks, {} requests, {} task runs",
127            self.blocks.load(Ordering::Relaxed),
128            self.requests,
129            self.runs.load(Ordering::Relaxed)
130        );
131    }
132}
133
134#[cfg(bela_device)]
135fn main() -> Result<(), bela::Error> {
136    bela::Bela::run(Report::new(), &bela::Settings::new())
137}
138
139#[cfg(not(bela_device))]
140fn main() -> ExitCode {
141    eprintln!("This example must be cross-compiled for Bela Gem (aarch64-unknown-linux-gnu).");
142    ExitCode::FAILURE
143}