use core::fmt;
use core::iter;
use core::mem;
use core::ops::Range;
use core::slice;
use bela_sys::BelaContext;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum PinMode {
#[default]
Input,
Output,
}
const DIGITAL_VALUE_SHIFT: usize = 16;
#[inline]
pub(crate) const fn partition(len: usize, thread: usize, count: usize) -> Range<usize> {
let count = if count == 0 { 1 } else { count };
if thread >= count {
return len..len;
}
let start = len * thread / count;
let end = len * (thread + 1) / count;
start..end
}
macro_rules! metadata_accessors {
($($context:ty),+ $(,)?) => {
$(
impl $context {
#[must_use]
#[inline]
pub const fn as_sys(&self) -> &BelaContext {
&self.0
}
#[must_use]
#[inline]
pub const fn audio_frames(&self) -> usize {
self.0.audioFrames as usize
}
#[must_use]
#[inline]
pub const fn audio_in_channels(&self) -> usize {
self.0.audioInChannels as usize
}
#[must_use]
#[inline]
pub const fn audio_out_channels(&self) -> usize {
self.0.audioOutChannels as usize
}
#[must_use]
#[inline]
pub const fn audio_sample_rate(&self) -> f32 {
self.0.audioSampleRate
}
#[must_use]
#[inline]
pub const fn analog_frames(&self) -> usize {
self.0.analogFrames as usize
}
#[must_use]
#[inline]
pub const fn analog_in_channels(&self) -> usize {
self.0.analogInChannels as usize
}
#[must_use]
#[inline]
pub const fn analog_out_channels(&self) -> usize {
self.0.analogOutChannels as usize
}
#[must_use]
#[inline]
pub const fn analog_sample_rate(&self) -> f32 {
self.0.analogSampleRate
}
#[must_use]
#[inline]
pub const fn digital_frames(&self) -> usize {
self.0.digitalFrames as usize
}
#[must_use]
#[inline]
pub const fn digital_channels(&self) -> usize {
self.0.digitalChannels as usize
}
#[must_use]
#[inline]
pub const fn digital_sample_rate(&self) -> f32 {
self.0.digitalSampleRate
}
#[must_use]
#[inline]
pub const fn audio_frames_elapsed(&self) -> u64 {
self.0.audioFramesElapsed
}
#[must_use]
#[inline]
pub const fn underrun_count(&self) -> u32 {
self.0.underrunCount
}
#[must_use]
#[inline]
pub const fn this_thread(&self) -> usize {
self.0.thisThread as usize
}
#[must_use]
#[inline]
pub const fn thread_count(&self) -> usize {
let count = self.0.threadCount as usize;
if count == 0 { 1 } else { count }
}
}
)+
};
}
macro_rules! metadata_debug {
($($context:ident $([$($extra:ident),+ $(,)?])?),+ $(,)?) => {
$(
impl fmt::Debug for $context {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct(stringify!($context))
.field("audio_frames", &self.audio_frames())
.field("audio_in_channels", &self.audio_in_channels())
.field("audio_out_channels", &self.audio_out_channels())
.field("audio_sample_rate", &self.audio_sample_rate())
.field("analog_frames", &self.analog_frames())
.field("analog_in_channels", &self.analog_in_channels())
.field("analog_out_channels", &self.analog_out_channels())
.field("analog_sample_rate", &self.analog_sample_rate())
.field("digital_frames", &self.digital_frames())
.field("digital_channels", &self.digital_channels())
.field("digital_sample_rate", &self.digital_sample_rate())
.field("audio_frames_elapsed", &self.audio_frames_elapsed())
.field("underrun_count", &self.underrun_count())
.field("this_thread", &self.this_thread())
.field("thread_count", &self.thread_count())
$($(.field(stringify!($extra), &self.$extra()))+)?
.finish_non_exhaustive()
}
}
)+
};
}
#[repr(transparent)]
pub struct SetupContext(BelaContext);
#[repr(transparent)]
pub struct CleanupContext(BelaContext);
#[repr(transparent)]
pub struct BlockContext(BelaContext);
#[repr(transparent)]
pub struct RenderContext(BelaContext);
metadata_accessors!(SetupContext, CleanupContext, BlockContext, RenderContext);
metadata_debug!(
SetupContext,
CleanupContext,
BlockContext,
RenderContext[audio_frame_range, analog_frame_range, digital_frame_range],
);
macro_rules! from_mut_ptr {
($($context:ident: $phase:literal),+ $(,)?) => {
$(
impl $context {
#[doc = concat!(
"Reborrows a raw `BelaContext` pointer as a [`",
stringify!($context),
"`].\n\n# Safety\n\n`ptr` must be non-null, properly aligned, and point to a \
live `BelaContext` that is not accessed through any other reference for the \
duration of `'a`. The buffer pointers inside must be either null or valid \
for the lengths implied by the frame and channel counts, and for each \
domain — audio, analog — the input buffer must not overlap the output \
buffer: [`PairedIo`] borrows both from one call and relies on that \
separation. Every context libbela hands to a callback satisfies it — \
`BelaContextManager` (`/root/Bela/core/BelaContextManager.cpp`) allocates \
`audioInV`/`audioOutV` and `analogInV`/`analogOutV` as independent \
`std::vector<float>`s and stores each one's own `.data()` pointer in the \
context — but it is a constraint on what `ptr` may point to that this crate \
cannot check, so it is one a context built by hand or by a test fixture must \
keep too.\n\nThe result \
stands in for the context of the ", $phase, " callback, and some accessors \
take it as proof of being in one — see the type documentation for what they \
rely on. A context conjured up elsewhere is not that proof."
)]
pub const unsafe fn from_mut_ptr<'a>(ptr: *mut BelaContext) -> &'a mut Self {
unsafe { &mut *ptr.cast::<Self>() }
}
}
)+
};
}
from_mut_ptr!(
SetupContext: "setup",
CleanupContext: "cleanup",
BlockContext: "render_pre / render_post",
RenderContext: "render",
);
pub struct PairedIo<'a> {
input: &'a [f32],
in_channels: usize,
output: &'a mut [f32],
out_channels: usize,
output_range: Range<usize>,
}
impl fmt::Debug for PairedIo<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PairedIo")
.field("in_channels", &self.in_channels)
.field("out_channels", &self.out_channels)
.field("output_range", &self.output_range)
.finish_non_exhaustive()
}
}
impl<'a> PairedIo<'a> {
const fn new(
input: &'a [f32],
in_channels: usize,
output: &'a mut [f32],
out_channels: usize,
output_range: Range<usize>,
) -> Self {
Self {
input,
in_channels,
output,
out_channels,
output_range,
}
}
#[must_use]
#[inline]
pub const fn input(&self) -> &[f32] {
self.input
}
#[must_use]
#[inline]
pub const fn in_channels(&self) -> usize {
self.in_channels
}
#[inline]
pub const fn output(&mut self) -> &mut [f32] {
self.output
}
#[must_use]
#[inline]
pub const fn out_channels(&self) -> usize {
self.out_channels
}
#[must_use]
#[inline]
pub const fn output_range(&self) -> Range<usize> {
self.output_range.start..self.output_range.end
}
pub fn frames(&mut self) -> impl Iterator<Item = (&[f32], &mut [f32])> + '_ {
let start = self.output_range.start;
let len = self.output_range.end - self.output_range.start;
let in_channels = self.in_channels;
let out_channels = self.out_channels;
let input = self.input;
let mut output: &mut [f32] = self.output;
let mut index = 0_usize;
iter::from_fn(move || {
if index >= len {
return None;
}
let frame = start + index;
index += 1;
let in_frame = if in_channels == 0 {
&[][..]
} else {
let offset = frame * in_channels;
&input[offset..offset + in_channels]
};
let out_frame: &mut [f32] = if out_channels == 0 {
&mut [][..]
} else {
let (frame_slice, rest) = mem::take(&mut output).split_at_mut(out_channels);
output = rest;
frame_slice
};
Some((in_frame, out_frame))
})
}
}
impl BlockContext {
pub const unsafe fn as_sys_mut(&mut self) -> &mut BelaContext {
&mut self.0
}
#[must_use]
#[inline]
pub const fn audio_in(&self) -> &[f32] {
unsafe {
shared(
self.0.audioIn,
self.audio_frames() * self.audio_in_channels(),
)
}
}
#[inline]
pub const fn audio_out(&mut self) -> &mut [f32] {
unsafe {
exclusive(
self.0.audioOut,
self.audio_frames() * self.audio_out_channels(),
)
}
}
#[must_use]
#[inline]
pub const fn analog_in(&self) -> &[f32] {
unsafe {
shared(
self.0.analogIn,
self.analog_frames() * self.analog_in_channels(),
)
}
}
#[inline]
pub const fn analog_out(&mut self) -> &mut [f32] {
unsafe {
exclusive(
self.0.analogOut,
self.analog_frames() * self.analog_out_channels(),
)
}
}
#[must_use]
#[inline]
pub const fn digital(&self) -> &[u32] {
unsafe { shared(self.0.digital, self.digital_frames()) }
}
#[inline]
pub const fn digital_mut(&mut self) -> &mut [u32] {
unsafe { exclusive(self.0.digital, self.digital_frames()) }
}
#[inline]
pub const fn audio_io(&mut self) -> PairedIo<'_> {
let in_channels = self.audio_in_channels();
let out_channels = self.audio_out_channels();
let frames = self.audio_frames();
let input = unsafe { shared(self.0.audioIn, frames * in_channels) };
let output = unsafe { exclusive(self.0.audioOut, frames * out_channels) };
PairedIo::new(input, in_channels, output, out_channels, 0..frames)
}
#[inline]
pub const fn analog_io(&mut self) -> PairedIo<'_> {
let in_channels = self.analog_in_channels();
let out_channels = self.analog_out_channels();
let frames = self.analog_frames();
let input = unsafe { shared(self.0.analogIn, frames * in_channels) };
let output = unsafe { exclusive(self.0.analogOut, frames * out_channels) };
PairedIo::new(input, in_channels, output, out_channels, 0..frames)
}
#[must_use]
#[inline]
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]
}
#[inline]
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]
#[inline]
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;
}
}
#[inline]
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]
#[inline]
pub fn digital_read(&self, frame: usize, channel: usize) -> bool {
let mask = digital_value_mask(self.digital_channels(), channel);
self.digital()[frame] & mask != 0
}
pub fn digital_write(&mut self, frame: usize, channel: usize, value: bool) {
let mask = digital_value_mask(self.digital_channels(), channel);
for word in self.digital_mut().iter_mut().skip(frame) {
set_bits(word, mask, value);
}
}
#[inline]
pub fn digital_write_once(&mut self, frame: usize, channel: usize, value: bool) {
let mask = digital_value_mask(self.digital_channels(), channel);
set_bits(&mut self.digital_mut()[frame], mask, value);
}
pub fn pin_mode(&mut self, frame: usize, channel: usize, mode: PinMode) {
let mask = digital_direction_mask(self.digital_channels(), 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 = digital_direction_mask(self.digital_channels(), channel);
set_bits(&mut self.digital_mut()[frame], mask, mode == PinMode::Input);
}
}
impl RenderContext {
#[must_use]
#[inline]
pub const fn audio_frame_range(&self) -> Range<usize> {
partition(self.audio_frames(), self.this_thread(), self.thread_count())
}
#[must_use]
#[inline]
pub const fn analog_frame_range(&self) -> Range<usize> {
partition(
self.analog_frames(),
self.this_thread(),
self.thread_count(),
)
}
#[must_use]
#[inline]
pub const fn digital_frame_range(&self) -> Range<usize> {
partition(
self.digital_frames(),
self.this_thread(),
self.thread_count(),
)
}
#[must_use]
pub const fn audio_in(&self) -> &[f32] {
unsafe {
shared(
self.0.audioIn,
self.audio_frames() * self.audio_in_channels(),
)
}
}
#[must_use]
pub const fn analog_in(&self) -> &[f32] {
unsafe {
shared(
self.0.analogIn,
self.analog_frames() * self.analog_in_channels(),
)
}
}
#[must_use]
#[inline]
pub const fn digital(&self) -> &[u32] {
let range = self.digital_frame_range();
unsafe { share(self.0.digital, self.digital_frames(), 1, range) }
}
#[inline]
pub const fn audio_out(&mut self) -> &mut [f32] {
let range = self.audio_frame_range();
self.audio_share(range)
}
#[inline]
pub const fn analog_out(&mut self) -> &mut [f32] {
let range = self.analog_frame_range();
self.analog_share(range)
}
#[inline]
pub const fn digital_mut(&mut self) -> &mut [u32] {
let range = self.digital_frame_range();
self.digital_share(range)
}
#[inline]
pub const fn audio_io(&mut self) -> PairedIo<'_> {
let in_channels = self.audio_in_channels();
let out_channels = self.audio_out_channels();
let frames = self.audio_frames();
let range = self.audio_frame_range();
let input = unsafe { shared(self.0.audioIn, frames * in_channels) };
let output = self.audio_share(range.start..range.end);
PairedIo::new(input, in_channels, output, out_channels, range)
}
#[inline]
pub const fn analog_io(&mut self) -> PairedIo<'_> {
let in_channels = self.analog_in_channels();
let out_channels = self.analog_out_channels();
let frames = self.analog_frames();
let range = self.analog_frame_range();
let input = unsafe { shared(self.0.analogIn, frames * in_channels) };
let output = self.analog_share(range.start..range.end);
PairedIo::new(input, in_channels, output, out_channels, range)
}
#[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]
}
#[inline]
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");
let range = self.audio_frame_range();
let index = frame_offset(&range, frame, "audio") * channels + channel;
self.audio_share(range)[index] = 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 range = self.analog_frame_range();
let skip = frame_offset(&range, frame, "analog");
for samples in self.analog_share(range).chunks_mut(channels).skip(skip) {
samples[channel] = value;
}
}
#[inline]
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");
let range = self.analog_frame_range();
let index = frame_offset(&range, frame, "analog") * channels + channel;
self.analog_share(range)[index] = value;
}
#[must_use]
#[inline]
pub fn digital_read(&self, frame: usize, channel: usize) -> bool {
let mask = digital_value_mask(self.digital_channels(), channel);
let range = self.digital_frame_range();
let index = frame_offset(&range, frame, "digital");
let words = unsafe { share(self.0.digital, self.digital_frames(), 1, range) };
words[index] & mask != 0
}
pub fn digital_write(&mut self, frame: usize, channel: usize, value: bool) {
let mask = digital_value_mask(self.digital_channels(), channel);
let range = self.digital_frame_range();
let skip = frame_offset(&range, frame, "digital");
for word in self.digital_share(range).iter_mut().skip(skip) {
set_bits(word, mask, value);
}
}
#[inline]
pub fn digital_write_once(&mut self, frame: usize, channel: usize, value: bool) {
let mask = digital_value_mask(self.digital_channels(), channel);
let range = self.digital_frame_range();
let index = frame_offset(&range, frame, "digital");
set_bits(&mut self.digital_share(range)[index], mask, value);
}
pub fn pin_mode(&mut self, frame: usize, channel: usize, mode: PinMode) {
let mask = digital_direction_mask(self.digital_channels(), channel);
let range = self.digital_frame_range();
let skip = frame_offset(&range, frame, "digital");
for word in self.digital_share(range).iter_mut().skip(skip) {
set_bits(word, mask, mode == PinMode::Input);
}
}
pub fn pin_mode_once(&mut self, frame: usize, channel: usize, mode: PinMode) {
let mask = digital_direction_mask(self.digital_channels(), channel);
let range = self.digital_frame_range();
let index = frame_offset(&range, frame, "digital");
set_bits(
&mut self.digital_share(range)[index],
mask,
mode == PinMode::Input,
);
}
#[inline]
const fn audio_share(&mut self, range: Range<usize>) -> &mut [f32] {
unsafe {
share_mut(
self.0.audioOut,
self.audio_frames(),
self.audio_out_channels(),
range,
)
}
}
#[inline]
const fn analog_share(&mut self, range: Range<usize>) -> &mut [f32] {
unsafe {
share_mut(
self.0.analogOut,
self.analog_frames(),
self.analog_out_channels(),
range,
)
}
}
#[inline]
const fn digital_share(&mut self, range: Range<usize>) -> &mut [u32] {
unsafe { share_mut(self.0.digital, self.digital_frames(), 1, range) }
}
}
pub trait CallbackContext: sealed::Sealed {}
mod sealed {
pub trait Sealed {}
}
macro_rules! callback_context {
($($context:ty),+ $(,)?) => {
$(
impl sealed::Sealed for $context {}
impl CallbackContext for $context {}
)+
};
}
callback_context!(SetupContext, CleanupContext, BlockContext, RenderContext);
#[inline]
const fn samples(frames: usize, channels: usize, range: &Range<usize>) -> (usize, usize) {
let end = if range.end < frames {
range.end
} else {
frames
};
let start = if range.start < end { range.start } else { end };
(start * channels, (end - start) * channels)
}
#[inline]
const unsafe fn share<'a, T>(
ptr: *const T,
frames: usize,
channels: usize,
range: Range<usize>,
) -> &'a [T] {
if ptr.is_null() {
return &[];
}
let (offset, len) = samples(frames, channels, &range);
unsafe { slice::from_raw_parts(ptr.add(offset), len) }
}
#[inline]
const unsafe fn share_mut<'a, T>(
ptr: *mut T,
frames: usize,
channels: usize,
range: Range<usize>,
) -> &'a mut [T] {
if ptr.is_null() {
return &mut [];
}
let (offset, len) = samples(frames, channels, &range);
unsafe { slice::from_raw_parts_mut(ptr.add(offset), len) }
}
#[inline]
fn frame_offset(range: &Range<usize>, frame: usize, domain: &str) -> usize {
assert!(
range.contains(&frame),
"{domain} frame {frame} is outside this thread's range {range:?}"
);
frame - range.start
}
#[inline]
fn digital_value_mask(channels: usize, channel: usize) -> u32 {
assert!(channel < channels, "digital channel out of range");
1 << (channel + DIGITAL_VALUE_SHIFT)
}
#[inline]
fn digital_direction_mask(channels: usize, channel: usize) -> u32 {
assert!(channel < channels, "digital channel out of range");
1 << channel
}
#[inline]
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) }
}
}
#[inline]
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) }
}
}
#[inline]
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"
)]
pub(crate) 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;
pub(crate) struct Fixture {
audio_in: Vec<f32>,
pub(crate) audio_out: Vec<f32>,
analog_in: Vec<f32>,
pub(crate) analog_out: Vec<f32>,
pub(crate) digital: Vec<u32>,
pub(crate) context: BelaContext,
}
impl Fixture {
pub(crate) fn new() -> Box<Self> {
Self::with_threads(1)
}
pub(crate) fn with_threads(threads: u32) -> 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 = 0;
fixture.context.threadCount = threads;
fixture
}
pub(crate) fn block(&mut self) -> &mut BlockContext {
unsafe { BlockContext::from_mut_ptr(&raw mut self.context) }
}
pub(crate) fn setup(&mut self) -> &mut SetupContext {
unsafe { SetupContext::from_mut_ptr(&raw mut self.context) }
}
pub(crate) fn cleanup(&mut self) -> &mut CleanupContext {
unsafe { CleanupContext::from_mut_ptr(&raw mut self.context) }
}
pub(crate) fn render(&mut self, thread: u32) -> &mut RenderContext {
self.context.thisThread = thread;
unsafe { RenderContext::from_mut_ptr(&raw mut self.context) }
}
}
#[test]
fn metadata_accessors_reflect_the_struct() {
let mut fixture = Fixture::with_threads(4);
let context = fixture.block();
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(), 0);
assert_eq!(context.thread_count(), 4);
}
#[test]
fn the_same_metadata_is_on_every_phase() {
let mut fixture = Fixture::new();
assert_eq!(fixture.setup().audio_frames(), AUDIO_FRAMES);
assert_eq!(fixture.render(0).audio_sample_rate(), 44100.0);
let cleanup = unsafe { CleanupContext::from_mut_ptr(&raw mut fixture.context) };
assert_eq!(cleanup.audio_frames_elapsed(), 128);
}
#[test]
fn one_render_thread_is_spelled_either_way() {
for spelling in [0, 1] {
let mut fixture = Fixture::with_threads(spelling);
assert_eq!(
fixture.render(0).thread_count(),
1,
"threadCount {spelling} means one render thread"
);
assert_eq!(
fixture.render(0).audio_frame_range(),
0..AUDIO_FRAMES,
"the one thread gets the whole block"
);
}
}
#[test]
fn partitions_tile_the_block_exactly() {
for frames in 0..40_usize {
for count in 1..8_usize {
let mut previous_end = 0;
for thread in 0..count {
let range = partition(frames, thread, count);
assert_eq!(
range.start, previous_end,
"{frames} frames, {count} threads"
);
assert!(range.start <= range.end, "{frames} frames, {count} threads");
previous_end = range.end;
}
assert_eq!(previous_end, frames, "{frames} frames, {count} threads");
}
}
}
#[test]
fn uneven_partitions_differ_by_at_most_one_frame() {
let lengths: Vec<usize> = (0..4).map(|t| partition(7, t, 4).len()).collect();
assert_eq!(lengths, vec![1, 2, 2, 2]);
assert_eq!(lengths.iter().sum::<usize>(), 7);
}
#[test]
fn more_threads_than_frames_gives_empty_ranges() {
let ranges: Vec<Range<usize>> = (0..4).map(|t| partition(2, t, 4)).collect();
assert_eq!(ranges, vec![0..0, 0..1, 1..1, 1..2]);
}
#[test]
fn a_thread_outside_the_count_gets_nothing() {
assert_eq!(partition(8, 4, 4), 8..8);
assert_eq!(partition(8, 9, 4), 8..8);
}
#[test]
fn a_zero_thread_count_is_one_thread() {
assert_eq!(partition(8, 0, 0), 0..8);
}
#[test]
fn audio_read_uses_the_interleaved_layout() {
let mut fixture = Fixture::new();
let context = fixture.block();
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.block().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.block().analog_read(2, 3), 23.0);
}
#[test]
fn analog_write_persists_to_the_end_of_the_block() {
let mut fixture = Fixture::new();
fixture.block().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.block().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.block();
context.digital_write_once(0, 3, true);
assert_eq!(fixture.digital[0], 1 << (3 + 16));
let context = fixture.block();
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.block().digital_write(1, 5, true);
for frame in 0..DIGITAL_FRAMES {
assert_eq!(fixture.digital[frame], u32::from(frame >= 1) << (5 + 16));
}
fixture.block().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.block().pin_mode(0, 7, PinMode::Input);
for frame in 0..DIGITAL_FRAMES {
assert_eq!(fixture.digital[frame], 1 << 7);
}
fixture.block().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 { BlockContext::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.block().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.block().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.block().digital_write(0, DIGITAL_CHANNELS, true);
}
#[test]
fn paired_audio_view_covers_the_whole_block_on_a_block_context() {
let mut fixture = Fixture::new();
let mut io = fixture.block().audio_io();
assert_eq!(io.in_channels(), AUDIO_IN_CHANNELS);
assert_eq!(io.out_channels(), AUDIO_OUT_CHANNELS);
assert_eq!(io.output_range(), 0..AUDIO_FRAMES);
assert_eq!(io.input().len(), AUDIO_FRAMES * AUDIO_IN_CHANNELS);
assert_eq!(io.output().len(), AUDIO_FRAMES * AUDIO_OUT_CHANNELS);
assert_eq!(
io.input()[AUDIO_IN_CHANNELS + 1],
11.0,
"frame 1, channel 1"
);
}
#[test]
fn paired_audio_frames_writes_reach_the_underlying_buffer_on_a_block_context() {
let mut fixture = Fixture::new();
{
let mut io = fixture.block().audio_io();
for (input, output) in io.frames() {
for (sample, value) in output.iter_mut().zip(input) {
*sample = *value;
}
}
}
for frame in 0..AUDIO_FRAMES {
for channel in 0..AUDIO_IN_CHANNELS {
let expected = (frame * 10 + channel) as f32;
assert_eq!(
fixture.audio_out[frame * AUDIO_OUT_CHANNELS + channel],
expected,
"frame {frame} channel {channel}"
);
}
for channel in AUDIO_IN_CHANNELS..AUDIO_OUT_CHANNELS {
assert_eq!(
fixture.audio_out[frame * AUDIO_OUT_CHANNELS + channel],
0.0,
"frame {frame} channel {channel} has no input counterpart"
);
}
}
}
#[test]
fn paired_analog_view_is_independent_of_the_audio_buffers() {
let mut fixture = Fixture::new();
{
let mut io = fixture.block().analog_io();
assert_eq!(io.in_channels(), ANALOG_IN_CHANNELS);
assert_eq!(io.out_channels(), ANALOG_OUT_CHANNELS);
assert_eq!(io.input().len(), ANALOG_FRAMES * ANALOG_IN_CHANNELS);
io.output().fill(9.0);
}
assert!(fixture.analog_out.iter().all(|&v| v == 9.0));
assert!(
fixture.audio_out.iter().all(|&v| v == 0.0),
"analog_io must not touch the audio buffers"
);
}
#[test]
fn paired_view_frames_does_not_panic_when_output_has_no_channels() {
let mut context: BelaContext = unsafe { mem::zeroed() };
context.audioFrames = 3;
context.audioInChannels = 2;
context.audioOutChannels = 0;
let audio_in = [0.0_f32, 1.0, 10.0, 11.0, 20.0, 21.0];
context.audioIn = audio_in.as_ptr();
let context = unsafe { BlockContext::from_mut_ptr(&raw mut context) };
let mut io = context.audio_io();
assert_eq!(io.out_channels(), 0);
let mut frame_count = 0;
for (input, output) in io.frames() {
assert!(output.is_empty());
assert_eq!(input.len(), 2);
frame_count += 1;
}
assert_eq!(
frame_count, 3,
"three frames, each with zero output channels"
);
}
#[test]
fn paired_view_frames_does_not_panic_when_input_has_no_channels() {
let mut context: BelaContext = unsafe { mem::zeroed() };
context.audioFrames = 3;
context.audioInChannels = 0;
context.audioOutChannels = 2;
let mut audio_out = [0.0_f32; 6];
context.audioOut = audio_out.as_mut_ptr();
let context = unsafe { BlockContext::from_mut_ptr(&raw mut context) };
let mut io = context.audio_io();
assert_eq!(io.in_channels(), 0);
let mut frame_count = 0;
for (input, output) in io.frames() {
assert!(input.is_empty());
assert_eq!(output.len(), 2);
frame_count += 1;
}
assert_eq!(
frame_count, 3,
"three frames, each with zero input channels"
);
}
#[test]
fn a_render_context_reads_the_whole_block() {
let mut fixture = Fixture::with_threads(4);
let context = fixture.render(3);
assert_eq!(context.audio_frame_range(), 3..4, "one frame of four");
assert_eq!(context.audio_read(0, 1), 1.0);
assert_eq!(context.audio_in().len(), AUDIO_FRAMES * AUDIO_IN_CHANNELS);
assert_eq!(context.analog_read(0, 3), 3.0);
}
#[test]
fn a_render_context_writes_only_its_own_frames() {
let mut fixture = Fixture::with_threads(2);
for thread in 0..2 {
let context = fixture.render(thread);
let range = context.audio_frame_range();
for frame in range {
context.audio_write(frame, 0, frame as f32 + 1.0);
}
}
for frame in 0..AUDIO_FRAMES {
assert_eq!(
fixture.audio_out[frame * AUDIO_OUT_CHANNELS],
frame as f32 + 1.0,
"frame {frame}"
);
}
}
#[test]
fn the_output_slice_is_this_threads_share() {
let mut fixture = Fixture::with_threads(2);
let context = fixture.render(1);
let out = context.audio_out();
assert_eq!(out.len(), 2 * AUDIO_OUT_CHANNELS, "two of four frames");
out[0] = 9.0;
assert_eq!(fixture.audio_out[2 * AUDIO_OUT_CHANNELS], 9.0);
assert_eq!(fixture.audio_out[0], 0.0, "frame 0 belongs to thread 0");
}
#[test]
fn paired_audio_frames_align_input_and_output_when_the_range_does_not_start_at_zero() {
let mut fixture = Fixture::with_threads(4);
{
let context = fixture.render(2);
let mut io = context.audio_io();
assert_eq!(io.output_range(), 2..3);
let mut frames = io.frames();
let (input, output) = frames.next().expect("one frame in this thread's range");
assert_eq!(input, [20.0, 21.0]);
output.copy_from_slice(&[1.0, 2.0, 3.0, 4.0]);
assert!(
frames.next().is_none(),
"thread 2 of 4 owns exactly one frame"
);
}
assert_eq!(
fixture.audio_out[2 * AUDIO_OUT_CHANNELS..3 * AUDIO_OUT_CHANNELS],
[1.0, 2.0, 3.0, 4.0]
);
assert_eq!(
fixture.audio_out[0..AUDIO_OUT_CHANNELS],
[0.0; AUDIO_OUT_CHANNELS],
"frame 0 belongs to another thread"
);
}
#[test]
fn analog_and_digital_slices_are_partitioned_too() {
let mut fixture = Fixture::with_threads(4);
let context = fixture.render(2);
assert_eq!(context.analog_frame_range(), 2..3);
assert_eq!(context.digital_frame_range(), 2..3);
assert_eq!(context.analog_out().len(), ANALOG_OUT_CHANNELS);
assert_eq!(context.digital_mut().len(), 1);
}
#[test]
fn persisting_writes_stop_at_the_end_of_the_range() {
let mut fixture = Fixture::with_threads(2);
fixture.render(0).analog_write(0, 0, 0.7);
fixture.render(0).digital_write(0, 5, true);
for frame in 0..ANALOG_FRAMES {
let expected = if frame < 2 { 0.7 } else { 0.0 };
assert_eq!(
fixture.analog_out[frame * ANALOG_OUT_CHANNELS],
expected,
"analog frame {frame}"
);
}
for frame in 0..DIGITAL_FRAMES {
assert_eq!(
fixture.digital[frame],
u32::from(frame < 2) << (5 + 16),
"digital frame {frame}"
);
}
}
#[test]
fn an_empty_range_hands_out_nothing_to_write() {
let mut fixture = Fixture::with_threads(8);
let context = fixture.render(0);
assert_eq!(context.audio_frame_range(), 0..0);
assert!(context.audio_out().is_empty());
assert!(context.analog_out().is_empty());
assert!(context.digital_mut().is_empty());
}
#[test]
#[should_panic(expected = "audio frame 0 is outside this thread's range 2..4")]
fn writing_another_threads_frame_panics() {
let mut fixture = Fixture::with_threads(2);
fixture.render(1).audio_write(0, 0, 1.0);
}
#[test]
#[should_panic(expected = "analog frame 3 is outside this thread's range 0..2")]
fn a_persisting_analog_write_outside_the_range_panics() {
let mut fixture = Fixture::with_threads(2);
fixture.render(0).analog_write(3, 0, 1.0);
}
#[test]
#[should_panic(expected = "digital frame 0 is outside this thread's range 2..4")]
fn a_digital_write_outside_the_range_panics() {
let mut fixture = Fixture::with_threads(2);
fixture.render(1).digital_write(0, 5, true);
}
#[test]
fn digital_reads_are_this_threads_share_too() {
let mut fixture = Fixture::with_threads(2);
fixture.render(0).digital_write_once(1, 5, true);
let context = fixture.render(0);
assert_eq!(context.digital().len(), 2, "two of four frames");
assert!(context.digital_read(1, 5));
assert!(!context.digital_read(0, 5));
}
#[test]
#[should_panic(expected = "digital frame 3 is outside this thread's range 0..2")]
fn a_digital_read_outside_the_range_panics() {
let mut fixture = Fixture::with_threads(2);
let _ = fixture.render(0).digital_read(3, 5);
}
#[test]
fn audio_and_analog_reads_are_not_bounded_that_way() {
let mut fixture = Fixture::with_threads(2);
let context = fixture.render(1);
assert_eq!(context.audio_read(0, 0), 0.0);
assert_eq!(context.analog_read(0, 0), 0.0);
}
#[test]
fn a_pin_begins_as_an_input() {
assert_eq!(PinMode::default(), PinMode::Input);
}
#[test]
fn a_context_debugs_as_the_configuration_it_describes() {
let mut fixture = Fixture::new();
let printed = format!("{:?}", fixture.block());
assert!(
printed.starts_with("BlockContext {"),
"should name the phase it is: {printed}"
);
for field in [
"audio_frames: 4",
"audio_out_channels: 4",
"audio_sample_rate: 44100.0",
"analog_out_channels: 2",
"digital_channels: 16",
"audio_frames_elapsed: 128",
"thread_count: 1",
] {
assert!(printed.contains(field), "missing {field} in {printed}");
}
assert!(
!printed.contains("audio_out:") && !printed.contains("audioOut"),
"should not print the buffers: {printed}"
);
}
#[test]
fn each_phase_debugs_under_its_own_name() {
let mut fixture = Fixture::new();
assert!(format!("{:?}", fixture.setup()).starts_with("SetupContext {"));
assert!(format!("{:?}", fixture.cleanup()).starts_with("CleanupContext {"));
assert!(format!("{:?}", fixture.render(0)).starts_with("RenderContext {"));
}
#[test]
fn a_render_context_debugs_the_ranges_that_are_its_own() {
let mut fixture = Fixture::with_threads(2);
let printed = format!("{:?}", fixture.render(1));
assert!(
printed.contains("audio_frame_range: 2..4"),
"the second of two threads writes the second half: {printed}"
);
for field in ["analog_frame_range: 2..4", "digital_frame_range: 2..4"] {
assert!(printed.contains(field), "missing {field} in {printed}");
}
assert!(printed.contains("this_thread: 1"));
assert!(!format!("{:?}", fixture.block()).contains("audio_frame_range"));
}
}