bela 0.8.0

Safe Rust API for real-time audio on Bela Gem
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
use core::ffi::c_int;
use core::fmt;
use core::marker::PhantomData;
use core::time::Duration;
use std::ffi::OsStr;
use std::thread;

use bela_sys::BelaInitSettings;

use crate::application::BelaApplication;
use crate::cmdline::{self, Arguments};
use crate::cpu;
use crate::error::Error;
use crate::runtime::{Runtime, trampoline, user_data};
use crate::settings::{self, Settings};
use crate::singleton::Claim;
use crate::task;

/// Owns the `BelaInitSettings` allocation while it is being filled in,
/// so that leaving [`Bela::new`] frees it however the leaving happens.
///
/// The settings are libbela's to allocate and free —
/// `Bela_InitSettings_alloc` hands back a structure with heap behind
/// its gain arrays — and they are wanted for exactly as long as the
/// `Bela_initAudio` call they are the argument to. Freeing at each
/// `return` was enough while everything in between was this crate's
/// own code. It stopped being enough with
/// [`BelaApplication::validate_settings`], which is application code
/// called from inside that window: a panic there unwinds past every
/// `return`, and a caller that catches it around [`Bela::new`] would
/// leak the allocation once per attempt. None of that is undefined
/// behaviour, which is why a guard and nothing stronger — but a
/// program that retries in a loop should not lose memory for it.
struct InitSettings {
    raw: *mut BelaInitSettings,
}

impl InitSettings {
    /// Allocates the settings and takes ownership of them.
    fn alloc() -> Self {
        Self {
            // Safety: no arguments and no state to get wrong; the
            // pointer belongs to this value from here on.
            raw: unsafe { bela_sys::Bela_InitSettings_alloc() },
        }
    }

    /// The pointer libbela's own functions take, borrowed for as long
    /// as this guard is.
    #[allow(
        clippy::needless_pass_by_ref_mut,
        reason = "the settings are written through the pointer, so an exclusive borrow is what \
                  keeps a second writer out; the compiler cannot see that through a raw pointer"
    )]
    const fn as_mut_ptr(&mut self) -> *mut BelaInitSettings {
        self.raw
    }
}

impl Drop for InitSettings {
    fn drop(&mut self) {
        // Safety: allocated by `Bela_InitSettings_alloc` and freed
        // exactly once, since nothing else holds the pointer.
        unsafe { bela_sys::Bela_InitSettings_free(self.raw) };
    }
}

/// Owns an initialised Bela audio system and the application driven by
/// it.
///
/// Construction initialises the audio system (`Bela_initAudio`);
/// dropping the value stops audio if needed, runs `cleanup`
/// (`Bela_cleanupAudio`) and frees the application. For the common
/// "run until stopped" case, use [`Bela::run`].
///
/// Only one `Bela` may exist at a time: the underlying C API is a
/// process-wide singleton. A second [`new`](Bela::new) fails with
/// [`Error::AudioSystemExists`] rather than reaching into globals the
/// first one is using — from this thread or any other.
///
/// One at a time, and in some processes none at all: once a
/// `Bela_initAudio` has failed here, every later [`new`](Bela::new)
/// fails with [`Error::AudioSystemPoisoned`], which that method
/// explains.
///
/// Only available on the device target (`aarch64-unknown-linux-gnu`).
pub struct Bela<T: BelaApplication> {
    // Owned; boxed so the address handed to libbela stays stable, kept
    // raw so the audio threads' access is never aliased by a &mut.
    runtime: *mut Runtime<T>,
    started: bool,
    /// Released once this audio system is gone, so the next one can be
    /// built. Declared last: fields drop after `Drop::drop`, so the
    /// claim outlives the teardown that runs there.
    _claim: Claim,
    _marker: PhantomData<T>,
}

impl<T: BelaApplication> Bela<T> {
    /// Initialises the audio system with `application` and `settings`
    /// applied on top of `Bela_defaultSettings()`.
    ///
    /// # Errors
    /// Returns [`Error::AudioSystemExists`] when another audio system
    /// is alive in this process, [`Error::AudioSystemPoisoned`] when an
    /// earlier initialisation in this process failed, [`Error::Init`]
    /// when `Bela_initAudio` fails, e.g. when the audio hardware is
    /// unavailable or already in use, and [`Error::CpuMonitoringCycle`],
    /// [`Error::CpuMonitoringPeriodSize`] or [`Error::CpuMonitoring`]
    /// when [`Settings::cpu_monitoring`] asks for something that cannot
    /// be served.
    ///
    /// Before any of that it returns [`Error::SampleRate`],
    /// [`Error::PruNumber`] or one of the four
    /// `Error::Multiplexer…` variants for a resolved configuration
    /// libbela would refuse inside `Bela_initAudio`, or not refuse at
    /// all and leave to the PRU firmware. These are as reachable from
    /// here as from [`new_with_args`](Bela::new_with_args), because the
    /// board's own `CL=` line in `~/.bela/belaconfig` goes through
    /// Bela's parser inside `Bela_defaultSettings`; the checks are
    /// listed there.
    ///
    /// Last of the checks made before the audio system is built, and
    /// the only one that is the application's own:
    /// [`Error::SettingsRefused`] when
    /// [`BelaApplication::validate_settings`] declined the resolved
    /// settings, carrying the reason it gave. Like the others it costs
    /// the attempt and nothing else — the process can build an audio
    /// system afterwards with settings the application does accept,
    /// which is what separates it from a refusal in
    /// [`setup`](BelaApplication::setup).
    ///
    /// # A failed initialisation is fatal to the process
    ///
    /// [`Error::Init`] means `Bela_initAudio` failed partway through,
    /// and libbela keeps no record of how far it got. Whatever the
    /// attempt had already taken is still held — up to and including
    /// the audio hardware, and the CPU monitoring counters this crate
    /// turns on just before the call when [`Settings::cpu_monitoring`]
    /// asked for them — and this crate does not call
    /// `Bela_cleanupAudio` to hand any of it back, because on that path
    /// the call itself segfaults. That is measured rather than assumed,
    /// including in the order this method would have to make the call;
    /// see "Audio thread" in `docs/board-facts.md`.
    ///
    /// So the process-wide claim is released as unusable rather than
    /// free, and every later `Bela::new` in this process fails with
    /// [`Error::AudioSystemPoisoned`] without touching libbela. That
    /// refusal is the whole of what this crate can do about it: going
    /// ahead is not a worse-behaved audio system but a segfault, on a
    /// Bela Gem reported as `Mcasp::start() called while already
    /// running` in every run measured.
    ///
    /// What is unusable is this process, not the board. A new process
    /// gets a working audio system straight away, with nothing to reset
    /// in between — so treat this error as a reason to exit, and leave
    /// retrying to whatever started the program.
    ///
    /// The ordinary way to arrive here is a
    /// [`setup`](BelaApplication::setup) callback returning `false`,
    /// which fails the initialisation after the hardware is up. An
    /// application that is refusing the *configuration* rather than
    /// something it found on the board should say so from
    /// [`validate_settings`](BelaApplication::validate_settings)
    /// instead, which is asked before any of this happens.
    pub fn new(application: T, settings: &Settings) -> Result<Self, Error> {
        Self::init(application, settings, None)
    }

    /// Initialises the audio system like [`new`](Bela::new), with
    /// Bela's standard command-line options applied on top of
    /// `settings`.
    ///
    /// `args` is the whole argument list with the program name first,
    /// as [`std::env::args_os()`] yields it; parsing starts at the
    /// second entry, the way a C `main` does. The options are the ones
    /// every other way of writing a Bela program accepts — `--period`,
    /// `--verbose`, `--use-analog` and the rest, printed by
    /// [`print_usage`](crate::print_usage) — and they are applied last,
    /// so the application keeps the defaults it was built with but can
    /// still be reconfigured without rebuilding.
    ///
    /// Options of the program's own are not part of the set: parse them
    /// first, with whatever argument parser the program already uses,
    /// and hand on what is left. See
    /// [`examples/command_line.rs`][example]. An argument that is not
    /// an option at all is ignored — `getopt` moves those to the end of
    /// the list and never reports them, which is also what Bela's C
    /// program templates do with them.
    ///
    /// # Errors
    /// In addition to the errors [`new`](Bela::new) returns:
    /// [`Error::CommandLine`] when an argument is not one of the
    /// standard options, is missing its value or is otherwise rejected,
    /// and [`Error::CommandLineNul`] when an argument contains a NUL
    /// byte.
    ///
    /// # What is checked before the audio system is built
    ///
    /// Six combinations are refused once the defaults, `settings` and
    /// the command line have all been applied, and before
    /// `Bela_initAudio` is called:
    ///
    /// | resolved settings | error |
    /// |---|---|
    /// | a sample rate of 0, which is what `--sample-rate` gives for anything `atof` cannot read and what a negative rate is clamped to | [`Error::SampleRate`] |
    /// | `--pru-number` other than 0 or 1 | [`Error::PruNumber`] |
    /// | `--mux-channels` other than 0, 2, 4 or 8 | [`Error::MultiplexerChannels`] |
    /// | `--mux-channels` with `--pru-number 0` | [`Error::MultiplexerPru`] |
    /// | `--mux-channels` with `--use-analog 0` | [`Error::MultiplexerWithoutAnalog`] |
    /// | `--mux-channels` with a number of analog input channels other than 8 | [`Error::MultiplexerAnalogChannels`] |
    ///
    /// None of them is a working configuration that has been taken
    /// away. Each was measured on a Gem Stereo failing in a place that
    /// costs the caller more than an error does.
    ///
    /// Five of the six fail inside `Bela_initAudio` — the sample rate
    /// in `Bela_getHwConfigPrivate`, the two PRU rules in `RTAudio.cpp`'s
    /// initial sanity checks, and the multiplexer channel and analog
    /// input counts in `PRU::initialise` — and what that costs is not
    /// the attempt but the process: no audio system can be built after
    /// it, as [`new`](Bela::new) describes.
    ///
    /// The sixth, the multiplexer with the analog inputs off, is the
    /// one libbela does not check at all: its count rules sit behind an
    /// `if` that analog being off skips, so the settings reach the PRU
    /// firmware, which gives up — `Invalid PRU configuration settings`,
    /// `PRU timeout`, `McASP error, abort` — and ends the process from
    /// inside libbela with nothing returned to anyone.
    ///
    /// A seventh refusal is the application's own, and is about what it
    /// can run under rather than what libbela can:
    /// [`BelaApplication::validate_settings`] is asked about the same
    /// resolved settings, after these six and before anything is done
    /// about them, and what it declines becomes
    /// [`Error::SettingsRefused`]. That is where "this program needs
    /// six analog inputs" or "this program needs the analog channels
    /// resampled to the audio rate" belongs — especially here, where
    /// the command line is what may have changed them out from under
    /// the application's own [`Settings`].
    ///
    /// # What is passed through
    ///
    /// Everything else the C runtime accepts, including the options
    /// this crate has no other way of asking for.
    ///
    /// `--mux-channels 2`, `4` and `8` are among them: a Gem brings the
    /// multiplexer up and the PRU fills the demultiplexed buffer, and
    /// no accessor here reaches it — `multiplexerAnalogRead` and
    /// `multiplexerChannelForFrame` are left out on purpose, because
    /// the Capelet cannot be attached to the board this crate is
    /// measured against. So the option takes effect and its readings
    /// stay out of reach. That is a gap in what this crate offers
    /// rather than a reason to refuse the flag.
    ///
    /// libbela also reshapes several values rather than refusing them,
    /// and this crate does not copy those rules — a copy would drift
    /// from the library it describes:
    ///
    /// - `--analog-channels` snaps to 8, 4 or 2. `-C 0` and `-C 3` both
    ///   give **2** analog inputs, not none and not three; `--use-analog 0`
    ///   is how to have none.
    /// - `--digital-channels` clamps to 16, and 0 turns the digital
    ///   channels off altogether.
    /// - `--board` naming hardware that is not there is ignored, an
    ///   unrecognised name included.
    /// - `--stop-button-pin` out of range runs on without a working
    ///   stop button, and `--codec-mode`, `--disabled-digital-channels`
    ///   and the audio expander options are accepted and do nothing
    ///   visible on a Gem.
    ///
    /// Two period sizes have no check either, in neither direction: a
    /// Gem Stereo with its eight analog inputs cannot keep up with
    /// `--period 1` or `--period 3` and dies in the PRU as above, while
    /// 2 and everything from 4 up run — and both failures move as soon
    /// as the analog configuration does, so there is no floor a check
    /// could hold. At the other end, a period of 256 frames or more
    /// moves the callback behind libbela's context FIFO: a digital
    /// direction and value set only once then fail to persist at the
    /// physical pin while everything reports success. Re-applying both
    /// in every application block restores the measured output loopback;
    /// see [`Settings::period_size`].
    ///
    /// `--sample-rate` is the same story: it is parsed after `settings`
    /// has already been applied, so it overrides
    /// [`Settings::audio_sample_rate`] rather than being overridden by
    /// it. Anything `atof` cannot read, and a negative rate, both
    /// resolve to 0 and are then refused with [`Error::SampleRate`]
    /// below — nothing above this method's own checks understands a
    /// board's ceiling on what the rate can be, so a value past it is
    /// passed on and can still abort the process from inside the codec;
    /// see [`Settings::audio_sample_rate`].
    ///
    /// # A malformed `--json-string` ends the process
    ///
    /// `--json-string {` throws an uncaught `nlohmann::json` exception
    /// and the process ends on `SIGABRT`. It happens inside the parse —
    /// `Bela_getopt_long`, which this method calls — and so before
    /// `Bela_initAudio` and before the checks above. There is nothing
    /// to return and nothing to catch: the call this crate made does
    /// not come back.
    ///
    /// Refusing the option here would not close that path and would
    /// take away valid JSON, which works: `Bela_defaultSettings` runs
    /// the board's own `CL=` line from `~/.bela/belaconfig` through the
    /// same parser, so the same abort is reachable from
    /// [`new`](Bela::new), which looks at no arguments at all.
    /// The neighbouring option is better behaved — `--json-file`
    /// naming a missing file warns and carries on with the settings it
    /// already had.
    ///
    /// [example]: https://github.com/akiomik/bela-rs/blob/main/bela/examples/command_line.rs
    pub fn new_with_args<I, S>(application: T, settings: &Settings, args: I) -> Result<Self, Error>
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        // Before the claim: copying the arguments touches nothing an
        // audio system owns, and a list C cannot represent is worth
        // refusing whether or not one exists.
        let arguments = Arguments::new(args)?;
        Self::init(application, settings, Some(arguments))
    }

    /// Initialises the audio system, optionally letting `arguments`
    /// have the last word on the settings.
    fn init(
        application: T,
        settings: &Settings,
        mut arguments: Option<Arguments>,
    ) -> Result<Self, Error> {
        // First, because everything below reaches into libbela's
        // globals — including, before `Bela_initAudio` is called at
        // all, the CPU monitoring counters an audio system that is
        // already running would be writing.
        let mut claim = Claim::take()?;
        // Checked before anything is allocated, so a cycle libbela
        // cannot take costs nothing to reject.
        let monitoring = settings
            .cpu_monitoring_cycle()
            .map(cpu::check_cycle)
            .transpose()?;
        let mut init_settings = InitSettings::alloc();
        let (ret, runtime) = unsafe {
            let raw = init_settings.as_mut_ptr();
            bela_sys::Bela_defaultSettings(raw);
            settings.apply_to(&mut *raw);
            // The command line last, so it overrides the defaults the
            // application was built with rather than the other way
            // around: being able to reconfigure a binary from outside
            // is the whole point of it.
            //
            // Then refuse what the safe API cannot serve — and what the
            // application says it cannot run under — rather than
            // initialising something unsound, and turn monitoring on.
            // All of it once the settings are fully resolved, including
            // whatever the command line just changed, but before
            // `Bela_initAudio`, since the `setup` callback runs inside
            // that call and should already see the answer
            // `SetupContext::cpu_usage` will give for the rest of the
            // run. The priming tic it takes is what the audio thread's
            // first reading is measured from, so everything from here
            // to `start` is startup time that reading includes.
            //
            // Monitoring is applied after the checks and not before,
            // because it is the first thing here that changes anything:
            // an application that refuses the settings should leave the
            // counters as it found them.
            let prepared = arguments
                .as_mut()
                .map_or(Ok(()), |arguments| cmdline::parse(arguments, &mut *raw))
                .and_then(|()| {
                    settings::check_supported(&*raw, settings.cpu_monitoring_cycle(), &application)
                })
                .and_then(|()| cpu::apply_monitoring(monitoring));
            // No free here, and none below: `init_settings` owns the
            // allocation and frees it however this scope is left.
            prepared?;
            // Built here rather than earlier, because how many render
            // states it needs is a resolved setting like any other:
            // this is the `threadCount` everything above has settled
            // on, and the one libbela is about to be handed.
            let runtime = Box::into_raw(Box::new(Runtime::new(
                application,
                settings::render_threads(&*raw),
            )));
            (*raw).setup = Some(trampoline::setup::<T>);
            (*raw).render_pre = Some(trampoline::render_pre::<T>);
            (*raw).render = Some(trampoline::render::<T>);
            (*raw).render_post = Some(trampoline::render_post::<T>);
            (*raw).cleanup = Some(trampoline::cleanup::<T>);
            let ret = bela_sys::Bela_initAudio(raw, user_data(runtime));
            (ret, runtime)
        };
        // libbela has copied whatever it keeps by now: the settings
        // were only ever an argument to the call above.
        drop(init_settings);
        if ret != 0 {
            // The audio system never took ownership of the callbacks.
            drop(unsafe { Box::from_raw(runtime) });
            // Every failure above this point leaves libbela in a state
            // the next attempt can resolve — CPU monitoring can be left
            // initialised, but the next `new` applies or disables it
            // either way. This one cannot be resolved: `Bela_initAudio`
            // got partway and no call undoes it, so the claim is
            // released as unusable rather than free and the next `new`
            // is refused instead of segfaulting.
            claim.poison();
            return Err(Error::Init(ret));
        }
        Ok(Self {
            runtime,
            started: false,
            _claim: claim,
            _marker: PhantomData,
        })
    }

    /// Starts the real-time audio thread.
    ///
    /// # Errors
    /// Returns [`Error::Start`] when `Bela_startAudio` fails.
    pub fn start(&mut self) -> Result<(), Error> {
        if self.started {
            return Ok(());
        }
        let ret = unsafe { bela_sys::Bela_startAudio() };
        if ret != 0 {
            return Err(Error::Start(ret));
        }
        self.started = true;
        Ok(())
    }

    /// Stops the real-time audio thread. Also happens on drop.
    ///
    /// Auxiliary tasks do not survive this: the handles an application
    /// holds are retired, and creating one while it runs fails with
    /// [`Error::TaskCreateWhileStopping`].
    pub fn stop(&mut self) {
        if self.started {
            task::teardown(|| self.stop_audio());
        }
    }

    /// Stops audio if it is running, without touching the task
    /// lifecycle; callers do that around it.
    fn stop_audio(&mut self) {
        if self.started {
            unsafe { bela_sys::Bela_stopAudio() };
            self.started = false;
        }
    }

    /// How many callbacks this audio system has refused, while it was
    /// running, for breaking the protocol its render states rely on.
    ///
    /// Zero for every run that behaved. Anything else means libbela
    /// made a callback somewhere the crate could not hand out the
    /// references [`BelaApplication`] promises — several `render` calls
    /// with the same thread number, say. The callback was skipped and a
    /// stop requested, so this is a reason a run ended, not damage that
    /// can be undone.
    ///
    /// Refusals *during* a shutdown are not counted here, and are not a
    /// fault in the same sense: libbela abandons the block it is in
    /// when a stop arrives, which can leave a `render_post` overlapping
    /// a `render` that has not finished. Refusing that is the guard
    /// working. Keeping the two apart is what lets an ordinary Ctrl-C
    /// stay an ordinary Ctrl-C, and
    /// [`until_stopped`](Bela::until_stopped) reports the count for
    /// those separately rather than failing on it.
    ///
    /// [`until_stopped`](Bela::until_stopped) reads this for you and
    /// fails with [`Error::CallbackFaults`]; this is for a program
    /// driving [`start`](Bela::start) and [`stop`](Bela::stop) itself.
    #[must_use]
    pub fn callback_faults(&self) -> u32 {
        // Safety: the runtime is alive for as long as `self` is, and
        // reading the counter is an atomic load.
        unsafe { &*self.runtime }.faults()
    }

    /// How many callbacks this audio system has refused *during* a
    /// shutdown, which is a different thing from
    /// [`callback_faults`](Bela::callback_faults) and not a failure.
    ///
    /// libbela abandons the block it is in when a stop arrives: the
    /// secondary render threads check the stop flag just before calling
    /// `render`, and the main thread stops waiting for them on the same
    /// flag, so a `render_post` can arrive while a `render` is still
    /// finishing. Refusing that is the guard doing its job.
    ///
    /// Non-zero means the last block was cut short — some of its frames
    /// were never rendered, and its `render_post` may not have run.
    /// That is worth knowing for a program that counts frames rather
    /// than blocks, which is why it can be asked.
    /// [`until_stopped`](Bela::until_stopped) reports it on the console
    /// instead; this is for a program driving [`start`](Bela::start) and
    /// [`stop`](Bela::stop) itself, which has no other way to find out.
    #[must_use]
    pub fn callback_faults_while_stopping(&self) -> u32 {
        // Safety: the runtime is alive for as long as `self` is, and
        // reading the counter is an atomic load.
        unsafe { &*self.runtime }.faults_while_stopping()
    }

    /// Initialises and starts the audio system, blocks until a stop is
    /// requested, then shuts down.
    ///
    /// Installs SIGINT/SIGTERM/SIGHUP handlers that request a stop, so
    /// Ctrl-C, `systemctl stop` and a dropped ssh connection all shut
    /// down cleanly (mirroring the C example templates).
    ///
    /// Note that Ctrl-C only reaches the program when ssh allocates a
    /// terminal (`ssh -t`); otherwise it just kills the local client
    /// and leaves the program running on the board.
    ///
    /// # Errors
    /// Returns [`Error::Init`] or [`Error::Start`] when the audio
    /// system fails to initialise or start. What an [`Error::Init`]
    /// leaves behind is described on [`new`](Bela::new); it is fatal to
    /// the process here too. Returns [`Error::CallbackFaults`] when the
    /// run ended because a callback was refused, so `Ok(())` means the
    /// run was stopped by someone asking it to.
    pub fn run(application: T, settings: &Settings) -> Result<(), Error> {
        Self::new(application, settings)?.until_stopped()
    }

    /// Runs like [`run`](Bela::run), with Bela's standard command-line
    /// options applied on top of `settings`.
    ///
    /// See [`new_with_args`](Bela::new_with_args) for what they are,
    /// where they sit, which of them are checked before the audio
    /// system is built and which are passed on to libbela as they
    /// stand.
    ///
    /// # Errors
    /// The errors of [`new_with_args`](Bela::new_with_args), plus the
    /// [`Error::Start`] and [`Error::CallbackFaults`] of
    /// [`run`](Bela::run).
    pub fn run_with_args<I, S>(application: T, settings: &Settings, args: I) -> Result<(), Error>
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        Self::new_with_args(application, settings, args)?.until_stopped()
    }

    /// Starts the audio system, blocks until a stop is requested, then
    /// shuts down — [`run`](Bela::run) without the construction.
    ///
    /// This is the way to run an audio system that had something said
    /// to it between [`new`](Bela::new) and the run loop, rather than
    /// reimplementing the loop and its signal handling to get that
    /// window. Setting a level is the case it exists for:
    ///
    /// ```no_run
    /// use bela::{Bela, Channel, Settings};
    /// # use bela::{BelaApplication, RenderContext, SetupContext, ThreadInfo};
    /// # struct App;
    /// # impl BelaApplication for App {
    /// #     type RenderState = ();
    /// #     fn create_render_state(&mut self, _t: ThreadInfo, _c: &SetupContext) {}
    /// #     fn render(&self, _s: &mut (), _c: &mut RenderContext) {}
    /// # }
    ///
    /// fn main() -> Result<(), bela::Error> {
    ///     let mut bela = Bela::new(App, &Settings::new())?;
    ///     bela.set_audio_input_gain(Channel::All, 30.0)?;
    ///     bela.until_stopped()
    /// }
    /// ```
    ///
    /// Installs the same SIGINT/SIGTERM/SIGHUP handlers
    /// [`run`](Bela::run) does; see it for what they mean over ssh.
    ///
    /// # Errors
    /// Returns [`Error::Start`] when the audio system fails to start,
    /// and [`Error::CallbackFaults`] when the run ended because a
    /// callback was refused *while it was running* — a stop asked for
    /// by the crate rather than by anyone else, which `Ok(())` would
    /// otherwise hide.
    ///
    /// A callback refused during the shutdown itself is not that, and
    /// does not fail this: libbela abandons the block it is in when a
    /// stop arrives, and refusing a `render_post` that overlaps a
    /// `render` still finishing is the guard doing its job on an
    /// ordinary Ctrl-C. Those are reported on the console instead, and
    /// mean the last block may be short. See
    /// [`callback_faults`](Bela::callback_faults).
    pub fn until_stopped(mut self) -> Result<(), Error> {
        let handler = request_stop_on_signal as extern "C" fn(c_int);
        for signal in [libc::SIGINT, libc::SIGTERM, libc::SIGHUP] {
            unsafe { libc::signal(signal, handler as libc::sighandler_t) };
        }
        self.start()?;
        while !crate::stop_requested() {
            thread::sleep(Duration::from_millis(10));
        }
        self.stop();
        // Read once audio has stopped, so that every render, render_pre
        // and render_post of the run has been counted. The `cleanup`
        // callback runs later, in the drop below, and so is not covered
        // by either number — nothing here could report it, since the
        // counters go with the runtime it is dropped along with. What
        // it could be refused for is a claim it cannot take, a thread
        // count that disagrees, or states that were never built; the
        // render threads are joined by now and the other two were
        // settled in `setup`, so none of them can happen on this path.
        //
        let while_stopping = self.callback_faults_while_stopping();
        if while_stopping != 0 {
            // Not a failure: this is libbela abandoning the block it
            // was in when the stop arrived, and the guard declining to
            // hand out references over the top of it. Said out loud all
            // the same, because an application that counts frames will
            // see the block go missing.
            crate::rt_println!(
                "bela: {while_stopping} callback(s) were refused while stopping, which is how a \
                 block in flight is abandoned; the last block may be short"
            );
        }
        match self.callback_faults() {
            0 => Ok(()),
            faults => Err(Error::CallbackFaults(faults)),
        }
    }
}

/// What can be said about an audio system without disturbing it:
/// whether it is running, and the two callback fault counts.
///
/// Written by hand rather than derived so that an application type
/// does not have to be [`Debug`] for the handle that owns it to be —
/// nothing of the application is printed. The rest of the state is the
/// pointer to the runtime and the process-wide claim, neither of which
/// says anything a reader of a debug line could use.
impl<T: BelaApplication> fmt::Debug for Bela<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Bela")
            .field("started", &self.started)
            .field("callback_faults", &self.callback_faults())
            .field(
                "callback_faults_while_stopping",
                &self.callback_faults_while_stopping(),
            )
            .finish_non_exhaustive()
    }
}

// Async-signal-safe: Bela_requestStop only sets a flag.
extern "C" fn request_stop_on_signal(_signal: c_int) {
    crate::request_stop();
}

impl<T: BelaApplication> Drop for Bela<T> {
    fn drop(&mut self) {
        // One teardown window over the whole shutdown, including the
        // cleanup callback and the case where audio was never started:
        // each Bela version deletes the auxiliary tasks somewhere in
        // here, and no handle may look live while that happens.
        task::teardown(|| {
            self.stop_audio();
            // Runs the cleanup callback, which still borrows the app.
            unsafe { bela_sys::Bela_cleanupAudio() };
        });
        // The app is only freed once the callback can no longer run.
        drop(unsafe { Box::from_raw(self.runtime) });
    }
}