use core::slice;
use bela_sys::BelaContext;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PinMode {
Input,
Output,
}
const DIGITAL_VALUE_SHIFT: usize = 16;
#[repr(transparent)]
pub struct Context(BelaContext);
impl Context {
pub unsafe fn from_mut_ptr<'a>(ptr: *mut BelaContext) -> &'a mut Self {
unsafe { &mut *ptr.cast::<Self>() }
}
#[must_use]
pub const fn as_sys(&self) -> &BelaContext {
&self.0
}
pub const unsafe fn as_sys_mut(&mut self) -> &mut BelaContext {
&mut self.0
}
#[must_use]
pub const fn audio_frames(&self) -> usize {
self.0.audioFrames as usize
}
#[must_use]
pub const fn audio_in_channels(&self) -> usize {
self.0.audioInChannels as usize
}
#[must_use]
pub const fn audio_out_channels(&self) -> usize {
self.0.audioOutChannels as usize
}
#[must_use]
pub const fn audio_sample_rate(&self) -> f32 {
self.0.audioSampleRate
}
#[must_use]
pub const fn analog_frames(&self) -> usize {
self.0.analogFrames as usize
}
#[must_use]
pub const fn analog_in_channels(&self) -> usize {
self.0.analogInChannels as usize
}
#[must_use]
pub const fn analog_out_channels(&self) -> usize {
self.0.analogOutChannels as usize
}
#[must_use]
pub const fn analog_sample_rate(&self) -> f32 {
self.0.analogSampleRate
}
#[must_use]
pub const fn digital_frames(&self) -> usize {
self.0.digitalFrames as usize
}
#[must_use]
pub const fn digital_channels(&self) -> usize {
self.0.digitalChannels as usize
}
#[must_use]
pub const fn digital_sample_rate(&self) -> f32 {
self.0.digitalSampleRate
}
#[must_use]
pub const fn audio_frames_elapsed(&self) -> u64 {
self.0.audioFramesElapsed
}
#[must_use]
pub const fn underrun_count(&self) -> u32 {
self.0.underrunCount
}
#[must_use]
pub const fn this_thread(&self) -> u32 {
self.0.thisThread
}
#[must_use]
pub const fn thread_count(&self) -> u32 {
self.0.threadCount
}
#[must_use]
pub const fn audio_in(&self) -> &[f32] {
unsafe {
shared(
self.0.audioIn,
self.audio_frames() * self.audio_in_channels(),
)
}
}
pub const fn audio_out(&mut self) -> &mut [f32] {
unsafe {
exclusive(
self.0.audioOut,
self.audio_frames() * self.audio_out_channels(),
)
}
}
#[must_use]
pub const fn analog_in(&self) -> &[f32] {
unsafe {
shared(
self.0.analogIn,
self.analog_frames() * self.analog_in_channels(),
)
}
}
pub const fn analog_out(&mut self) -> &mut [f32] {
unsafe {
exclusive(
self.0.analogOut,
self.analog_frames() * self.analog_out_channels(),
)
}
}
#[must_use]
pub const fn digital(&self) -> &[u32] {
unsafe { shared(self.0.digital, self.digital_frames()) }
}
pub const fn digital_mut(&mut self) -> &mut [u32] {
unsafe { exclusive(self.0.digital, self.digital_frames()) }
}
#[must_use]
pub fn audio_read(&self, frame: usize, channel: usize) -> f32 {
let channels = self.audio_in_channels();
assert!(channel < channels, "audio input channel out of range");
self.audio_in()[frame * channels + channel]
}
pub fn audio_write(&mut self, frame: usize, channel: usize, value: f32) {
let channels = self.audio_out_channels();
assert!(channel < channels, "audio output channel out of range");
self.audio_out()[frame * channels + channel] = value;
}
#[must_use]
pub fn analog_read(&self, frame: usize, channel: usize) -> f32 {
let channels = self.analog_in_channels();
assert!(channel < channels, "analog input channel out of range");
self.analog_in()[frame * channels + channel]
}
pub fn analog_write(&mut self, frame: usize, channel: usize, value: f32) {
let channels = self.analog_out_channels();
assert!(channel < channels, "analog output channel out of range");
let frames = self.analog_frames();
let out = self.analog_out();
for f in frame..frames {
out[f * channels + channel] = value;
}
}
pub fn analog_write_once(&mut self, frame: usize, channel: usize, value: f32) {
let channels = self.analog_out_channels();
assert!(channel < channels, "analog output channel out of range");
self.analog_out()[frame * channels + channel] = value;
}
#[must_use]
pub fn digital_read(&self, frame: usize, channel: usize) -> bool {
let mask = self.digital_value_mask(channel);
self.digital()[frame] & mask != 0
}
pub fn digital_write(&mut self, frame: usize, channel: usize, value: bool) {
let mask = self.digital_value_mask(channel);
for word in self.digital_mut().iter_mut().skip(frame) {
set_bits(word, mask, value);
}
}
pub fn digital_write_once(&mut self, frame: usize, channel: usize, value: bool) {
let mask = self.digital_value_mask(channel);
set_bits(&mut self.digital_mut()[frame], mask, value);
}
pub fn pin_mode(&mut self, frame: usize, channel: usize, mode: PinMode) {
let mask = self.digital_direction_mask(channel);
for word in self.digital_mut().iter_mut().skip(frame) {
set_bits(word, mask, mode == PinMode::Input);
}
}
pub fn pin_mode_once(&mut self, frame: usize, channel: usize, mode: PinMode) {
let mask = self.digital_direction_mask(channel);
set_bits(&mut self.digital_mut()[frame], mask, mode == PinMode::Input);
}
fn digital_value_mask(&self, channel: usize) -> u32 {
assert!(
channel < self.digital_channels(),
"digital channel out of range"
);
1 << (channel + DIGITAL_VALUE_SHIFT)
}
fn digital_direction_mask(&self, channel: usize) -> u32 {
assert!(
channel < self.digital_channels(),
"digital channel out of range"
);
1 << channel
}
}
const unsafe fn shared<'a, T>(ptr: *const T, len: usize) -> &'a [T] {
if ptr.is_null() {
&[]
} else {
unsafe { slice::from_raw_parts(ptr, len) }
}
}
const unsafe fn exclusive<'a, T>(ptr: *mut T, len: usize) -> &'a mut [T] {
if ptr.is_null() {
&mut []
} else {
unsafe { slice::from_raw_parts_mut(ptr, len) }
}
}
const fn set_bits(word: &mut u32, mask: u32, on: bool) {
if on {
*word |= mask;
} else {
*word &= !mask;
}
}
#[cfg(test)]
#[allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::float_cmp,
reason = "tests use small exact values where these casts and comparisons are lossless"
)]
mod tests {
use core::mem;
use super::*;
const AUDIO_FRAMES: usize = 4;
const AUDIO_IN_CHANNELS: usize = 2;
const AUDIO_OUT_CHANNELS: usize = 4;
const ANALOG_FRAMES: usize = 4;
const ANALOG_IN_CHANNELS: usize = 4;
const ANALOG_OUT_CHANNELS: usize = 2;
const DIGITAL_FRAMES: usize = 4;
const DIGITAL_CHANNELS: usize = 16;
struct Fixture {
audio_in: Vec<f32>,
audio_out: Vec<f32>,
analog_in: Vec<f32>,
analog_out: Vec<f32>,
digital: Vec<u32>,
context: BelaContext,
}
impl Fixture {
fn new() -> Box<Self> {
let audio_in: Vec<f32> = (0..AUDIO_FRAMES * AUDIO_IN_CHANNELS)
.map(|i| {
let (frame, channel) = (i / AUDIO_IN_CHANNELS, i % AUDIO_IN_CHANNELS);
(frame * 10 + channel) as f32
})
.collect();
let analog_in: Vec<f32> = (0..ANALOG_FRAMES * ANALOG_IN_CHANNELS)
.map(|i| {
let (frame, channel) = (i / ANALOG_IN_CHANNELS, i % ANALOG_IN_CHANNELS);
(frame * 10 + channel) as f32
})
.collect();
let mut fixture = Box::new(Self {
audio_in,
audio_out: vec![0.0; AUDIO_FRAMES * AUDIO_OUT_CHANNELS],
analog_in,
analog_out: vec![0.0; ANALOG_FRAMES * ANALOG_OUT_CHANNELS],
digital: vec![0; DIGITAL_FRAMES],
context: unsafe { mem::zeroed() },
});
fixture.context.audioIn = fixture.audio_in.as_ptr();
fixture.context.audioOut = fixture.audio_out.as_mut_ptr();
fixture.context.analogIn = fixture.analog_in.as_ptr();
fixture.context.analogOut = fixture.analog_out.as_mut_ptr();
fixture.context.digital = fixture.digital.as_mut_ptr();
fixture.context.audioFrames = AUDIO_FRAMES as u32;
fixture.context.audioInChannels = AUDIO_IN_CHANNELS as u32;
fixture.context.audioOutChannels = AUDIO_OUT_CHANNELS as u32;
fixture.context.audioSampleRate = 44100.0;
fixture.context.analogFrames = ANALOG_FRAMES as u32;
fixture.context.analogInChannels = ANALOG_IN_CHANNELS as u32;
fixture.context.analogOutChannels = ANALOG_OUT_CHANNELS as u32;
fixture.context.analogSampleRate = 44100.0;
fixture.context.digitalFrames = DIGITAL_FRAMES as u32;
fixture.context.digitalChannels = DIGITAL_CHANNELS as u32;
fixture.context.audioFramesElapsed = 128;
fixture.context.thisThread = 1;
fixture.context.threadCount = 4;
fixture
}
fn context(&mut self) -> &mut Context {
unsafe { Context::from_mut_ptr(&raw mut self.context) }
}
}
#[test]
fn metadata_accessors_reflect_the_struct() {
let mut fixture = Fixture::new();
let context = fixture.context();
assert_eq!(context.audio_frames(), AUDIO_FRAMES);
assert_eq!(context.audio_in_channels(), AUDIO_IN_CHANNELS);
assert_eq!(context.audio_out_channels(), AUDIO_OUT_CHANNELS);
assert_eq!(context.audio_sample_rate(), 44100.0);
assert_eq!(context.analog_frames(), ANALOG_FRAMES);
assert_eq!(context.digital_channels(), DIGITAL_CHANNELS);
assert_eq!(context.audio_frames_elapsed(), 128);
assert_eq!(context.underrun_count(), 0);
assert_eq!(context.this_thread(), 1);
assert_eq!(context.thread_count(), 4);
}
#[test]
fn audio_read_uses_the_interleaved_layout() {
let mut fixture = Fixture::new();
let context = fixture.context();
assert_eq!(context.audio_read(0, 0), 0.0);
assert_eq!(context.audio_read(0, 1), 1.0);
assert_eq!(context.audio_read(3, 1), 31.0);
assert_eq!(context.audio_in().len(), AUDIO_FRAMES * AUDIO_IN_CHANNELS);
}
#[test]
fn audio_write_targets_exactly_one_sample() {
let mut fixture = Fixture::new();
fixture.context().audio_write(2, 3, 0.5);
let index = 2 * AUDIO_OUT_CHANNELS + 3;
for (i, &sample) in fixture.audio_out.iter().enumerate() {
let expected = if i == index { 0.5 } else { 0.0 };
assert_eq!(sample, expected, "sample {i}");
}
}
#[test]
fn analog_read_uses_the_interleaved_layout() {
let mut fixture = Fixture::new();
assert_eq!(fixture.context().analog_read(2, 3), 23.0);
}
#[test]
fn analog_write_persists_to_the_end_of_the_block() {
let mut fixture = Fixture::new();
fixture.context().analog_write(1, 0, 0.7);
for frame in 0..ANALOG_FRAMES {
let expected = if frame >= 1 { 0.7 } else { 0.0 };
assert_eq!(fixture.analog_out[frame * ANALOG_OUT_CHANNELS], expected);
assert_eq!(fixture.analog_out[frame * ANALOG_OUT_CHANNELS + 1], 0.0);
}
}
#[test]
fn analog_write_once_targets_exactly_one_sample() {
let mut fixture = Fixture::new();
fixture.context().analog_write_once(1, 1, 0.7);
let index = ANALOG_OUT_CHANNELS + 1;
for (i, &sample) in fixture.analog_out.iter().enumerate() {
let expected = if i == index { 0.7 } else { 0.0 };
assert_eq!(sample, expected, "sample {i}");
}
}
#[test]
fn digital_value_bits_live_in_the_high_half_word() {
let mut fixture = Fixture::new();
let context = fixture.context();
context.digital_write_once(0, 3, true);
assert_eq!(fixture.digital[0], 1 << (3 + 16));
let context = fixture.context();
assert!(context.digital_read(0, 3));
assert!(!context.digital_read(0, 2));
assert!(!context.digital_read(1, 3));
}
#[test]
fn digital_write_persists_and_clears() {
let mut fixture = Fixture::new();
fixture.context().digital_write(1, 5, true);
for frame in 0..DIGITAL_FRAMES {
assert_eq!(fixture.digital[frame], u32::from(frame >= 1) << (5 + 16));
}
fixture.context().digital_write(2, 5, false);
for frame in 0..DIGITAL_FRAMES {
assert_eq!(fixture.digital[frame], u32::from(frame == 1) << (5 + 16));
}
}
#[test]
fn pin_mode_sets_direction_bits_in_the_low_half_word() {
let mut fixture = Fixture::new();
fixture.context().pin_mode(0, 7, PinMode::Input);
for frame in 0..DIGITAL_FRAMES {
assert_eq!(fixture.digital[frame], 1 << 7);
}
fixture.context().pin_mode_once(2, 7, PinMode::Output);
for frame in 0..DIGITAL_FRAMES {
assert_eq!(fixture.digital[frame], u32::from(frame != 2) << 7);
}
}
#[test]
fn disabled_io_yields_empty_slices() {
let mut context: BelaContext = unsafe { mem::zeroed() };
let context = unsafe { Context::from_mut_ptr(&raw mut context) };
assert!(context.audio_in().is_empty());
assert!(context.audio_out().is_empty());
assert!(context.analog_in().is_empty());
assert!(context.analog_out().is_empty());
assert!(context.digital().is_empty());
}
#[test]
#[should_panic(expected = "audio input channel out of range")]
fn audio_read_rejects_out_of_range_channels() {
let mut fixture = Fixture::new();
let _ = fixture.context().audio_read(0, AUDIO_IN_CHANNELS);
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn audio_read_rejects_out_of_range_frames() {
let mut fixture = Fixture::new();
let _ = fixture.context().audio_read(AUDIO_FRAMES, 0);
}
#[test]
#[should_panic(expected = "digital channel out of range")]
fn digital_write_rejects_out_of_range_channels() {
let mut fixture = Fixture::new();
fixture.context().digital_write(0, DIGITAL_CHANNELS, true);
}
}