#![cfg_attr(
not(bela_device),
allow(
dead_code,
reason = "only the fallback main is reachable off-device; the application code should still compile and lint"
)
)]
use core::f32::consts::TAU;
#[cfg(bela_device)]
use std::env;
#[cfg(not(bela_device))]
use std::process::ExitCode;
use bela::{
BelaApplication, BlockContext, CleanupContext, ControlValue, Controller, MidiChannel,
MidiInput, MidiMessage, MidiOutput, MidiSender, Note, RenderContext, SetupContext, ThreadInfo,
rt_println,
};
const QUEUE_CAPACITY: usize = 16;
const AMPLITUDE: f32 = 0.3;
const ALL_NOTES_OFF: Controller = match Controller::new(123) {
Some(controller) => controller,
None => unreachable!(),
};
struct Monosynth {
input: Option<MidiInput>,
output: Option<MidiOutput>,
in_port: String,
out_port: String,
playing: Option<Note>,
phase_increment: f32,
phase: f32,
sample_rate: f32,
received: u64,
echoed: u64,
dropped: u64,
}
struct Voice {
first_frame: usize,
phase: f32,
sender: Option<MidiSender>,
}
impl BelaApplication for Monosynth {
type RenderState = Voice;
fn setup(&mut self, context: &SetupContext) -> bool {
self.sample_rate = context.audio_sample_rate();
let ports = bela::midi_ports();
if self.in_port.is_empty() {
let Some(first) = ports.first() else {
println!("no MIDI ports; nothing to open");
return false;
};
self.in_port.clone_from(first);
}
if self.out_port.is_empty() {
self.out_port.clone_from(&self.in_port);
}
match MidiInput::open(&self.in_port) {
Ok(input) => self.input = Some(input),
Err(error) => {
println!("cannot read from {}: {error}", self.in_port);
return false;
}
}
match MidiOutput::open(&self.out_port, context, QUEUE_CAPACITY) {
Ok(output) => self.output = Some(output),
Err(error) => {
println!("cannot write to {}: {error}", self.out_port);
return false;
}
}
println!(
"setup: in {}, out {}, of {} port(s)",
self.in_port,
self.out_port,
ports.len()
);
true
}
fn create_render_state(&mut self, thread: ThreadInfo, context: &SetupContext) -> Voice {
Voice {
first_frame: thread.frame_range(context.audio_frames()).start,
phase: 0.0,
sender: self
.output
.as_mut()
.and_then(|output| output.take_sender(thread.index())),
}
}
fn render_pre(&mut self, states: &mut [Voice], context: &mut BlockContext) {
if let Some(input) = self.input.as_mut() {
while let Some(message) = input.read() {
self.received += 1;
rt_println!("{message:?}");
match message {
MidiMessage::NoteOn { note, velocity, .. } if velocity.get() > 0 => {
self.playing = Some(note);
self.phase_increment = increment(note, self.sample_rate);
}
MidiMessage::NoteOn { note, .. } | MidiMessage::NoteOff { note, .. }
if self.playing == Some(note) =>
{
self.playing = None;
self.phase_increment = 0.0;
}
_ => {}
}
let sent = states
.first_mut()
.and_then(|voice| voice.sender.as_mut())
.is_some_and(|sender| sender.send(context, message).is_ok());
if sent {
self.echoed += 1;
} else {
self.dropped += 1;
}
}
}
for state in states {
#[allow(
clippy::cast_precision_loss,
reason = "a frame index within a block is far below f32's exact integer range"
)]
let offset = state.first_frame as f32 * self.phase_increment;
state.phase = self.phase + offset;
}
}
fn render(&self, state: &mut Voice, context: &mut RenderContext) {
for frame in context.audio_frame_range() {
let sample = if self.phase_increment > 0.0 {
AMPLITUDE * state.phase.sin()
} else {
0.0
};
for channel in 0..context.audio_out_channels() {
context.audio_write(frame, channel, sample);
}
state.phase += self.phase_increment;
}
}
fn render_post(&mut self, _states: &mut [Voice], context: &mut BlockContext) {
#[allow(
clippy::cast_precision_loss,
reason = "a block's frame count is far below f32's exact integer range"
)]
let advanced = context.audio_frames() as f32 * self.phase_increment;
self.phase = (self.phase + advanced) % TAU;
}
fn cleanup(&mut self, _states: &mut [Voice], _context: &CleanupContext) {
if let Some(output) = self.output.as_mut() {
for channel in 0..=MidiChannel::MAX.get() {
let all_off = MidiMessage::ControlChange {
channel: MidiChannel::new(channel).expect("0..=15 is every channel"),
controller: ALL_NOTES_OFF,
value: ControlValue::MIN,
};
if let Err(error) = output.send(all_off) {
println!("cleanup: all notes off was not sent: {error}");
break;
}
}
}
println!(
"cleanup: {} message(s) received, {} echoed, {} dropped",
self.received, self.echoed, self.dropped
);
}
}
fn increment(note: Note, sample_rate: f32) -> f32 {
let semitones = f32::from(note.get()) - 69.0;
let frequency = 440.0 * (semitones / 12.0).exp2();
TAU * frequency / sample_rate
}
#[cfg(bela_device)]
fn main() -> Result<(), bela::Error> {
let mut arguments = env::args().skip(1);
let synth = Monosynth {
input: None,
output: None,
in_port: arguments.next().unwrap_or_default(),
out_port: arguments.next().unwrap_or_default(),
playing: None,
phase_increment: 0.0,
phase: 0.0,
sample_rate: 0.0,
received: 0,
echoed: 0,
dropped: 0,
};
bela::Bela::run(synth, &bela::Settings::new())
}
#[cfg(not(bela_device))]
fn main() -> ExitCode {
eprintln!("This example must be cross-compiled for Bela Gem (aarch64-unknown-linux-gnu).");
ExitCode::FAILURE
}