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
//! Output levels and input gain: the codec's analogue volume controls.
//!
//! Four knobs, all of them on the codec rather than in the signal the
//! application renders: the line out level, the headphone level, the
//! audio input gain and the speaker amplifier's mute. They belong to
//! the audio system, so they are set through the
//! [`Bela`](crate::Bela) handle that owns it —
//! [`set_line_out_level`](crate::Bela::set_line_out_level) and the
//! rest.
//!
//! # Why they are not settings
//!
//! `BelaInitSettings` carries the same three gains as
//! `BelaChannelGainArray` fields, which reads like the natural home for
//! them. It is not: libbela applies those arrays by calling exactly the
//! functions wrapped here, from inside `Bela_initAudio`, and the codec
//! only writes its registers once audio starts. Setting a level between
//! [`Bela::new`](crate::Bela::new) and
//! [`Bela::start`](crate::Bela::start) therefore reaches the hardware in
//! the same state and at the same moment as a settings-time gain would,
//! which is why [`Bela::until_stopped`] is public: it is
//! [`Bela::run`](crate::Bela::run) with that window left open.
//!
//! That leaves nothing for [`Settings`](crate::Settings) to carry but
//! the arrays' storage and a second way to say the same thing — the
//! exception being
//! [`Settings::begin_muted`](crate::Settings::begin_muted), which
//! changes what `Bela_startAudio` does and so cannot be expressed as a
//! call before it.
//!
//! # What a level may be
//!
//! Any finite number of decibels up to [`MAX_DECIBELS`] in magnitude.
//! The codec clamps what it cannot do — Bela's own range for each knob
//! is on the method — but a value libbela could not convert into
//! register values at all is refused here rather than passed on: the
//! conversion is a C cast to `int`, which is undefined for a NaN or an
//! out-of-range float, and every clamp on the C side is a comparison a
//! NaN slips through.
//!
//! # Not real-time safe
//!
//! Each call talks to the codec over I²C, which makes no promises about
//! how long it takes; Bela's own documentation says not to call these
//! from `render`. Taking them on the `Bela` handle keeps them out of
//! reach there: `render` gets a
//! [`RenderContext`](crate::RenderContext), and the
//! handle stays with the thread that owns the audio system.
//!
//! # On a Bela Gem Stereo
//!
//! What the codec does with a level is hardware-specific, and this is
//! what was measured on the board (see `docs/board-facts.md`):
//!
//! - only channels 0 and 1 exist. [`Channel::One`] above that is
//!   refused for the line out and the headphone output, and accepted
//!   but ignored for the input gain.
//! - a level outside the codec's range is clamped rather than refused,
//!   so nothing reports that `+18` dB on the line out became `+9`.
//! - the input gain has a floor of -12 dB and a step of 1.5 dB below
//!   zero, where it is the ADC's attenuator rather than the
//!   preamplifier —
//!   [`set_audio_input_gain`](crate::Bela::set_audio_input_gain) has
//!   both halves.
//! - the board's output level is
//!   [`set_headphone_level`](crate::Bela::set_headphone_level), not
//!   [`set_line_out_level`](crate::Bela::set_line_out_level): the
//!   latter reports success and leaves the output where it was. The
//!   two calls write different registers of the codec, and on this
//!   board only the ones behind the headphone level reach what leaves
//!   it.
//! - there is no amplifier mute pin, so
//!   [`mute_speakers`](crate::Bela::mute_speakers) and
//!   [`Settings::begin_muted`](crate::Settings::begin_muted) succeed
//!   without doing anything. They are wrapped for the Bela hardware
//!   that does have one.
//!
//! [`Bela::until_stopped`]: crate::Bela::until_stopped

use core::ffi::c_int;

#[cfg(bela_device)]
use crate::application::BelaApplication;
use crate::error::Error;
#[cfg(bela_device)]
use crate::system::Bela;

/// Largest magnitude, in decibels, a level or gain may have.
///
/// Not a codec limit — every codec clamps long before this, and
/// `docs/board-facts.md` records where. It is the point past which
/// libbela cannot convert the value at all: it turns decibels into
/// register values with C casts of `floorf(decibels * 2 + 0.5)` and
/// `decibels * 2` to `int`, and converting a float that does not fit in
/// an `int` — or is not a number at all — is undefined behaviour in
/// C++. So [`Bela::set_line_out_level`](crate::Bela::set_line_out_level)
/// and its siblings refuse those values instead of passing them on;
/// see [`Error::Decibels`](crate::Error::Decibels).
pub const MAX_DECIBELS: f32 = 1e9;

/// Checks that libbela can convert `decibels` into register values.
///
/// The clamping each codec does is comparisons against its own range,
/// which a NaN fails on the way in and on the way out, so it reaches
/// the cast like any other value. Nothing downstream can defend
/// against that; this is where it has to happen.
///
/// # Errors
/// Returns [`Error::Decibels`] for a value that is not finite, or whose
/// magnitude is above [`MAX_DECIBELS`].
#[cfg_attr(
    not(bela_device),
    allow(
        dead_code,
        reason = "only the device-gated audio system sets levels; still unit-tested on the host"
    )
)]
fn check_decibels(decibels: f32) -> Result<(), Error> {
    if decibels.is_finite() && decibels.abs() <= MAX_DECIBELS {
        Ok(())
    } else {
        Err(Error::Decibels)
    }
}

/// Which channel a level or gain applies to.
///
/// Channels are numbered as the context's audio channels are, from
/// zero.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Channel {
    /// Every channel the codec has.
    All,
    /// One channel, counted the way
    /// [`RenderContext::audio_out_channels`](crate::RenderContext::audio_out_channels)
    /// counts them.
    One(usize),
}

impl Channel {
    /// The C spelling, where a negative channel number means "all".
    ///
    /// A channel number too large for a C `int` saturates rather than
    /// wrapping: the wrapped value could be negative, which would set
    /// every channel instead of reporting a channel that does not
    /// exist.
    #[cfg_attr(
        not(bela_device),
        allow(
            dead_code,
            reason = "only the device-gated audio system sets levels; still unit-tested on the host"
        )
    )]
    const fn to_sys(self) -> c_int {
        match self {
            Self::All => -1,
            #[allow(
                clippy::cast_possible_truncation,
                clippy::cast_possible_wrap,
                reason = "the comparison rules out the values that would truncate or wrap"
            )]
            Self::One(channel) if channel <= c_int::MAX as usize => channel as c_int,
            Self::One(_) => c_int::MAX,
        }
    }
}

#[cfg(bela_device)]
impl<T: BelaApplication> Bela<T> {
    /// Sets the level of the line output, in decibels.
    ///
    /// Zero is full scale and negative values attenuate; how far in
    /// either direction depends on the codec. On a Bela Gem Stereo the
    /// codec takes channels 0 and 1, attenuation in 0.5 dB steps down
    /// to -63.5 dB and boost up to +9 dB, and clamps a value outside
    /// that without reporting it — but what the board puts out does not
    /// follow any of it, which is the section below.
    ///
    /// Takes effect immediately once audio is running, and is otherwise
    /// remembered and applied when it starts — so this is also how a
    /// program that uses [`until_stopped`](Bela::until_stopped) sets
    /// the level audio comes up with. The example on
    /// [`until_stopped`](Bela::until_stopped) is that program, and
    /// [`examples/levels.rs`][example] is a whole one that sets all
    /// four controls and reports what each call returned.
    ///
    /// # No effect on a Bela Gem Stereo's output
    ///
    /// That board's audio output does not change with this level: a
    /// 440 Hz tone recorded off it came out at the same amplitude with
    /// the line out set to 0, -12 and -24 dB, and every call reported
    /// success. What moves it there is
    /// [`set_headphone_level`](Bela::set_headphone_level): the same
    /// measurement had the output following that call dB for dB. The
    /// two write different registers of the codec, and on this board
    /// only the set behind the headphone level reaches what leaves it
    /// — see `docs/board-facts.md` for the measurement and the
    /// registers. Other Bela hardware is not covered by it: this stays
    /// the call for the line out where a board has one.
    ///
    /// # Errors
    /// Returns [`Error::LineOutLevel`] when the codec refuses the call,
    /// which on a Bela Gem Stereo is what a channel above 1 gets, and
    /// [`Error::Decibels`] for a level libbela could not convert — see
    /// [`MAX_DECIBELS`](crate::MAX_DECIBELS).
    ///
    /// [example]: https://github.com/akiomik/bela-rs/blob/main/bela/examples/levels.rs
    pub fn set_line_out_level(&mut self, channel: Channel, decibels: f32) -> Result<(), Error> {
        check_decibels(decibels)?;
        // Safety: an audio system exists — this needs the handle that
        // owns it — and libbela's own settings path calls this the same
        // way, from the thread that brought the audio system up.
        let ret = unsafe { bela_sys::Bela_setLineOutLevel(channel.to_sys(), decibels) };
        if ret == 0 {
            Ok(())
        } else {
            Err(Error::LineOutLevel(ret))
        }
    }

    /// Sets the level of the onboard headphone amplifier, in decibels.
    ///
    /// The headphone output only, as far as libbela documents it: the
    /// line out and the speakers are unaffected. Bela's documented
    /// range is -63.5 dB to 0 dB in 0.5 dB steps, and the default is
    /// -6 dB. Like [`set_line_out_level`](Bela::set_line_out_level), it
    /// applies at once while audio runs and is otherwise applied when
    /// it starts.
    ///
    /// # It is the output level on a Bela Gem Stereo
    ///
    /// On that board this is the level of what comes out, not of a
    /// separate headphone output: a 440 Hz tone recorded off it lost
    /// 23.26 dB for the 24 dB asked of this call, while the same 24 dB
    /// asked of [`set_line_out_level`](Bela::set_line_out_level) left
    /// it where it was. See `docs/board-facts.md`.
    ///
    /// # Errors
    /// Returns [`Error::HeadphoneLevel`] when the codec refuses the
    /// call, which on a Bela Gem Stereo is what a channel above 1 gets,
    /// and [`Error::Decibels`] for a level libbela could not convert —
    /// see [`MAX_DECIBELS`](crate::MAX_DECIBELS).
    pub fn set_headphone_level(&mut self, channel: Channel, decibels: f32) -> Result<(), Error> {
        check_decibels(decibels)?;
        // Safety: as for `set_line_out_level`.
        let ret = unsafe { bela_sys::Bela_setHpLevel(channel.to_sys(), decibels) };
        if ret == 0 {
            Ok(())
        } else {
            Err(Error::HeadphoneLevel(ret))
        }
    }

    /// Sets the gain of the audio input, in decibels.
    ///
    /// Above zero this is the programmable gain amplifier ahead of the
    /// ADC, so it changes what the audio inputs actually sample — turn
    /// it up for a quiet source rather than scaling in `render`, which
    /// only amplifies the noise the ADC already digitised. Below zero
    /// it is a separate attenuator at the ADC's input, with a range of
    /// its own. Neither half affects the analog inputs.
    ///
    /// Bela's documented range is the amplifier's: 0 dB to 59.5 dB in
    /// 0.5 dB steps, with 16 dB the default. A negative gain is
    /// accepted but not documented, and what it does is the codec's —
    /// the section below is a Bela Gem Stereo's answer.
    ///
    /// # Two controls on a Bela Gem Stereo
    ///
    /// Above zero this is the preamplifier, and the board follows it:
    /// +6 dB measured +6.01 dB at the input.
    ///
    /// Below zero it is not. libbela pins the preamplifier at 0 dB and
    /// attenuates in the ADC's own input control instead, which has
    /// eight steps of 1.5 dB and no ninth — so **-12 dB is as quiet as
    /// this call gets**: -13.5, -18 and -24 dB all measured the same
    /// as -12 dB. Between 0 and -12 dB the request is taken toward
    /// zero to a multiple of 1.5 dB, which makes -1 dB the same as
    /// 0 dB and -4 dB the same as -3 dB. At or below -96 dB the
    /// amplifier is muted outright and nothing arrives at all; between
    /// that and -12 dB there is nothing to ask for.
    ///
    /// Every one of those calls reports success, so a source loud
    /// enough to clip the ADC at -12 dB has to be attenuated before it
    /// reaches the board — this call has nothing left to give.
    /// Hardware with a different codec has its own floor; see
    /// `docs/board-facts.md` for the measurement and the codec path
    /// behind it.
    ///
    /// # Errors
    /// Returns [`Error::AudioInputGain`] when the codec refuses the
    /// call — a Bela Gem Stereo does not: a channel it does not have is
    /// accepted and ignored — and [`Error::Decibels`] for a gain
    /// libbela could not convert, see
    /// [`MAX_DECIBELS`](crate::MAX_DECIBELS).
    pub fn set_audio_input_gain(&mut self, channel: Channel, decibels: f32) -> Result<(), Error> {
        check_decibels(decibels)?;
        // Safety: as for `set_line_out_level`.
        let ret = unsafe { bela_sys::Bela_setAudioInputGain(channel.to_sys(), decibels) };
        if ret == 0 {
            Ok(())
        } else {
            Err(Error::AudioInputGain(ret))
        }
    }

    /// Mutes or unmutes the onboard speaker amplifiers.
    ///
    /// This drives the amplifier's mute pin, so it silences the
    /// speakers without touching any level. Which state audio comes up
    /// in is
    /// [`Settings::begin_muted`](crate::Settings::begin_muted):
    /// [`start`](Bela::start) unmutes unless that asked otherwise, so
    /// muting before it has no effect.
    ///
    /// A Bela Gem Stereo has no amplifier mute pin — measured, see
    /// `docs/board-facts.md` — and libbela then reports success without
    /// doing anything.
    ///
    /// # Errors
    /// Returns [`Error::MuteSpeakers`] when libbela refuses the call.
    pub fn mute_speakers(&mut self, mute: bool) -> Result<(), Error> {
        // Safety: as for `set_line_out_level`.
        let ret = unsafe { bela_sys::Bela_muteSpeakers(c_int::from(mute)) };
        if ret == 0 {
            Ok(())
        } else {
            Err(Error::MuteSpeakers(ret))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn all_channels_is_belas_negative_channel() {
        assert_eq!(Channel::All.to_sys(), -1);
    }

    #[test]
    fn a_channel_number_is_passed_through() {
        assert_eq!(Channel::One(0).to_sys(), 0);
        assert_eq!(Channel::One(1).to_sys(), 1);
        assert_eq!(
            Channel::One(c_int::MAX.unsigned_abs() as usize).to_sys(),
            c_int::MAX
        );
    }

    #[test]
    fn ordinary_levels_are_accepted() {
        for decibels in [0.0, -6.0, 16.0, -63.5, 59.5, -200.0, MAX_DECIBELS] {
            assert_eq!(check_decibels(decibels), Ok(()), "{decibels} dB");
        }
        // The codec clamps its own range; refusing here would be this
        // crate inventing a limit libbela does not have.
        assert_eq!(check_decibels(-MAX_DECIBELS), Ok(()));
    }

    #[test]
    fn a_level_libbela_could_not_convert_is_refused() {
        // Each of these reaches a C cast to `int` — the clamps on the
        // way are comparisons a NaN fails — and that cast is undefined
        // behaviour for them, so they must not get that far.
        for decibels in [
            f32::NAN,
            f32::INFINITY,
            f32::NEG_INFINITY,
            MAX_DECIBELS * 2.0,
            -MAX_DECIBELS * 2.0,
            f32::MIN,
            f32::MAX,
        ] {
            assert_eq!(check_decibels(decibels), Err(Error::Decibels), "{decibels}");
        }
    }

    #[test]
    fn the_accepted_levels_survive_belas_conversion() {
        // What libbela does with a level: `floorf(dB * 2 + 0.5)` cast
        // to `int`. The limit is only worth anything if the largest
        // accepted value still fits there.
        // In f64, which holds these exactly, so the check is about the
        // limit rather than about its own rounding.
        let half_dbs = f64::from(MAX_DECIBELS) * 2.0;
        let converted = (half_dbs + 0.5).floor();

        assert!(
            converted <= f64::from(c_int::MAX),
            "{converted} does not fit in the C int libbela casts to"
        );
    }

    #[test]
    fn a_channel_number_too_large_for_a_c_int_saturates() {
        // Wrapping would turn a channel that does not exist into
        // "every channel", which is the one answer that must not
        // happen.
        for channel in [c_int::MAX.unsigned_abs() as usize + 1, usize::MAX] {
            assert!(
                Channel::One(channel).to_sys() > 0,
                "channel {channel} must not arrive as a request to set every channel"
            );
        }
    }
}