Skip to main content

io_config/
io_config.rs

1//! Hardware probe for how a board configures its analog and digital
2//! I/O.
3//!
4//! Written to answer the first half of #11. The analog and digital
5//! accessors on the context types have only ever run against hand-built
6//! contexts, and two of the claims their documentation rests on — that
7//! a Gem's analog outputs are its audio outputs with a +2 channel
8//! offset, and that `uniformSampleRate` is on by default — come from
9//! Bela's migration guide rather than from a measurement. Both are
10//! about numbers a board reports, so both can be settled before any
11//! wire is connected. `scripts/probe-io.sh` drives this, and what it
12//! finds belongs in `docs/board-facts.md`.
13//!
14//! Nothing here reads or writes a sample. Confirming that a pin does
15//! what the accessor says needs a voltage on it, which is the other
16//! half of #11 and a different instrument.
17//!
18//! It is not a check with a right answer, so it is not part of
19//! `scripts/smoke-test.sh`: what it prints is the board's description
20//! of itself, and a pass/fail gate has nothing to compare that against
21//! until the facts are written down.
22//!
23//! # What each run does
24//!
25//! - `hardware` — what libbela says the board is, before any audio
26//!   system exists: the detected board and the version of the library
27//!   that detected it, the `BelaHwConfig` that hardware implies, and
28//!   the analog and digital fields of `Bela_defaultSettings`. The only
29//!   probe here that brings nothing up, so it is also the only one
30//!   whose answer cannot depend on the settings it was asked for.
31//! - `context [options]` — brings one audio system up with the options
32//!   given, reports the `BelaContext` that `setup` sees and the one the
33//!   first block sees, renders for a moment and tears it down. The
34//!   options are the ones that decide the shape of the block:
35//!
36//!   ```text
37//!   --period <frames>        --uniform on|off
38//!   --analog on|off          --digital on|off
39//!   --analog-in <channels>   --analog-out <channels>
40//!   --digital-channels <channels>
41//!   ```
42//!
43//!   Unset options are left to `Bela_defaultSettings`, which is the
44//!   point: the run with no options at all is the one that says what a
45//!   board does when nobody asks for anything.
46//!
47//! # Both ends of the block are reported
48//!
49//! `setup` and the first block are asked the same questions because
50//! they need not give the same answers: `setup` runs inside
51//! `Bela_initAudio`, before an audio thread exists, and a frame count
52//! that is only filled in once one does would show up as a difference
53//! between the two. The accessors are documented as if there were no
54//! difference, so a difference is a finding.
55//!
56//! # One configuration per process
57//!
58//! A failed `Bela_initAudio` poisons the process it happened in
59//! (`docs/board-facts.md`), and every configuration here is one that
60//! might fail. Each run therefore takes one configuration and exits,
61//! and the driving script starts a fresh process for the next.
62//!
63//! Cross-compile and run on the board (see docs/cross-compile.md):
64//!
65//! ```sh
66//! cargo build -p bela --release --target aarch64-unknown-linux-gnu --example io_config
67//! ```
68
69#![cfg_attr(
70    not(bela_device),
71    allow(
72        dead_code,
73        reason = "only the fallback main is reachable off-device; the probe code should still compile and lint"
74    )
75)]
76
77use std::process::ExitCode;
78
79use bela::{BelaApplication, BlockContext, RenderContext, SetupContext, ThreadInfo, rt_println};
80
81/// Reports the shape of the block from both ends of it.
82struct Report {
83    /// Whether the next block is the first one. The report is per run,
84    /// not per block: at 2760 blocks a second the second one would only
85    /// bury the first.
86    first_block: bool,
87}
88
89impl BelaApplication for Report {
90    type RenderState = ();
91
92    fn setup(&mut self, context: &SetupContext) -> bool {
93        // `println!`, not `rt_println!`: `setup` runs inside
94        // `Bela_initAudio` before there is an audio thread, and a
95        // flushed write is what a script reading this over ssh needs.
96        // The block report below is on the audio thread and cannot use
97        // it.
98        report_context("setup", context);
99        true
100    }
101
102    fn create_render_state(&mut self, _thread: ThreadInfo, _context: &SetupContext) {}
103
104    fn render_pre(&mut self, _states: &mut [()], context: &mut BlockContext) {
105        if !self.first_block {
106            return;
107        }
108        self.first_block = false;
109        // Bela's own real-time printing, because this is the audio
110        // thread. One key per line: a message is capped at
111        // `MESSAGE_CAPACITY` bytes and a truncated one would read as a
112        // missing field.
113        rt_println!(
114            "io-config: block-audio=frames:{},in:{},out:{},rate:{}",
115            context.audio_frames(),
116            context.audio_in_channels(),
117            context.audio_out_channels(),
118            context.audio_sample_rate()
119        );
120        rt_println!(
121            "io-config: block-analog=frames:{},in:{},out:{},rate:{}",
122            context.analog_frames(),
123            context.analog_in_channels(),
124            context.analog_out_channels(),
125            context.analog_sample_rate()
126        );
127        rt_println!(
128            "io-config: block-digital=frames:{},channels:{},rate:{}",
129            context.digital_frames(),
130            context.digital_channels(),
131            context.digital_sample_rate()
132        );
133        rt_println!(
134            "io-config: block-threads=this:{},count:{},counted:{}",
135            context.as_sys().thisThread,
136            context.as_sys().threadCount,
137            context.thread_count()
138        );
139    }
140
141    // Silence: this probe is about how the audio system was configured,
142    // not about what it renders.
143    fn render(&self, _state: &mut (), _context: &mut RenderContext) {}
144}
145
146/// Reports one step, on the spot.
147///
148/// Flushed, because stdout is a pipe when the driving script is
149/// listening and an audio system that goes on to fail should not take
150/// what was already established with it.
151fn report(key: &str, value: &str) {
152    use std::io::{self, Write};
153
154    println!("io-config: {key}={value}");
155    let _ = io::stdout().flush();
156}
157
158/// The whole shape of the block, as `setup` sees it.
159///
160/// Split across three lines by domain, so that a run with analog or
161/// digital disabled shows a line of zeros rather than a gap.
162fn report_context(phase: &str, context: &SetupContext) {
163    report(
164        &format!("{phase}-audio"),
165        &format!(
166            "frames:{},in:{},out:{},rate:{}",
167            context.audio_frames(),
168            context.audio_in_channels(),
169            context.audio_out_channels(),
170            context.audio_sample_rate()
171        ),
172    );
173    report(
174        &format!("{phase}-analog"),
175        &format!(
176            "frames:{},in:{},out:{},rate:{}",
177            context.analog_frames(),
178            context.analog_in_channels(),
179            context.analog_out_channels(),
180            context.analog_sample_rate()
181        ),
182    );
183    report(
184        &format!("{phase}-digital"),
185        &format!(
186            "frames:{},channels:{},rate:{}",
187            context.digital_frames(),
188            context.digital_channels(),
189            context.digital_sample_rate()
190        ),
191    );
192    // Raw as well as counted. `thread_count()` reads a `threadCount`
193    // of 0 as 1, which is a `BelaContext`'s other way of spelling one
194    // render thread — so the crate's number cannot say which of the
195    // two libbela wrote, and a record of board behaviour wants the
196    // field as it stands.
197    report(
198        &format!("{phase}-threads"),
199        &format!(
200            "this:{},count:{},counted:{}",
201            context.as_sys().thisThread,
202            context.as_sys().threadCount,
203            context.thread_count()
204        ),
205    );
206}
207
208#[cfg(bela_device)]
209mod probes {
210    use core::time::Duration;
211    use std::thread;
212
213    use bela::bela_sys::{
214        Bela_HwConfig_delete, Bela_HwConfig_new, Bela_InitSettings_alloc, Bela_InitSettings_free,
215        Bela_defaultSettings,
216    };
217    use bela::{Bela, Board, DetectMode, Settings, Version};
218
219    use super::{Report, report};
220
221    /// How long the audio system renders before it is torn down. Long
222    /// enough that a working one reports thousands of blocks, so that
223    /// "came up but never rendered" cannot be mistaken for it.
224    const RENDER_TIME: Duration = Duration::from_secs(1);
225
226    /// Reads the cached detection rather than scanning.
227    ///
228    /// `DetectMode::Cache` is what the daemon leaves behind in
229    /// `/run/bela/belaconfig`, and a scan would go out over I²C to find
230    /// out something this probe only wants reported.
231    const DETECT_CACHED: DetectMode = DetectMode::Cache;
232
233    /// The board with the number it is, so that the record says
234    /// `GemStereo(2)` rather than either on its own.
235    ///
236    /// A value the crate has no name for is worth more than "unknown":
237    /// it means the board's libbela knows a hardware the vendored
238    /// headers do not, and [`Board::Unrecognised`] prints as
239    /// `unrecognised(<n>)` rather than losing the number.
240    fn hardware_name(board: Board) -> String {
241        if board.is_recognised() {
242            format!("{board}({raw})", raw = board.to_sys())
243        } else {
244            board.to_string()
245        }
246    }
247
248    /// What the board is, and what libbela expects of it, with no audio
249    /// system anywhere in the picture.
250    pub(crate) fn hardware() {
251        let board = Board::detect(DETECT_CACHED);
252        report("detect-hw", &hardware_name(board));
253        // The library that answered, which is not necessarily the one
254        // this was built against: every number below is a claim about a
255        // particular libbela, and this is which one.
256        report("version", &Version::running().to_string());
257        let hw = board.to_sys();
258
259        // The configuration libbela associates with that hardware,
260        // which is where a Gem's channel counts come from before any
261        // settings are applied. Null is an answer too: it is what a
262        // hardware libbela has no configuration for looks like.
263        let config = unsafe { Bela_HwConfig_new(hw) };
264        if config.is_null() {
265            report("hw-config", "null");
266        } else {
267            let config_ref = unsafe { &*config };
268            report(
269                "hw-config",
270                &format!(
271                    "rate:{},audio-in:{},audio-out:{},analog-in:{},analog-out:{},digital:{}",
272                    config_ref.audioSampleRate,
273                    config_ref.audioInChannels,
274                    config_ref.audioOutChannels,
275                    config_ref.analogInChannels,
276                    config_ref.analogOutChannels,
277                    config_ref.digitalChannels
278                ),
279            );
280            unsafe { Bela_HwConfig_delete(config) };
281        }
282
283        // The defaults an application inherits by setting nothing,
284        // which is what `Settings`'s "unset fields keep the values
285        // produced by `Bela_defaultSettings()`" means in practice.
286        let raw = unsafe { Bela_InitSettings_alloc() };
287        if raw.is_null() {
288            report("defaults", "alloc-failed");
289            return;
290        }
291        unsafe { Bela_defaultSettings(raw) };
292        let defaults = unsafe { &*raw };
293        report(
294            "defaults-analog",
295            &format!(
296                "use:{},in:{},out:{},uniform:{}",
297                defaults.useAnalog,
298                defaults.numAnalogInChannels,
299                defaults.numAnalogOutChannels,
300                defaults.uniformSampleRate
301            ),
302        );
303        report(
304            "defaults-digital",
305            &format!(
306                "use:{},channels:{}",
307                defaults.useDigital, defaults.numDigitalChannels
308            ),
309        );
310        report(
311            "defaults-audio",
312            &format!(
313                "period:{},rate:{},threads:{}",
314                defaults.periodSize, defaults.audioSampleRate, defaults.threadCount
315            ),
316        );
317        unsafe { Bela_InitSettings_free(raw) };
318    }
319
320    /// One audio system, one configuration, both ends of one block.
321    pub(crate) fn context(settings: &Settings) {
322        let app = Report { first_block: true };
323        let mut bela = match Bela::new(app, settings) {
324            Ok(bela) => bela,
325            Err(error) => {
326                // A configuration the board will not have is a finding,
327                // not a failure of the probe: which combinations are
328                // refused is part of what #11 asks.
329                report("init", &format!("failed-{error:?}"));
330                return;
331            }
332        };
333        report("init", "created");
334        if let Err(error) = bela.start() {
335            report("start", &format!("failed-{error:?}"));
336            return;
337        }
338        report("start", "started");
339        thread::sleep(RENDER_TIME);
340        drop(bela);
341        report("run", "stopped");
342    }
343}
344
345/// Turns the command line into the settings to bring an audio system up
346/// with.
347///
348/// Every option is optional and an unset one is left to
349/// `Bela_defaultSettings`, so `context` with no options is a valid run
350/// and the interesting one.
351#[cfg(bela_device)]
352fn parse_settings(arguments: &[String]) -> Option<bela::Settings> {
353    fn switch(value: &str) -> Option<bool> {
354        match value {
355            "on" => Some(true),
356            "off" => Some(false),
357            _ => None,
358        }
359    }
360
361    let mut settings = bela::Settings::new();
362    let mut rest = arguments.iter();
363    while let Some(option) = rest.next() {
364        let value = rest.next()?;
365        settings = match option.as_str() {
366            "--period" => settings.period_size(value.parse().ok()?),
367            "--uniform" => settings.uniform_sample_rate(switch(value)?),
368            "--analog" => settings.use_analog(switch(value)?),
369            "--digital" => settings.use_digital(switch(value)?),
370            "--analog-in" => settings.num_analog_in_channels(value.parse().ok()?),
371            "--analog-out" => settings.num_analog_out_channels(value.parse().ok()?),
372            "--digital-channels" => settings.num_digital_channels(value.parse().ok()?),
373            _ => return None,
374        };
375    }
376    Some(settings)
377}
378
379#[cfg(bela_device)]
380fn main() -> ExitCode {
381    use std::env::args;
382
383    let arguments: Vec<String> = args().skip(1).collect();
384    match arguments.split_first() {
385        Some((probe, rest)) if probe == "hardware" && rest.is_empty() => probes::hardware(),
386        Some((probe, rest)) if probe == "context" => {
387            let Some(settings) = parse_settings(rest) else {
388                eprintln!("context: cannot read the options {rest:?}");
389                return ExitCode::FAILURE;
390            };
391            // Echoed so that a log read on its own says which run it is.
392            let asked_for = if rest.is_empty() {
393                "defaults".to_owned()
394            } else {
395                rest.join(" ")
396            };
397            report("settings", &asked_for);
398            probes::context(&settings);
399        }
400        _ => {
401            eprintln!(
402                "usage: io_config (hardware | context [options])\n\
403                 \x20 options: --period <frames> | --uniform on|off | --analog on|off\n\
404                 \x20          | --digital on|off | --analog-in <channels>\n\
405                 \x20          | --analog-out <channels> | --digital-channels <channels>\n\
406                 one configuration per run: a failed initialisation poisons its process"
407            );
408            return ExitCode::FAILURE;
409        }
410    }
411    ExitCode::SUCCESS
412}
413
414#[cfg(not(bela_device))]
415fn main() -> ExitCode {
416    eprintln!("This example must be cross-compiled for Bela Gem (aarch64-unknown-linux-gnu).");
417    ExitCode::FAILURE
418}