Skip to main content

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}