use core::ffi::c_int;
#[cfg(bela_device)]
use crate::application::BelaApplication;
use crate::error::Error;
#[cfg(bela_device)]
use crate::system::Bela;
pub const MAX_DECIBELS: f32 = 1e9;
#[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)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Channel {
All,
One(usize),
}
impl Channel {
#[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> {
pub fn set_line_out_level(&mut self, channel: Channel, decibels: f32) -> Result<(), Error> {
check_decibels(decibels)?;
let ret = unsafe { bela_sys::Bela_setLineOutLevel(channel.to_sys(), decibels) };
if ret == 0 {
Ok(())
} else {
Err(Error::LineOutLevel(ret))
}
}
pub fn set_headphone_level(&mut self, channel: Channel, decibels: f32) -> Result<(), Error> {
check_decibels(decibels)?;
let ret = unsafe { bela_sys::Bela_setHpLevel(channel.to_sys(), decibels) };
if ret == 0 {
Ok(())
} else {
Err(Error::HeadphoneLevel(ret))
}
}
pub fn set_audio_input_gain(&mut self, channel: Channel, decibels: f32) -> Result<(), Error> {
check_decibels(decibels)?;
let ret = unsafe { bela_sys::Bela_setAudioInputGain(channel.to_sys(), decibels) };
if ret == 0 {
Ok(())
} else {
Err(Error::AudioInputGain(ret))
}
}
pub fn mute_speakers(&mut self, mute: bool) -> Result<(), Error> {
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");
}
assert_eq!(check_decibels(-MAX_DECIBELS), Ok(()));
}
#[test]
fn a_level_libbela_could_not_convert_is_refused() {
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() {
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() {
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"
);
}
}
}