1#![cfg_attr(
30 not(bela_device),
31 allow(
32 dead_code,
33 reason = "only the fallback main is reachable off-device; the application code should still compile and lint"
34 )
35)]
36
37use core::f32::consts::TAU;
38use core::num::NonZeroU32;
39use core::sync::atomic::{AtomicU32, Ordering};
40#[cfg(not(bela_device))]
41use std::process::ExitCode;
42use std::sync::Arc;
43
44use bela::{
45 AuxiliaryTask, BelaApplication, BlockContext, CleanupContext, CpuTimer, Priority,
46 RenderContext, SetupContext, ThreadInfo, rt_println,
47};
48
49const OSCILLATORS: usize = 64;
51const BASE_FREQUENCY: f32 = 110.0;
52const AMPLITUDE: f32 = 0.2;
53
54const MEASUREMENTS_PER_CYCLE: u32 = 2000;
58
59const TASK_PRIORITY: Priority = Priority::new(75).expect("75 is within Bela's priority range");
61
62#[derive(Debug, Default)]
65struct Published {
66 thread: AtomicU32,
67 section: AtomicU32,
68}
69
70struct Load {
71 published: Arc<Published>,
72 task: Option<AuxiliaryTask>,
73 phases: [f32; OSCILLATORS],
77 phase_increments: [f32; OSCILLATORS],
78 blocks: u64,
79 blocks_per_report: u64,
80}
81
82struct Bank {
84 first_frame: usize,
87 phases: [f32; OSCILLATORS],
88 timer: CpuTimer,
90}
91
92impl Load {
93 fn new() -> Self {
94 Self {
95 published: Arc::new(Published::default()),
96 task: None,
97 phases: [0.0; OSCILLATORS],
98 phase_increments: [0.0; OSCILLATORS],
99 blocks: 0,
100 blocks_per_report: 1,
102 }
103 }
104}
105
106const fn cycle() -> NonZeroU32 {
107 NonZeroU32::new(MEASUREMENTS_PER_CYCLE).expect("the cycle length is a non-zero constant")
108}
109
110impl BelaApplication for Load {
111 type RenderState = Bank;
112
113 fn setup(&mut self, context: &SetupContext) -> bool {
114 let sample_rate = context.audio_sample_rate();
115 for (index, increment) in self.phase_increments.iter_mut().enumerate() {
116 #[allow(
117 clippy::cast_precision_loss,
118 reason = "the oscillator index is far below f32's exact integer range"
119 )]
120 let harmonic = index as f32 + 1.0;
121 *increment = TAU * BASE_FREQUENCY * harmonic / sample_rate;
122 }
123
124 #[allow(
125 clippy::cast_possible_truncation,
126 clippy::cast_sign_loss,
127 reason = "the sample rate is a small positive number"
128 )]
129 let sample_rate_hz = sample_rate as u64;
130 self.blocks_per_report = (sample_rate_hz / context.audio_frames().max(1) as u64).max(1);
131
132 if context.cpu_usage().is_none() {
133 rt_println!("setup: CPU monitoring is off; run with Settings::cpu_monitoring");
134 return false;
135 }
136
137 let published = Arc::clone(&self.published);
141 let task = AuxiliaryTask::new("bela-rs-cpu", TASK_PRIORITY, move || {
142 let thread = f32::from_bits(published.thread.load(Ordering::Relaxed));
143 let section = f32::from_bits(published.section.load(Ordering::Relaxed));
144 rt_println!("cpu: audio thread {thread:.1}%; oscillators {section:.1}%");
145 });
146
147 match task {
148 Ok(task) => {
149 self.task = Some(task);
150 rt_println!(
151 "setup: {OSCILLATORS} oscillators over {} render thread(s), \
152 reporting every {} blocks",
153 context.thread_count(),
154 self.blocks_per_report
155 );
156 true
157 }
158 Err(error) => {
159 rt_println!("setup: could not create the task: {error}");
160 false
161 }
162 }
163 }
164
165 fn create_render_state(&mut self, thread: ThreadInfo, context: &SetupContext) -> Bank {
166 Bank {
167 first_frame: thread.frame_range(context.audio_frames()).start,
168 phases: [0.0; OSCILLATORS],
169 timer: CpuTimer::new(cycle()),
170 }
171 }
172
173 fn render_pre(&mut self, states: &mut [Bank], _context: &mut BlockContext) {
175 for state in states {
176 #[allow(
177 clippy::cast_precision_loss,
178 reason = "a frame index within a block is far below f32's exact integer range"
179 )]
180 let offset = state.first_frame as f32;
181 for (phase, (block_phase, increment)) in state
182 .phases
183 .iter_mut()
184 .zip(self.phases.iter().zip(&self.phase_increments))
185 {
186 *phase = block_phase + offset * increment;
187 }
188 }
189 }
190
191 fn render(&self, state: &mut Bank, context: &mut RenderContext) {
194 let _oscillators = state.timer.measure();
198
199 let channels = context.audio_out_channels();
200 for frame in context.audio_frame_range() {
201 let mut sample = 0.0;
202 for (phase, increment) in state.phases.iter_mut().zip(&self.phase_increments) {
203 sample += phase.sin();
204 *phase += increment;
205 if *phase >= TAU {
206 *phase -= TAU;
207 }
208 }
209 #[allow(
210 clippy::cast_precision_loss,
211 reason = "the oscillator count is far below f32's exact integer range"
212 )]
213 let sample = AMPLITUDE * sample / OSCILLATORS as f32;
214 for channel in 0..channels {
215 context.audio_write(frame, channel, sample);
216 }
217 }
218 }
219
220 fn render_post(&mut self, states: &mut [Bank], context: &mut BlockContext) {
222 #[allow(
223 clippy::cast_precision_loss,
224 reason = "a block's frame count is far below f32's exact integer range"
225 )]
226 let frames = context.audio_frames() as f32;
227 for (phase, increment) in self.phases.iter_mut().zip(&self.phase_increments) {
228 *phase = (*phase + frames * increment) % TAU;
229 }
230
231 self.blocks += 1;
232 if self.blocks % self.blocks_per_report != 0 {
233 return;
234 }
235 let thread = context.cpu_usage().map_or(0.0, |usage| usage.percentage());
238 let section = states
239 .first()
240 .map_or(0.0, |bank| bank.timer.usage().percentage());
241 self.published
242 .thread
243 .store(thread.to_bits(), Ordering::Relaxed);
244 self.published
245 .section
246 .store(section.to_bits(), Ordering::Relaxed);
247 if let Some(task) = &self.task {
248 task.schedule(context);
249 }
250 }
251
252 fn cleanup(&mut self, states: &mut [Bank], context: &CleanupContext) {
253 if let Some(usage) = context.cpu_usage() {
256 rt_println!("cleanup: audio thread {usage}");
257 }
258 if let Some(bank) = states.first() {
259 rt_println!(
260 "cleanup: oscillators {}; {} blocks rendered",
261 bank.timer.usage(),
262 self.blocks
263 );
264 }
265 }
266}
267
268#[cfg(bela_device)]
273fn report_fifo_guard() {
274 let settings = bela::Settings::new()
275 .cpu_monitoring(cycle())
276 .period_size(bela::MAX_MONITORED_PERIOD_SIZE * 2);
277 let outcome = match bela::Bela::new(Load::new(), &settings) {
278 Err(bela::Error::CpuMonitoringPeriodSize(frames)) => format!("refused at {frames} frames"),
279 Err(error) => format!("other-error {error}"),
280 Ok(_) => "accepted".to_owned(),
282 };
283 println!("fifo-guard: {outcome}");
284}
285
286#[cfg(bela_device)]
287fn main() -> Result<(), bela::Error> {
288 report_fifo_guard();
289 bela::Bela::run(Load::new(), &bela::Settings::new().cpu_monitoring(cycle()))
290}
291
292#[cfg(not(bela_device))]
293fn main() -> ExitCode {
294 eprintln!("This example must be cross-compiled for Bela Gem (aarch64-unknown-linux-gnu).");
295 ExitCode::FAILURE
296}