bela/settings.rs
1use core::ffi::c_int;
2use core::fmt;
3use core::num::NonZeroU32;
4
5use bela_sys::BelaInitSettings;
6
7use crate::application::BelaApplication;
8use crate::cpu;
9use crate::error::Error;
10
11/// Overrides applied on top of Bela's default initialisation settings.
12///
13/// Unset fields keep the values produced by `Bela_defaultSettings()` on
14/// the device, so this type never has to replicate the C-side defaults.
15///
16/// Every method here is a `const fn`, starting with
17/// [`new`](Settings::new), so a whole configuration can be settled at
18/// compile time and handed to every audio system a program builds:
19///
20/// ```
21/// use bela::Settings;
22///
23/// const SETTINGS: Settings = Settings::new().period_size(64).use_analog(true);
24/// # let _ = SETTINGS;
25/// ```
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct Settings {
28 period_size: Option<u32>,
29 audio_sample_rate: Option<NonZeroU32>,
30 use_analog: Option<bool>,
31 use_digital: Option<bool>,
32 num_analog_in_channels: Option<u32>,
33 num_analog_out_channels: Option<u32>,
34 num_digital_channels: Option<u32>,
35 detect_underruns: Option<bool>,
36 verbose: Option<bool>,
37 high_performance_mode: Option<bool>,
38 uniform_sample_rate: Option<bool>,
39 stop_button_pin: Option<StopButtonPin>,
40 enable_led: Option<bool>,
41 thread_count: Option<NonZeroU32>,
42 cpu_monitoring: Option<NonZeroU32>,
43 begin_muted: Option<bool>,
44}
45
46impl Default for Settings {
47 /// The same empty set of overrides [`new`](Settings::new) makes.
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53impl Settings {
54 /// Creates an empty set of overrides.
55 ///
56 /// The fields are written out rather than derived so that this can
57 /// be `const`, which is what lets a whole configuration be one —
58 /// every builder method below already was.
59 #[must_use]
60 pub const fn new() -> Self {
61 Self {
62 period_size: None,
63 audio_sample_rate: None,
64 use_analog: None,
65 use_digital: None,
66 num_analog_in_channels: None,
67 num_analog_out_channels: None,
68 num_digital_channels: None,
69 detect_underruns: None,
70 verbose: None,
71 high_performance_mode: None,
72 uniform_sample_rate: None,
73 stop_button_pin: None,
74 enable_led: None,
75 thread_count: None,
76 cpu_monitoring: None,
77 begin_muted: None,
78 }
79 }
80
81 /// Number of audio frames per period ("block size").
82 ///
83 /// # The context FIFO changes digital output persistence
84 ///
85 /// On a Bela Gem Stereo, a period of 256 frames or more moves the
86 /// application callback behind libbela's context FIFO. A digital
87 /// output configured once and written only when its value changes
88 /// then stops driving the pin, while initialisation and audio still
89 /// succeed without a warning. Re-applying its direction and current
90 /// value in every application block restores the output loopback at
91 /// 256 and 320 frames; `examples/io_digital --repeat` is the probe
92 /// for that workaround. It does not independently establish whether
93 /// FIFO-mode input sampling works. See "What a digital pin does" in
94 /// `docs/board-facts.md` and [#89](https://github.com/akiomik/bela-rs/issues/89).
95 ///
96 /// Nothing here rejects such a period, because it is only the
97 /// digital domain that is affected and a program that never touches
98 /// a pin is unharmed by it.
99 #[must_use]
100 pub const fn period_size(mut self, frames: u32) -> Self {
101 self.period_size = Some(frames);
102 self
103 }
104
105 /// The audio sample rate, in Hz.
106 ///
107 /// `NonZeroU32` rather than the `f32` the C field is: zero is the
108 /// one value this crate's own resolved-settings check refuses, so
109 /// keeping it out of the type keeps that failure out of reach from
110 /// this builder, and an integer matches how the rest of this crate
111 /// spells a hardware quantity — [`thread_count`](Settings::thread_count) and
112 /// [`stop_button_pin`](Settings::stop_button_pin) narrow the same
113 /// way (C-CUSTOM-TYPE). What it cannot express is the C field's own
114 /// range: a negative rate, which only the command line can produce
115 /// and which resolves to 0 the same way a NaN from it does.
116 ///
117 /// # Nothing here checks what the hardware accepts
118 ///
119 /// A Bela Gem Stereo has been measured running at the rates
120 /// `docs/board-facts.md` lists between 8000 Hz and 106000 Hz, with
121 /// the analog and digital rates following when
122 /// [`uniform_sample_rate`](Settings::uniform_sample_rate) is on,
123 /// and aborting the process from inside the codec at every rate
124 /// tried from 108000 Hz up. Those are discrete points, not a swept
125 /// range: 106000 and 108000 are the closest pair measured on either
126 /// side of the ceiling, the rates between them were never tried,
127 /// and neither were most of the rates between the ones in the
128 /// lower list — so a rate this method accepts because it looks
129 /// close to a known-good one, such as 105000 Hz, is untested and
130 /// can still end the process on `SIGABRT` with nothing returned to
131 /// the caller, the same failure shape `--json-string {` has. That
132 /// ceiling is one board's, not a portable libbela contract, which
133 /// is why nothing here compiles it in as a check.
134 ///
135 /// # The command line still wins
136 ///
137 /// [`Bela::new_with_args`](crate::Bela::new_with_args) applies
138 /// `--sample-rate` after this builder's overrides, the same order
139 /// [`period_size`](Settings::period_size) loses to `--period` in —
140 /// so a rate set here is only the value a program starts with, not
141 /// one it is guaranteed to run under.
142 #[must_use]
143 pub const fn audio_sample_rate(mut self, hz: NonZeroU32) -> Self {
144 self.audio_sample_rate = Some(hz);
145 self
146 }
147
148 /// Whether to use the analog input and output.
149 #[must_use]
150 pub const fn use_analog(mut self, enabled: bool) -> Self {
151 self.use_analog = Some(enabled);
152 self
153 }
154
155 /// Whether to use the programmable GPIOs.
156 #[must_use]
157 pub const fn use_digital(mut self, enabled: bool) -> Self {
158 self.use_digital = Some(enabled);
159 self
160 }
161
162 /// How many analog input channels to use.
163 #[must_use]
164 pub const fn num_analog_in_channels(mut self, channels: u32) -> Self {
165 self.num_analog_in_channels = Some(channels);
166 self
167 }
168
169 /// How many analog output channels to use.
170 ///
171 /// # It has to match the inputs, and a Gem Stereo gives none
172 ///
173 /// libbela refuses a different number of analog inputs and outputs
174 /// — `Bela_initAudio` prints `TODO: a different number of channels
175 /// for inputs and outputs is not yet supported` and fails — so this
176 /// is only usable together with
177 /// [`num_analog_in_channels`](Settings::num_analog_in_channels) set
178 /// to the same number. A mismatch costs more than the error says:
179 /// a failed initialisation leaves the process unable to build
180 /// another audio system (see [`Bela::new`](crate::Bela::new)).
181 ///
182 /// A Bela Gem Stereo then reports 0 analog output channels
183 /// whatever was asked for, because it has none. Measured on the
184 /// board; see `docs/board-facts.md`.
185 #[must_use]
186 pub const fn num_analog_out_channels(mut self, channels: u32) -> Self {
187 self.num_analog_out_channels = Some(channels);
188 self
189 }
190
191 /// How many digital (GPIO) channels to use.
192 #[must_use]
193 pub const fn num_digital_channels(mut self, channels: u32) -> Self {
194 self.num_digital_channels = Some(channels);
195 self
196 }
197
198 /// Whether to detect and log underruns.
199 #[must_use]
200 pub const fn detect_underruns(mut self, enabled: bool) -> Self {
201 self.detect_underruns = Some(enabled);
202 self
203 }
204
205 /// Whether to use verbose logging.
206 #[must_use]
207 pub const fn verbose(mut self, enabled: bool) -> Self {
208 self.verbose = Some(enabled);
209 self
210 }
211
212 /// Whether to give more CPU to the audio task. The Linux side of
213 /// the board may freeze while the program is running.
214 #[must_use]
215 pub const fn high_performance_mode(mut self, enabled: bool) -> Self {
216 self.high_performance_mode = Some(enabled);
217 self
218 }
219
220 /// Whether analog channels should be resampled to the audio sample
221 /// rate. Enabled by default on Bela Gem.
222 ///
223 /// What it removes is a frame count that follows the analog
224 /// channel count rather than the block. Measured on a Gem Stereo
225 /// with 16-frame audio blocks, against whatever
226 /// [`audio_sample_rate`](Settings::audio_sample_rate) resolved to:
227 /// with it off, 8 analog input channels give 8 analog frames per
228 /// block at half the audio rate, 4 give 16 frames at the audio rate
229 /// itself and 2 give 32 frames at twice the audio rate. With it on,
230 /// every one of them gives 16 frames at the audio rate — the audio
231 /// block's own — which is what lets one loop over
232 /// [`audio_frames`](crate::BlockContext::audio_frames) read analog
233 /// inputs as it goes. See `docs/board-facts.md`.
234 #[must_use]
235 pub const fn uniform_sample_rate(mut self, enabled: bool) -> Self {
236 self.uniform_sample_rate = Some(enabled);
237 self
238 }
239
240 /// GPIO pin monitored for stopping the program.
241 ///
242 /// Pass `None` to disable monitoring. An unset setting leaves the
243 /// board's default alone.
244 #[must_use]
245 pub const fn stop_button_pin(mut self, pin: Option<u32>) -> Self {
246 self.stop_button_pin = Some(match pin {
247 Some(pin) => StopButtonPin::Gpio(pin),
248 None => StopButtonPin::Disabled,
249 });
250 self
251 }
252
253 /// Whether libbela uses the board's LEDs to show that it is
254 /// running and that an underrun happened.
255 ///
256 /// On unless the board's configuration says otherwise, and the
257 /// other board-level behaviour an application can decline besides
258 /// [`stop_button_pin`](Settings::stop_button_pin). One flag covers
259 /// two indicators, both libbela's own:
260 ///
261 /// - a running indicator, blinked by the PRU firmware for as long
262 /// as audio runs. That is the blue LED on a Bela Gem Stereo
263 /// (`GPIO0_45`, `P2_02`); on a board with no LED of its own it
264 /// is the board's own user LED, which libbela takes away from
265 /// its kernel trigger for the run and hands back afterwards.
266 /// - an underrun indicator, lit for 20000 frames — 0.42 s at
267 /// 48 kHz — whenever an underrun is detected. On a Gem Stereo
268 /// that is the red LED (`GPIO0_46`, `P2_04`).
269 ///
270 /// Off, libbela opens neither pin and gives the PRU no LED address
271 /// to write to, so nothing on the board lights for either event.
272 /// On a Gem Stereo that has been measured as the two GPIOs being
273 /// claimed or left alone together; see "The board LEDs" in
274 /// `docs/board-facts.md`.
275 ///
276 /// # It changes what is shown, not what is detected
277 ///
278 /// [`detect_underruns`](Settings::detect_underruns) is what decides
279 /// whether underruns are counted and logged, and libbela lights the
280 /// red LED from inside that same check. So detection off means no
281 /// underrun LED however this is set, and this off still leaves
282 /// underruns counted into
283 /// [`underrun_count`](crate::BlockContext::underrun_count) and
284 /// still printed to standard error. What it buys is a dark board,
285 /// not a quiet one.
286 ///
287 /// # It is not a way to use those LEDs
288 ///
289 /// Declining libbela's use of them does not hand them to the
290 /// application. They are ordinary GPIOs reached through sysfs,
291 /// which is file I/O and has no place in a real-time callback, and
292 /// this crate offers no API for them. An indicator a callback can
293 /// drive is an LED on a digital channel, where [`pin_mode`] and
294 /// [`digital_write`] are real-time safe and need nothing else —
295 /// those two on [`RenderContext`] in [`render`], and their
296 /// counterparts on [`BlockContext`] in [`render_pre`] and
297 /// [`render_post`].
298 ///
299 /// # The command line can still turn it off
300 ///
301 /// `--disable-led` is applied after this builder, the same order
302 /// [`period_size`](Settings::period_size) loses to `--period` in.
303 /// It goes one way only — Bela has no option that turns the LEDs
304 /// back on — so `enable_led(true)` is the value a run starts with
305 /// rather than one it is guaranteed to keep, and
306 /// [`ResolvedSettings::enable_led`] is where the value that
307 /// survived can be read.
308 ///
309 /// [`pin_mode`]: crate::RenderContext::pin_mode
310 /// [`digital_write`]: crate::RenderContext::digital_write
311 /// [`RenderContext`]: crate::RenderContext
312 /// [`BlockContext`]: crate::BlockContext
313 /// [`render`]: crate::BelaApplication::render
314 /// [`render_pre`]: crate::BelaApplication::render_pre
315 /// [`render_post`]: crate::BelaApplication::render_post
316 #[must_use]
317 pub const fn enable_led(mut self, enabled: bool) -> Self {
318 self.enable_led = Some(enabled);
319 self
320 }
321
322 /// Number of threads used for `render` (multithreaded rendering on
323 /// the quad-core Bela Gem).
324 ///
325 /// libbela creates `threads - 1` extra real-time threads and calls
326 /// [`render`](crate::BelaApplication::render) on all of them at
327 /// once, for the same block, over the same buffers. It partitions
328 /// nothing itself; the crate does, handing each thread its own
329 /// [`RenderState`](crate::BelaApplication::RenderState) and its own
330 /// share of the output frames. See
331 /// `docs/multithreaded-rendering.md`.
332 ///
333 /// More threads than the board has cores buys nothing: they render
334 /// the same block and every one of them has to finish before it
335 /// can be handed over. A Bela Gem has four.
336 ///
337 /// The count cannot be zero: libbela treats 0 and 1 as two
338 /// spellings of the same single render thread, while this API keeps
339 /// one spelling for one configuration.
340 ///
341 /// # An application can insist on the count it gets
342 ///
343 /// Unset, the count is whatever `Bela_defaultSettings()` produced,
344 /// which includes whatever a `Bela_userSettings()` hook the program
345 /// links did to it. So an application that only works on a
346 /// particular number of threads is better saying so than assuming
347 /// it: [`validate_settings`](crate::BelaApplication::validate_settings)
348 /// is given the resolved count as
349 /// [`ResolvedSettings::thread_count`], before any of it has been
350 /// acted on, and refusing there is an ordinary
351 /// [`Error::SettingsRefused`](crate::Error::SettingsRefused).
352 ///
353 /// Bela's standard command-line options cannot change it: the
354 /// version this crate is pinned to has none for the thread count.
355 ///
356 /// # It has to be the number libbela then renders on
357 ///
358 /// The render states are built from the resolved `threadCount`
359 /// before `Bela_initAudio` is called — so a libbela that went on to
360 /// render on a different number of threads would leave some of them
361 /// without a state, and the frame ranges would no longer tile the
362 /// block.
363 ///
364 /// This is a check on libbela rather than on the configuration,
365 /// which is why it stays where it is: the count the settings
366 /// resolved to has already been agreed with the application by
367 /// then, and what is left to catch is a `BelaContext` that reports
368 /// something else.
369 ///
370 /// [`Bela::new`](crate::Bela::new) refuses that rather than
371 /// rendering it: the `setup` callback checks the count the context
372 /// reports and aborts if it disagrees. That fails the
373 /// initialisation with [`Error::Init`](crate::Error::Init) — which,
374 /// as `Bela::new` documents, is fatal to the *process*, so every
375 /// later `Bela::new` in it returns
376 /// [`Error::AudioSystemPoisoned`](crate::Error::AudioSystemPoisoned).
377 ///
378 /// The Bela this crate is pinned to copies `threadCount` through
379 /// unchanged, so the disagreement has not been seen; the check is
380 /// there because a future one might not.
381 #[must_use]
382 pub const fn thread_count(mut self, threads: NonZeroU32) -> Self {
383 self.thread_count = Some(threads);
384 self
385 }
386
387 /// Measures how much of each block the audio thread uses,
388 /// averaging over `measurements_per_cycle` blocks.
389 ///
390 /// [`BlockContext::cpu_usage`](crate::BlockContext::cpu_usage) reads
391 /// the
392 /// result; without this it returns `None`. The cycle length trades
393 /// responsiveness against noise: at 44.1 kHz and 16 frames per
394 /// block, a block is about 0.36 ms, so 2000 blocks is a reading
395 /// roughly every 0.7 s.
396 ///
397 /// # Why it is a setting
398 ///
399 /// Turning monitoring on resets counters the audio thread owns, and
400 /// libbela decides whether to measure at all when that thread
401 /// starts. Both make this something to say before audio exists, so
402 /// it is applied by [`Bela::new`](crate::Bela::new) — which is also
403 /// what keeps it out of reach of code that could race with a
404 /// running audio thread.
405 ///
406 /// Note that this one is *not* applied by
407 /// [`apply_to`](Settings::apply_to): it is a separate C call rather
408 /// than a field of `BelaInitSettings`.
409 #[must_use]
410 pub const fn cpu_monitoring(mut self, measurements_per_cycle: NonZeroU32) -> Self {
411 self.cpu_monitoring = Some(measurements_per_cycle);
412 self
413 }
414
415 /// Whether the speaker amplifiers come up muted.
416 ///
417 /// The one level control that has to be a setting: [`Bela::start`]
418 /// unmutes the amplifiers unless this asked otherwise, so a
419 /// [`Bela::mute_speakers`] call before it is undone again. Everything
420 /// else the codec can be told — the line out level, the headphone
421 /// level and the input gain — is a call on the [`Bela`] handle,
422 /// which reaches the hardware in the same state when made before
423 /// [`Bela::start`].
424 ///
425 /// A Bela Gem Stereo has no amplifier mute pin, so this has no
426 /// effect there; see [`Bela::mute_speakers`].
427 ///
428 /// [`Bela`]: crate::Bela
429 /// [`Bela::start`]: crate::Bela::start
430 /// [`Bela::mute_speakers`]: crate::Bela::mute_speakers
431 #[must_use]
432 pub const fn begin_muted(mut self, muted: bool) -> Self {
433 self.begin_muted = Some(muted);
434 self
435 }
436
437 /// The requested acquisition cycle for the audio thread, if any.
438 #[cfg_attr(
439 not(bela_device),
440 allow(
441 dead_code,
442 reason = "only the device-gated audio system applies it; still unit-tested on the host"
443 )
444 )]
445 pub(crate) const fn cpu_monitoring_cycle(&self) -> Option<NonZeroU32> {
446 self.cpu_monitoring
447 }
448
449 /// Applies the overrides to a raw `BelaInitSettings`, leaving unset
450 /// fields untouched.
451 ///
452 /// This is the escape hatch for driving `Bela_initAudio` manually;
453 /// normally the audio system applies it for you on top of
454 /// `Bela_defaultSettings()`.
455 pub fn apply_to(&self, raw: &mut BelaInitSettings) {
456 if let Some(v) = self.period_size {
457 raw.periodSize = to_c_int(v);
458 }
459 if let Some(v) = self.audio_sample_rate {
460 raw.audioSampleRate = to_c_float(v);
461 }
462 if let Some(v) = self.use_analog {
463 raw.useAnalog = c_int::from(v);
464 }
465 if let Some(v) = self.use_digital {
466 raw.useDigital = c_int::from(v);
467 }
468 if let Some(v) = self.num_analog_in_channels {
469 raw.numAnalogInChannels = to_c_int(v);
470 }
471 if let Some(v) = self.num_analog_out_channels {
472 raw.numAnalogOutChannels = to_c_int(v);
473 }
474 if let Some(v) = self.num_digital_channels {
475 raw.numDigitalChannels = to_c_int(v);
476 }
477 if let Some(v) = self.detect_underruns {
478 raw.detectUnderruns = c_int::from(v);
479 }
480 if let Some(v) = self.verbose {
481 raw.verbose = c_int::from(v);
482 }
483 if let Some(v) = self.high_performance_mode {
484 raw.highPerformanceMode = c_int::from(v);
485 }
486 if let Some(v) = self.uniform_sample_rate {
487 raw.uniformSampleRate = c_int::from(v);
488 }
489 if let Some(stop_button_pin) = self.stop_button_pin {
490 raw.stopButtonPin = match stop_button_pin {
491 StopButtonPin::Gpio(pin) => to_c_int(pin),
492 StopButtonPin::Disabled => -1,
493 };
494 }
495 if let Some(v) = self.enable_led {
496 raw.enableLED = c_int::from(v);
497 }
498 if let Some(v) = self.thread_count {
499 raw.threadCount = v.get();
500 }
501 if let Some(v) = self.begin_muted {
502 raw.beginMuted = c_int::from(v);
503 }
504 }
505}
506
507#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
508enum StopButtonPin {
509 Gpio(u32),
510 Disabled,
511}
512
513// Settings values far exceed c_int::MAX in no realistic configuration;
514// saturate instead of wrapping if one ever does.
515fn to_c_int(value: u32) -> c_int {
516 c_int::try_from(value).unwrap_or(c_int::MAX)
517}
518
519// The measured rate ladder (docs/board-facts.md) tops out in the
520// hundreds of thousands, far inside an f32's 24-bit exact integer
521// range, so this loses no precision for any rate that has been seen to
522// run.
523#[allow(
524 clippy::cast_precision_loss,
525 reason = "audioSampleRate is a C float; requested rates stay far below where u32 -> f32 loses \
526 precision"
527)]
528const fn to_c_float(value: NonZeroU32) -> f32 {
529 value.get() as f32
530}
531
532/// The settings an audio system is about to be built with, as
533/// [`validate_settings`](BelaApplication::validate_settings) sees them.
534///
535/// A borrowed view of the `BelaInitSettings` this crate holds at the
536/// point where every layer has had its say: `Bela_defaultSettings()`
537/// first — which on a board has already applied the `CL=` line from
538/// `~/.bela/belaconfig` and whatever a `Bela_userSettings()` hook did
539/// — then [`Settings`], then Bela's standard command-line options
540/// where [`Bela::new_with_args`](crate::Bela::new_with_args) was given
541/// any. Nothing writes to those settings afterwards, so what an
542/// accessor here reports is what `Bela_initAudio` will be called with.
543///
544/// # What was asked for, not what the board will give
545///
546/// This is the request. What the hardware makes of it is
547/// [`SetupContext`](crate::SetupContext), and there is no way to have
548/// that any earlier: it does not exist until `Bela_initAudio` has run,
549/// which is the call this view exists to be consulted before.
550///
551/// The two differ wherever libbela reshapes a request rather than
552/// refusing it, and it does so often — `--analog-channels` snaps to 8,
553/// 4 or 2, `--digital-channels` clamps to 16, and a Bela Gem Stereo
554/// reports 0 analog output channels however many were asked for,
555/// because it has none (`docs/board-facts.md`). So a check written
556/// here answers "were these settings asked for", never "is this what
557/// the codec gave".
558///
559/// That is not a gap this crate can close. Asking the board and then
560/// declining means declining from
561/// [`setup`](BelaApplication::setup), which runs inside
562/// `Bela_initAudio` with the audio hardware already up, and a
563/// refusal from there fails the initialisation and leaves the process
564/// unable to build another audio system — see
565/// [`Bela::new`](crate::Bela::new). Refusing the request costs
566/// nothing; refusing the result costs the process.
567///
568/// # More than the C structure
569///
570/// [`Settings::cpu_monitoring`] is not a `BelaInitSettings` field —
571/// it is a separate C call the audio system makes just before
572/// `Bela_initAudio` — so it is carried here alongside the structure
573/// and reported by [`cpu_monitoring`](ResolvedSettings::cpu_monitoring).
574/// Without it this view would be missing the one setting that decides
575/// whether [`BlockContext::cpu_usage`](crate::BlockContext::cpu_usage)
576/// answers at all, and an application built around that reading would
577/// have nothing to check.
578///
579/// # What is not here
580///
581/// `numAudioInChannels` and `numAudioOutChannels`, which `Bela.h`
582/// marks `[ignored]`. Neither [`Settings`] nor any standard
583/// command-line option writes them, and libbela does not read them, so
584/// a comparison against one would be a comparison against a constant
585/// that says nothing about the audio system being built. How many
586/// audio channels there are is
587/// [`SetupContext::audio_in_channels`](crate::SetupContext::audio_in_channels)
588/// and its output counterpart, after the fact.
589///
590/// [`as_sys`](ResolvedSettings::as_sys) reaches the whole C structure
591/// for anything else, including the fields this crate has no safe
592/// vocabulary for.
593pub struct ResolvedSettings<'a> {
594 raw: &'a BelaInitSettings,
595 cpu_monitoring: Option<NonZeroU32>,
596}
597
598impl<'a> ResolvedSettings<'a> {
599 /// Borrows resolved settings, and the monitoring cycle that goes
600 /// with them, as the view an application sees.
601 ///
602 /// Not public: what makes this a *resolved* configuration is where
603 /// the audio system calls it from, and a view built anywhere else
604 /// would carry the name without the property.
605 #[cfg_attr(
606 not(bela_device),
607 allow(
608 dead_code,
609 reason = "only the device-gated audio system resolves settings; still unit-tested on \
610 the host"
611 )
612 )]
613 pub(crate) const fn new(raw: &'a BelaInitSettings, cpu_monitoring: Option<NonZeroU32>) -> Self {
614 Self {
615 raw,
616 cpu_monitoring,
617 }
618 }
619
620 /// Read access to the underlying `BelaInitSettings`.
621 ///
622 /// Everything except [`cpu_monitoring`](ResolvedSettings::cpu_monitoring),
623 /// which libbela keeps nowhere in this structure.
624 #[must_use]
625 #[inline]
626 pub const fn as_sys(&self) -> &BelaInitSettings {
627 self.raw
628 }
629
630 /// Requested number of audio frames per period ("block size").
631 ///
632 /// A C `int`, and reported as one: the resolved value is whatever
633 /// survived Bela's own parser, which reshapes rather than refuses
634 /// — `--period 0` arrives here as 1 — and this crate does not
635 /// reshape it further. There is no period size to check against
636 /// either: 2 runs on a Gem Stereo where 3 does not, and both
637 /// failures move as soon as the analog configuration does
638 /// (`docs/board-facts.md`).
639 #[must_use]
640 #[inline]
641 pub const fn period_size(&self) -> i32 {
642 self.raw.periodSize
643 }
644
645 /// Requested audio sample rate in Hz.
646 ///
647 /// 0 never reaches an application: it is what `--sample-rate`
648 /// gives for anything `atof` cannot read, and it is refused with
649 /// [`Error::SampleRate`] before this view is shown to anyone.
650 #[must_use]
651 #[inline]
652 pub const fn audio_sample_rate(&self) -> f32 {
653 self.raw.audioSampleRate
654 }
655
656 /// Whether the analog input and output were asked for.
657 #[must_use]
658 #[inline]
659 pub const fn use_analog(&self) -> bool {
660 self.raw.useAnalog != 0
661 }
662
663 /// Requested number of analog input channels.
664 ///
665 /// What the command line asked for after Bela's parser snapped it
666 /// to 8, 4 or 2, or what
667 /// [`Settings::num_analog_in_channels`] set, which is passed on as
668 /// it stands. 0 here is not "no analog inputs" —
669 /// [`use_analog`](ResolvedSettings::use_analog) is.
670 #[must_use]
671 #[inline]
672 pub const fn num_analog_in_channels(&self) -> i32 {
673 self.raw.numAnalogInChannels
674 }
675
676 /// Requested number of analog output channels.
677 ///
678 /// A Bela Gem Stereo has none, and reports 0 in the context
679 /// whatever this says; see
680 /// [`Settings::num_analog_out_channels`].
681 #[must_use]
682 #[inline]
683 pub const fn num_analog_out_channels(&self) -> i32 {
684 self.raw.numAnalogOutChannels
685 }
686
687 /// Whether the programmable GPIOs were asked for.
688 #[must_use]
689 #[inline]
690 pub const fn use_digital(&self) -> bool {
691 self.raw.useDigital != 0
692 }
693
694 /// Requested number of digital (GPIO) channels.
695 #[must_use]
696 #[inline]
697 pub const fn num_digital_channels(&self) -> i32 {
698 self.raw.numDigitalChannels
699 }
700
701 /// How many threads `render` will be called on, which is how many
702 /// [`RenderState`](BelaApplication::RenderState)s this audio system
703 /// will build.
704 ///
705 /// At least 1, for the same reason
706 /// [`RenderContext::thread_count`](crate::RenderContext::thread_count)
707 /// is: libbela spells one render thread as either 0 or 1, and this
708 /// reports the number of threads that will render.
709 ///
710 /// The resolved value — Bela's defaults, whatever a
711 /// `Bela_userSettings()` hook made of them, then
712 /// [`Settings::thread_count`]. Bela's standard command-line options
713 /// cannot change it, unlike most of this view; what they can change
714 /// is whether the rest of the configuration still suits the number.
715 /// So an application that only works on one thread — or only on
716 /// four — can say so here, and refusing costs nothing.
717 #[must_use]
718 #[inline]
719 pub const fn thread_count(&self) -> usize {
720 render_threads(self.raw)
721 }
722
723 /// Whether the analog channels were asked to be resampled to the
724 /// audio sample rate; see [`Settings::uniform_sample_rate`].
725 #[must_use]
726 #[inline]
727 pub const fn uniform_sample_rate(&self) -> bool {
728 self.raw.uniformSampleRate != 0
729 }
730
731 /// Whether high-performance mode was asked for.
732 #[must_use]
733 #[inline]
734 pub const fn high_performance_mode(&self) -> bool {
735 self.raw.highPerformanceMode != 0
736 }
737
738 /// Whether underrun detection and logging were asked for.
739 #[must_use]
740 #[inline]
741 pub const fn detect_underruns(&self) -> bool {
742 self.raw.detectUnderruns != 0
743 }
744
745 /// Whether libbela's own verbose logging was asked for.
746 #[must_use]
747 #[inline]
748 pub const fn verbose(&self) -> bool {
749 self.raw.verbose != 0
750 }
751
752 /// Whether libbela's running and underrun LEDs were left on; see
753 /// [`Settings::enable_led`].
754 ///
755 /// Worth reading rather than assuming: `--disable-led` is applied
756 /// after [`Settings`] and clears the flag whatever the application
757 /// asked for. So a program that must know whether the board will
758 /// light up — to put its own indicator somewhere else, or to
759 /// refuse the run — has to look at the resolved value here, not at
760 /// the [`Settings`] it was built with.
761 #[must_use]
762 #[inline]
763 pub const fn enable_led(&self) -> bool {
764 self.raw.enableLED != 0
765 }
766
767 /// Whether the speaker amplifiers were asked to come up muted.
768 #[must_use]
769 #[inline]
770 pub const fn begin_muted(&self) -> bool {
771 self.raw.beginMuted != 0
772 }
773
774 /// The CPU monitoring acquisition cycle that was asked for, in
775 /// measurements per cycle, or `None` when monitoring is off.
776 ///
777 /// What [`Settings::cpu_monitoring`] said, and the one thing here
778 /// that is not a `BelaInitSettings` field: monitoring is a separate
779 /// C call, which the audio system makes immediately after this hook
780 /// has accepted the configuration. Nothing else can change it — it
781 /// has no command-line option — so unlike the rest of this view it
782 /// is the application's own setting read back, and it is here
783 /// because an application that needs
784 /// [`BlockContext::cpu_usage`](crate::BlockContext::cpu_usage) to
785 /// answer has no other way to find out before `setup`.
786 ///
787 /// A cycle that is here has already passed this crate's checks:
788 /// [`Error::CpuMonitoringCycle`] for a length libbela cannot take
789 /// and [`Error::CpuMonitoringPeriodSize`] for a period size where
790 /// the counters would not describe the thread that renders.
791 #[must_use]
792 #[inline]
793 pub const fn cpu_monitoring(&self) -> Option<NonZeroU32> {
794 self.cpu_monitoring
795 }
796
797 /// Which GPIO pin is monitored for stopping the program, or `None`
798 /// when nothing is.
799 ///
800 /// The `Option` is libbela's own spelling read back: a negative pin
801 /// is how monitoring is turned off, which is what
802 /// [`Settings::stop_button_pin`] passes `None` on as. A pin the
803 /// board does not have is not refused anywhere and is reported here
804 /// as the number it is; such a run carries on without a working
805 /// stop button.
806 #[must_use]
807 #[inline]
808 pub const fn stop_button_pin(&self) -> Option<u32> {
809 let pin = self.raw.stopButtonPin;
810 if pin < 0 {
811 None
812 } else {
813 #[allow(
814 clippy::cast_sign_loss,
815 reason = "the branch above is what rules the negative values out"
816 )]
817 Some(pin as u32)
818 }
819 }
820}
821
822/// The accessors, and not the whole C structure: `BelaInitSettings`
823/// has a `Debug` of its own, with the callback pointers and the 256
824/// bytes of `pruFilename` in it, and [`as_sys`](ResolvedSettings::as_sys)
825/// is the way to it.
826impl fmt::Debug for ResolvedSettings<'_> {
827 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
828 f.debug_struct("ResolvedSettings")
829 .field("period_size", &self.period_size())
830 .field("audio_sample_rate", &self.audio_sample_rate())
831 .field("use_analog", &self.use_analog())
832 .field("num_analog_in_channels", &self.num_analog_in_channels())
833 .field("num_analog_out_channels", &self.num_analog_out_channels())
834 .field("use_digital", &self.use_digital())
835 .field("num_digital_channels", &self.num_digital_channels())
836 .field("thread_count", &self.thread_count())
837 .field("uniform_sample_rate", &self.uniform_sample_rate())
838 .field("high_performance_mode", &self.high_performance_mode())
839 .field("detect_underruns", &self.detect_underruns())
840 .field("verbose", &self.verbose())
841 .field("enable_led", &self.enable_led())
842 .field("begin_muted", &self.begin_muted())
843 .field("cpu_monitoring", &self.cpu_monitoring())
844 .field("stop_button_pin", &self.stop_button_pin())
845 .finish_non_exhaustive()
846 }
847}
848
849/// How many analog input channels the Multiplexer Capelet needs, which
850/// is all of them: `Error: multiplexer capelet can only be used with 8
851/// analog channels`.
852const MULTIPLEXER_ANALOG_CHANNELS: c_int = 8;
853
854/// Checks resolved settings for the combinations libbela accepts here
855/// and refuses — or cannot survive — later.
856///
857/// "Resolved" is the whole point of where this is called from: Bela's
858/// defaults, [`Settings`] and the command line have all had their say,
859/// so a program that asks for one half of a bad combination in its
860/// [`Settings`] and the other half on the command line is refused the
861/// same way as one that asks for both in either place.
862///
863/// Each check replaces a failure that has already been measured on the
864/// board and costs the caller more than an error (see "The Multiplexer
865/// Capelet" and "Command-line options" in `docs/board-facts.md`):
866///
867/// - five of them fail inside `Bela_initAudio`: a sample rate of 0 in
868/// `Bela_getHwConfigPrivate`, the two PRU rules in `RTAudio.cpp`'s
869/// initial sanity checks, and the multiplexer channel and analog
870/// input counts in `PRU::initialise`.
871/// What that costs is not the attempt but the process, which can
872/// build no audio system afterwards — see
873/// [`Bela::new`](crate::Bela::new);
874/// - the sixth, the multiplexer with the analog inputs off, is checked
875/// nowhere: libbela's count rules sit behind an `if` that analog
876/// being off skips, so the settings reach the PRU firmware, which
877/// gives up and ends the process from inside libbela with nothing
878/// returned to the caller at all.
879///
880/// Nothing valid is refused here. Multiplexer channel counts of 2, 4
881/// and 8 come up and run on a Gem, and stay accepted although the
882/// buffer they fill has no accessor in this crate; PRU 0 is a valid
883/// setting of its own, and only the multiplexer requires PRU 1.
884#[cfg_attr(
885 not(bela_device),
886 allow(
887 dead_code,
888 reason = "only the device-gated audio system initialises; still unit-tested on the host"
889 )
890)]
891pub(crate) const fn check_resolved(raw: &BelaInitSettings) -> Result<(), Error> {
892 if raw.audioSampleRate == 0.0 {
893 return Err(Error::SampleRate);
894 }
895 if raw.pruNumber < 0 || raw.pruNumber > 1 {
896 return Err(Error::PruNumber(raw.pruNumber));
897 }
898 // Everything below is about a multiplexer that was asked for. Off
899 // is the default and says nothing about the rest of the settings:
900 // with no multiplexer, PRU 0 and the analog inputs disabled are
901 // both ordinary configurations.
902 if raw.numMuxChannels == 0 {
903 return Ok(());
904 }
905 if !matches!(raw.numMuxChannels, 2 | 4 | 8) {
906 return Err(Error::MultiplexerChannels(raw.numMuxChannels));
907 }
908 if raw.pruNumber != 1 {
909 return Err(Error::MultiplexerPru(raw.pruNumber));
910 }
911 if raw.useAnalog == 0 {
912 return Err(Error::MultiplexerWithoutAnalog);
913 }
914 if raw.numAnalogInChannels != MULTIPLEXER_ANALOG_CHANNELS {
915 return Err(Error::MultiplexerAnalogChannels(raw.numAnalogInChannels));
916 }
917 Ok(())
918}
919
920/// Everything that is asked about a resolved configuration before
921/// `Bela_initAudio` is called, in the order it is asked.
922///
923/// The crate's own checks first, because they describe libbela rather
924/// than any one application: a configuration [`check_resolved`] refuses
925/// is one no application could have run under, so reporting it as the
926/// application's refusal would name the wrong culprit. The CPU
927/// monitoring period size goes with them, being another rule of this
928/// crate's own — and it is only asked when monitoring was asked for,
929/// since the limit is about the thread the counters measure.
930///
931/// [`BelaApplication::validate_settings`] comes last, with the same
932/// settings and nothing yet done about them: no monitoring counters
933/// reset, no render states allocated, no audio hardware brought up. A
934/// refusal from there is an ordinary [`Error`] and the process is left
935/// as it was.
936///
937/// `cpu_monitoring` is the cycle [`Settings::cpu_monitoring`] asked
938/// for, which is both what decides whether the period size is checked
939/// at all and the one part of the configuration the application cannot
940/// read off `BelaInitSettings`, so it is passed on to the view.
941#[cfg_attr(
942 not(bela_device),
943 allow(
944 dead_code,
945 reason = "only the device-gated audio system initialises; still unit-tested on the host"
946 )
947)]
948pub(crate) fn check_supported<T: BelaApplication>(
949 raw: &BelaInitSettings,
950 cpu_monitoring: Option<NonZeroU32>,
951 application: &T,
952) -> Result<(), Error> {
953 check_resolved(raw)?;
954 if cpu_monitoring.is_some() {
955 // Needs the resolved period size: unset in `Settings` means
956 // Bela's default, not "no period size". The raw value is
957 // signed even though Settings only accepts u32; map a negative
958 // resolved value to an impossible upper bound so the unsigned
959 // range check refuses it.
960 let period_size = u32::try_from(raw.periodSize).unwrap_or(u32::MAX);
961 cpu::check_period_size(period_size)?;
962 }
963 application
964 .validate_settings(&ResolvedSettings::new(raw, cpu_monitoring))
965 .map_err(Error::SettingsRefused)
966}
967
968/// How many threads `render` will be called on, once the settings are
969/// resolved against Bela's defaults.
970///
971/// At least 1: libbela passes `threadCount` through unchanged and only
972/// creates *extra* threads above 1, so 0 and 1 both mean the one thread
973/// that always renders. This is what the audio system sizes the render
974/// states from, and what
975/// [`RenderContext::thread_count`](crate::RenderContext::thread_count)
976/// reports back.
977#[cfg_attr(
978 not(bela_device),
979 allow(
980 dead_code,
981 reason = "only the device-gated audio system applies settings; still unit-tested on the host"
982 )
983)]
984pub(crate) const fn render_threads(raw: &BelaInitSettings) -> usize {
985 let count = raw.threadCount as usize;
986 if count == 0 { 1 } else { count }
987}
988
989#[cfg(test)]
990#[allow(
991 clippy::float_cmp,
992 reason = "the sample rate is copied verbatim, so the expected value is exact"
993)]
994mod tests {
995 use core::mem;
996
997 use super::*;
998 use crate::application::ThreadInfo;
999 use crate::context::{RenderContext, SetupContext};
1000
1001 const FOUR_THREADS: NonZeroU32 = NonZeroU32::new(4).expect("the test thread count is non-zero");
1002
1003 /// What an application refusing too few analog inputs says.
1004 const NEEDS_EIGHT: &str = "this application reads eight analog inputs";
1005 /// What an application refusing extra render threads says.
1006 const NEEDS_ONE_THREAD: &str = "this application renders on one thread";
1007 /// What an application refusing to run unmonitored says.
1008 const NEEDS_MONITORING: &str = "this application reports the CPU usage of every block";
1009
1010 fn cycle_of(measurements: u32) -> NonZeroU32 {
1011 NonZeroU32::new(measurements).expect("the test cycles are non-zero")
1012 }
1013
1014 /// Says nothing about the settings, which is the default hook.
1015 struct Anything;
1016
1017 impl BelaApplication for Anything {
1018 type RenderState = ();
1019
1020 fn create_render_state(&mut self, _thread: ThreadInfo, _context: &SetupContext) {}
1021
1022 fn render(&self, _state: &mut (), _context: &mut RenderContext) {}
1023 }
1024
1025 /// Will not run with fewer analog inputs than it reads.
1026 struct NeedsEightAnalogInputs;
1027
1028 impl BelaApplication for NeedsEightAnalogInputs {
1029 type RenderState = ();
1030
1031 fn validate_settings(&self, settings: &ResolvedSettings<'_>) -> Result<(), &'static str> {
1032 if settings.num_analog_in_channels() < 8 {
1033 return Err(NEEDS_EIGHT);
1034 }
1035 Ok(())
1036 }
1037
1038 fn create_render_state(&mut self, _thread: ThreadInfo, _context: &SetupContext) {}
1039
1040 fn render(&self, _state: &mut (), _context: &mut RenderContext) {}
1041 }
1042
1043 /// Reports the CPU usage of every block, so it will not run
1044 /// without the monitoring that makes the reading exist.
1045 struct NeedsMonitoring;
1046
1047 impl BelaApplication for NeedsMonitoring {
1048 type RenderState = ();
1049
1050 fn validate_settings(&self, settings: &ResolvedSettings<'_>) -> Result<(), &'static str> {
1051 if settings.cpu_monitoring().is_some() {
1052 Ok(())
1053 } else {
1054 Err(NEEDS_MONITORING)
1055 }
1056 }
1057
1058 fn create_render_state(&mut self, _thread: ThreadInfo, _context: &SetupContext) {}
1059
1060 fn render(&self, _state: &mut (), _context: &mut RenderContext) {}
1061 }
1062
1063 /// Built for one render thread, whoever asked for more.
1064 struct SingleThreaded;
1065
1066 impl BelaApplication for SingleThreaded {
1067 type RenderState = ();
1068
1069 fn validate_settings(&self, settings: &ResolvedSettings<'_>) -> Result<(), &'static str> {
1070 if settings.thread_count() == 1 {
1071 Ok(())
1072 } else {
1073 Err(NEEDS_ONE_THREAD)
1074 }
1075 }
1076
1077 fn create_render_state(&mut self, _thread: ThreadInfo, _context: &SetupContext) {}
1078
1079 fn render(&self, _state: &mut (), _context: &mut RenderContext) {}
1080 }
1081
1082 // Stands in for the output of `Bela_defaultSettings()`, which needs
1083 // libbela and therefore the board. The fields are the ones
1084 // `Bela_defaultSettings` sets, so what is built on top of this is
1085 // what the checks below would see on a board — a zeroed structure
1086 // would fail them for a sample rate and a PRU number nobody asked
1087 // for.
1088 fn fake_defaults() -> BelaInitSettings {
1089 let mut raw: BelaInitSettings = unsafe { mem::zeroed() };
1090 raw.audioSampleRate = 44100.0;
1091 raw.periodSize = 16;
1092 raw.useAnalog = 1;
1093 raw.numAnalogInChannels = 8;
1094 raw.numAnalogOutChannels = 8;
1095 raw.numMuxChannels = 0;
1096 raw.pruNumber = 1;
1097 raw.uniformSampleRate = 1;
1098 raw.stopButtonPin = 115;
1099 raw.verbose = 0;
1100 raw.detectUnderruns = 1;
1101 raw.enableLED = 1;
1102 raw
1103 }
1104
1105 // A resolved configuration with the multiplexer on, which is the
1106 // one every rule about it applies to.
1107 fn with_multiplexer(channels: c_int) -> BelaInitSettings {
1108 let mut raw = fake_defaults();
1109 raw.numMuxChannels = channels;
1110 raw
1111 }
1112
1113 #[test]
1114 fn a_whole_configuration_can_be_a_const() {
1115 // The point of `new` being const: this is evaluated at compile
1116 // time, so a builder method that stopped being one would fail
1117 // to compile here rather than fail an assertion.
1118 const SETTINGS: Settings = Settings::new()
1119 .period_size(64)
1120 .thread_count(FOUR_THREADS)
1121 .enable_led(false);
1122
1123 let mut raw = fake_defaults();
1124 SETTINGS.apply_to(&mut raw);
1125
1126 assert_eq!(raw.periodSize, 64);
1127 assert_eq!(raw.threadCount, 4);
1128 assert_eq!(raw.enableLED, 0);
1129 }
1130
1131 #[test]
1132 fn default_is_the_same_empty_set_of_overrides_as_new() {
1133 assert_eq!(Settings::default(), Settings::new());
1134 }
1135
1136 #[test]
1137 fn empty_settings_leave_defaults_untouched() {
1138 let mut raw = fake_defaults();
1139 Settings::new().apply_to(&mut raw);
1140
1141 assert_eq!(raw.periodSize, 16);
1142 assert_eq!(raw.audioSampleRate, 44100.0);
1143 assert_eq!(raw.useAnalog, 1);
1144 assert_eq!(raw.uniformSampleRate, 1);
1145 assert_eq!(raw.stopButtonPin, 115);
1146 assert_eq!(raw.enableLED, 1);
1147 }
1148
1149 #[test]
1150 fn an_audio_sample_rate_is_written_to_the_c_field() {
1151 let mut raw = fake_defaults();
1152 let hz = NonZeroU32::new(48000).expect("48000 is not zero");
1153 Settings::new().audio_sample_rate(hz).apply_to(&mut raw);
1154
1155 assert_eq!(raw.audioSampleRate, 48000.0);
1156 }
1157
1158 #[test]
1159 fn the_command_line_overrides_a_configured_sample_rate() {
1160 // The same order `Bela::new_with_args` applies them in:
1161 // `settings.apply_to` first, `--sample-rate` parsed on top of
1162 // it (`system.rs`'s `init`), so a rate set here is only the one
1163 // the run starts with.
1164 let mut raw = fake_defaults();
1165 let hz = NonZeroU32::new(48000).expect("48000 is not zero");
1166 Settings::new().audio_sample_rate(hz).apply_to(&mut raw);
1167 assert_eq!(raw.audioSampleRate, 48000.0);
1168
1169 raw.audioSampleRate = 96000.0;
1170 assert_eq!(raw.audioSampleRate, 96000.0);
1171 }
1172
1173 #[test]
1174 fn set_fields_override_defaults() {
1175 let mut raw = fake_defaults();
1176 Settings::new()
1177 .period_size(64)
1178 .use_analog(false)
1179 .verbose(true)
1180 .stop_button_pin(None)
1181 .thread_count(FOUR_THREADS)
1182 .apply_to(&mut raw);
1183
1184 assert_eq!(raw.periodSize, 64);
1185 assert_eq!(raw.useAnalog, 0);
1186 assert_eq!(raw.verbose, 1);
1187 assert_eq!(raw.stopButtonPin, -1);
1188 assert_eq!(raw.threadCount, 4);
1189 // Untouched by the overrides above.
1190 assert_eq!(raw.uniformSampleRate, 1);
1191 }
1192
1193 #[test]
1194 fn a_stop_button_pin_is_an_unsigned_gpio_number() {
1195 let mut raw = fake_defaults();
1196 Settings::new().stop_button_pin(Some(27)).apply_to(&mut raw);
1197
1198 assert_eq!(raw.stopButtonPin, 27);
1199
1200 Settings::new()
1201 .stop_button_pin(Some(u32::MAX))
1202 .apply_to(&mut raw);
1203 assert_eq!(raw.stopButtonPin, c_int::MAX);
1204 }
1205
1206 #[test]
1207 fn bools_map_to_c_ints() {
1208 let mut raw = fake_defaults();
1209 Settings::new()
1210 .use_digital(true)
1211 .detect_underruns(false)
1212 .high_performance_mode(true)
1213 .uniform_sample_rate(false)
1214 .enable_led(false)
1215 .begin_muted(true)
1216 .apply_to(&mut raw);
1217
1218 assert_eq!(raw.useDigital, 1);
1219 assert_eq!(raw.detectUnderruns, 0);
1220 assert_eq!(raw.highPerformanceMode, 1);
1221 assert_eq!(raw.uniformSampleRate, 0);
1222 assert_eq!(raw.enableLED, 0);
1223 assert_eq!(raw.beginMuted, 1);
1224 }
1225
1226 #[test]
1227 fn the_leds_can_be_asked_for_as_well_as_declined() {
1228 // Bela's default, but a board's configured `CL=` line can carry
1229 // `--disable-led`, so an application that wants the indicators
1230 // is better saying so than assuming them — the same reason
1231 // `begin_muted(false)` is worth spelling out.
1232 let mut raw = fake_defaults();
1233 raw.enableLED = 0;
1234
1235 Settings::new().enable_led(true).apply_to(&mut raw);
1236
1237 assert_eq!(raw.enableLED, 1);
1238 // Both sides of the accessor's own conversion, so that a
1239 // comparison the wrong way round would fail here rather than
1240 // pass every test that only ever looks at LEDs turned off.
1241 assert!(ResolvedSettings::new(&raw, None).enable_led());
1242 raw.enableLED = 0;
1243 assert!(!ResolvedSettings::new(&raw, None).enable_led());
1244 }
1245
1246 #[test]
1247 fn the_command_line_can_turn_the_leds_off_after_they_were_asked_for() {
1248 // `--disable-led` is parsed after `Settings` is applied
1249 // (`system.rs`'s `init`), and it only clears the flag: there is
1250 // no option that sets it, so this is the one direction the
1251 // command line can move it in.
1252 let mut raw = fake_defaults();
1253 Settings::new().enable_led(true).apply_to(&mut raw);
1254 assert_eq!(raw.enableLED, 1);
1255
1256 raw.enableLED = 0;
1257
1258 assert!(!ResolvedSettings::new(&raw, None).enable_led());
1259 }
1260
1261 #[test]
1262 fn coming_up_unmuted_is_still_said_out_loud() {
1263 // Bela's default, but an application that says so should not
1264 // depend on a board's configured `CL=` line agreeing.
1265 let mut raw = fake_defaults();
1266 raw.beginMuted = 1;
1267
1268 Settings::new().begin_muted(false).apply_to(&mut raw);
1269
1270 assert_eq!(raw.beginMuted, 0);
1271 }
1272
1273 #[test]
1274 fn cpu_monitoring_is_recorded_but_not_an_init_setting() {
1275 let cycle = NonZeroU32::new(2000).expect("2000 is not zero");
1276 let settings = Settings::new().cpu_monitoring(cycle);
1277 assert_eq!(settings.cpu_monitoring_cycle(), Some(cycle));
1278 assert_eq!(
1279 Settings::new().cpu_monitoring_cycle(),
1280 None,
1281 "monitoring should be off unless it was asked for"
1282 );
1283
1284 // It is a separate C call, so it must leave BelaInitSettings
1285 // alone; the audio system applies it itself.
1286 let mut raw = fake_defaults();
1287 let untouched = fake_defaults();
1288 settings.apply_to(&mut raw);
1289 assert_eq!(raw.periodSize, untouched.periodSize);
1290 assert_eq!(raw.useAnalog, untouched.useAnalog);
1291 assert_eq!(raw.uniformSampleRate, untouched.uniformSampleRate);
1292 assert_eq!(raw.stopButtonPin, untouched.stopButtonPin);
1293 }
1294
1295 #[test]
1296 fn both_spellings_of_one_render_thread_count_as_one() {
1297 // libbela creates extra threads only above 1, so a threadCount
1298 // of 0 renders on the one thread that always exists.
1299 for spelling in [0, 1] {
1300 let mut raw: BelaInitSettings = unsafe { mem::zeroed() };
1301 raw.threadCount = spelling;
1302
1303 assert_eq!(render_threads(&raw), 1, "threadCount {spelling}");
1304 }
1305 }
1306
1307 #[test]
1308 fn extra_render_threads_are_counted_as_asked_for() {
1309 let mut raw: BelaInitSettings = unsafe { mem::zeroed() };
1310 raw.threadCount = 4;
1311
1312 assert_eq!(render_threads(&raw), 4);
1313 }
1314
1315 #[test]
1316 fn belas_own_defaults_are_accepted() {
1317 assert_eq!(check_resolved(&fake_defaults()), Ok(()));
1318 }
1319
1320 #[test]
1321 fn a_sample_rate_of_zero_is_refused() {
1322 // What `-r abc` resolves to, `atof` giving 0, and what `-r -5`
1323 // is clamped to. libbela fails initialisation for it with a
1324 // message about a cape.
1325 let mut raw = fake_defaults();
1326 raw.audioSampleRate = 0.0;
1327
1328 assert_eq!(check_resolved(&raw), Err(Error::SampleRate));
1329 }
1330
1331 #[test]
1332 fn both_prus_are_accepted() {
1333 // libbela runs the audio code on either; only the multiplexer
1334 // needs PRU 1, which is a rule of its own.
1335 for number in [0, 1] {
1336 let mut raw = fake_defaults();
1337 raw.pruNumber = number;
1338
1339 assert_eq!(check_resolved(&raw), Ok(()), "PRU {number}");
1340 }
1341 }
1342
1343 #[test]
1344 fn a_pru_the_board_does_not_have_is_refused() {
1345 for number in [-1, 2, 5] {
1346 let mut raw = fake_defaults();
1347 raw.pruNumber = number;
1348
1349 assert_eq!(
1350 check_resolved(&raw),
1351 Err(Error::PruNumber(number)),
1352 "PRU {number}"
1353 );
1354 }
1355 }
1356
1357 #[test]
1358 fn the_multiplexer_channel_counts_libbela_takes_are_accepted() {
1359 // 0 is off; the rest come up and run on a Gem, and stay
1360 // accepted although this crate has no accessor for the buffer
1361 // they fill.
1362 for channels in [0, 2, 4, 8] {
1363 assert_eq!(
1364 check_resolved(&with_multiplexer(channels)),
1365 Ok(()),
1366 "{channels} multiplexer channels"
1367 );
1368 }
1369 }
1370
1371 #[test]
1372 fn any_other_multiplexer_channel_count_is_refused() {
1373 for channels in [-1, 1, 3, 16] {
1374 assert_eq!(
1375 check_resolved(&with_multiplexer(channels)),
1376 Err(Error::MultiplexerChannels(channels)),
1377 "{channels} multiplexer channels"
1378 );
1379 }
1380 }
1381
1382 #[test]
1383 fn the_multiplexer_is_refused_on_the_wrong_pru() {
1384 let mut raw = with_multiplexer(8);
1385 raw.pruNumber = 0;
1386
1387 assert_eq!(check_resolved(&raw), Err(Error::MultiplexerPru(0)));
1388 }
1389
1390 #[test]
1391 fn the_multiplexer_is_refused_without_the_analog_inputs() {
1392 // The combination nothing on the ARM side objects to: without
1393 // this check the PRU firmware ends the process.
1394 let mut raw = with_multiplexer(8);
1395 raw.useAnalog = 0;
1396
1397 assert_eq!(check_resolved(&raw), Err(Error::MultiplexerWithoutAnalog));
1398 }
1399
1400 #[test]
1401 fn the_multiplexer_is_refused_with_any_other_number_of_analog_inputs() {
1402 // 2 and 4 are what `-C 2` and `-C 4` resolve to; `-C 100` snaps
1403 // to 8 and is accepted, so this is the resolved count rather
1404 // than what was written on the command line. 16 is not
1405 // reachable from the command line at all and is from
1406 // `Settings`, which is passed on as it stands — the rule is
1407 // "other than 8", not "fewer than 8".
1408 for channels in [2, 4, 16] {
1409 let mut raw = with_multiplexer(8);
1410 raw.numAnalogInChannels = channels;
1411
1412 assert_eq!(
1413 check_resolved(&raw),
1414 Err(Error::MultiplexerAnalogChannels(channels)),
1415 "{channels} analog input channels"
1416 );
1417 }
1418 }
1419
1420 #[test]
1421 fn the_multiplexer_rules_only_apply_when_it_is_on() {
1422 // PRU 0 and no analog inputs are an ordinary configuration
1423 // until a multiplexer is asked for.
1424 let mut raw = fake_defaults();
1425 raw.pruNumber = 0;
1426 raw.useAnalog = 0;
1427 raw.numAnalogInChannels = 2;
1428
1429 assert_eq!(check_resolved(&raw), Ok(()));
1430 }
1431
1432 #[test]
1433 fn the_view_reports_the_settings_as_they_resolved() {
1434 let mut raw = fake_defaults();
1435 Settings::new()
1436 .period_size(64)
1437 .num_analog_in_channels(4)
1438 .use_digital(false)
1439 .verbose(true)
1440 .begin_muted(true)
1441 .enable_led(false)
1442 .stop_button_pin(Some(27))
1443 .apply_to(&mut raw);
1444 // `--sample-rate 48000` on top, applied where the audio system
1445 // applies the command line, and a thread count from a layer
1446 // `Settings` cannot reach — Bela's defaults or a
1447 // `Bela_userSettings()` hook.
1448 raw.audioSampleRate = 48000.0;
1449 raw.threadCount = 4;
1450
1451 let settings = ResolvedSettings::new(&raw, None);
1452
1453 assert_eq!(settings.period_size(), 64);
1454 assert_eq!(settings.audio_sample_rate(), 48000.0);
1455 assert!(settings.use_analog());
1456 assert_eq!(settings.num_analog_in_channels(), 4);
1457 assert_eq!(settings.num_analog_out_channels(), 8);
1458 assert!(!settings.use_digital());
1459 assert_eq!(settings.thread_count(), 4);
1460 assert!(settings.uniform_sample_rate());
1461 assert!(!settings.high_performance_mode());
1462 assert!(settings.verbose());
1463 assert!(settings.begin_muted());
1464 assert!(!settings.enable_led());
1465 assert!(settings.detect_underruns());
1466 assert_eq!(settings.stop_button_pin(), Some(27));
1467 // The whole structure is still reachable for what has no
1468 // accessor here.
1469 assert_eq!(settings.as_sys().numMuxChannels, 0);
1470 }
1471
1472 #[test]
1473 fn the_view_spells_one_render_thread_and_no_stop_button_the_way_the_rest_of_the_crate_does() {
1474 // A `threadCount` of 0 is libbela's other spelling of the one
1475 // thread that always renders, and a negative stop button pin is
1476 // how monitoring is turned off — the same two conversions
1477 // `render_threads` and `Settings::stop_button_pin` make.
1478 let mut raw = fake_defaults();
1479 raw.threadCount = 0;
1480 raw.stopButtonPin = -1;
1481
1482 let settings = ResolvedSettings::new(&raw, None);
1483
1484 assert_eq!(settings.thread_count(), 1);
1485 assert_eq!(settings.stop_button_pin(), None);
1486 }
1487
1488 #[test]
1489 fn an_application_that_says_nothing_accepts_every_configuration() {
1490 // The default hook: a trait implementation from before this
1491 // existed keeps initialising exactly as it did.
1492 assert_eq!(
1493 check_supported(&fake_defaults(), None, &Anything),
1494 Ok(()),
1495 "the default validate_settings should accept Bela's own defaults"
1496 );
1497 }
1498
1499 #[test]
1500 fn an_application_can_refuse_the_resolved_settings() {
1501 let mut raw = fake_defaults();
1502 Settings::new().num_analog_in_channels(4).apply_to(&mut raw);
1503
1504 assert_eq!(
1505 check_supported(&raw, None, &NeedsEightAnalogInputs),
1506 Err(Error::SettingsRefused(NEEDS_EIGHT))
1507 );
1508 // And accepts the configuration it was built for.
1509 assert_eq!(
1510 check_supported(&fake_defaults(), None, &NeedsEightAnalogInputs),
1511 Ok(())
1512 );
1513 }
1514
1515 #[test]
1516 fn an_application_can_refuse_a_resolved_thread_count() {
1517 // The hook is asked about the resolved settings rather than
1518 // about `Settings`, so a count the application never asked for
1519 // — from Bela's defaults or a `Bela_userSettings()` hook, which
1520 // both have their say before `Settings` is applied — is one it
1521 // can still turn down rather than render wrong.
1522 let mut raw = fake_defaults();
1523 raw.threadCount = 4;
1524
1525 assert_eq!(
1526 check_supported(&raw, None, &SingleThreaded),
1527 Err(Error::SettingsRefused(NEEDS_ONE_THREAD))
1528 );
1529
1530 // And the configuration it is built for is accepted, in both
1531 // of libbela's spellings of one render thread.
1532 for spelling in [0, 1] {
1533 raw.threadCount = spelling;
1534
1535 assert_eq!(
1536 check_supported(&raw, None, &SingleThreaded),
1537 Ok(()),
1538 "threadCount {spelling}"
1539 );
1540 }
1541 }
1542
1543 #[test]
1544 fn an_application_sees_the_cpu_monitoring_that_is_not_in_the_c_settings() {
1545 // `Settings::cpu_monitoring` is a separate C call and leaves
1546 // `BelaInitSettings` untouched, so without it being carried
1547 // into the view an application could not tell an audio system
1548 // that will report CPU usage from one that will not.
1549 let raw = fake_defaults();
1550 let cycle = cycle_of(2000);
1551
1552 assert_eq!(check_supported(&raw, Some(cycle), &NeedsMonitoring), Ok(()));
1553 assert_eq!(
1554 check_supported(&raw, None, &NeedsMonitoring),
1555 Err(Error::SettingsRefused(NEEDS_MONITORING))
1556 );
1557 assert_eq!(
1558 ResolvedSettings::new(&raw, Some(cycle)).cpu_monitoring(),
1559 Some(cycle)
1560 );
1561 assert_eq!(ResolvedSettings::new(&raw, None).cpu_monitoring(), None);
1562 }
1563
1564 #[test]
1565 fn the_crates_own_checks_are_made_before_the_applications() {
1566 // Both would refuse this configuration. The crate's answer is
1567 // the one to report: a sample rate of 0 is not a configuration
1568 // any application could have run under, so naming the
1569 // application as the one that turned it down would name the
1570 // wrong culprit.
1571 let mut raw = fake_defaults();
1572 raw.audioSampleRate = 0.0;
1573 raw.numAnalogInChannels = 4;
1574
1575 assert_eq!(
1576 check_supported(&raw, None, &NeedsEightAnalogInputs),
1577 Err(Error::SampleRate)
1578 );
1579 }
1580
1581 #[test]
1582 fn the_cpu_monitoring_period_size_is_checked_before_the_application_is_asked() {
1583 let mut raw = fake_defaults();
1584 raw.periodSize = c_int::try_from(crate::MAX_MONITORED_PERIOD_SIZE)
1585 .expect("the monitored period size limit fits in a C int")
1586 + 1;
1587 raw.numAnalogInChannels = 4;
1588
1589 let period_size = u32::try_from(raw.periodSize).expect("the period size above is positive");
1590 assert_eq!(
1591 check_supported(&raw, Some(cycle_of(2000)), &NeedsEightAnalogInputs),
1592 Err(Error::CpuMonitoringPeriodSize(period_size))
1593 );
1594 // Without monitoring there is no such limit, and the
1595 // application is the only one with anything to say.
1596 assert_eq!(
1597 check_supported(&raw, None, &NeedsEightAnalogInputs),
1598 Err(Error::SettingsRefused(NEEDS_EIGHT))
1599 );
1600 }
1601
1602 #[test]
1603 fn a_settings_and_a_command_line_half_make_the_same_refusal() {
1604 // The reason this is checked on the resolved settings: the
1605 // multiplexer from one place and four analog inputs from the
1606 // other is the same mistake as both from either.
1607 let mut from_settings = fake_defaults();
1608 Settings::new()
1609 .num_analog_in_channels(4)
1610 .apply_to(&mut from_settings);
1611 from_settings.numMuxChannels = 8;
1612
1613 assert_eq!(
1614 check_resolved(&from_settings),
1615 Err(Error::MultiplexerAnalogChannels(4))
1616 );
1617 }
1618}