use anyhow::{Context, Result};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use crossbeam_channel::{Receiver, Sender, bounded};
use crate::audio::chorus::Chorus;
use crate::audio::delay::Delay;
use crate::audio::drums::DrumMachine;
use crate::audio::fx::Reverb;
use crate::audio::voice::Voice;
use crate::params::{AudioEvent, ChannelNo, MidiNote, SynthParams};
const SCOPE_CHANNEL_CAPACITY: usize = 32;
const SCOPE_DECIMATION: usize = 4;
const SCOPE_BATCH: usize = 128;
const POLYPHONY: usize = 4;
const POLYPHONY_F32: f32 = 4.0;
pub const NUM_CHANNELS: usize = 4;
const MASTER_GAIN: f32 = 0.5;
#[derive(Clone, Copy, Default)]
struct VoiceSlot {
note: Option<MidiNote>,
age: u64,
}
struct AudioChannel {
params: SynthParams,
voices: [Voice; POLYPHONY],
slots: [VoiceSlot; POLYPHONY],
age_counter: u64,
}
impl AudioChannel {
fn new() -> Self {
Self {
params: SynthParams::default(),
voices: std::array::from_fn(|_| Voice::new()),
slots: std::array::from_fn(|_| VoiceSlot::default()),
age_counter: 0,
}
}
fn is_voice_idle(&self, idx: usize) -> bool {
let voice = &self.voices[idx];
!voice.active && !voice.env.is_active() && self.slots[idx].note.is_none()
}
fn allocate_voice_index(&self, midi: MidiNote) -> usize {
if let Some(idx) = self.slots.iter().position(|s| s.note == Some(midi)) {
return idx;
}
if let Some(idx) = (0..POLYPHONY).find(|&idx| self.is_voice_idle(idx)) {
return idx;
}
self.slots
.iter()
.enumerate()
.min_by_key(|(_, s)| s.age)
.map_or(0, |(idx, _)| idx)
}
fn note_on(&mut self, midi: MidiNote, sample_rate: f32) {
let idx = self.allocate_voice_index(midi);
self.age_counter = self.age_counter.saturating_add(1);
self.slots[idx].note = Some(midi);
self.slots[idx].age = self.age_counter;
self.voices[idx].note_on(midi, &self.params, sample_rate);
}
fn note_off(&mut self, midi: MidiNote) {
if let Some(idx) = self.slots.iter().position(|s| s.note == Some(midi)) {
self.voices[idx].note_off();
self.slots[idx].note = None;
}
}
fn panic(&mut self) {
for voice in &mut self.voices {
voice.panic();
}
for slot in &mut self.slots {
*slot = VoiceSlot::default();
}
self.age_counter = 0;
}
fn process(&mut self, sample_rate: f32) -> f32 {
self.voices
.iter_mut()
.map(|v| v.process(&self.params, sample_rate))
.sum::<f32>()
/ POLYPHONY_F32
}
}
struct AudioState<const N: usize> {
channels: [AudioChannel; N],
reverb: Reverb,
chorus: Chorus,
delay: Delay,
drums: DrumMachine,
event_rx: Receiver<AudioEvent>,
scope_tx: Sender<Vec<f32>>,
scope_accum: Vec<f32>,
scope_dec_counter: usize,
sample_rate: f32,
}
impl<const N: usize> AudioState<N> {
fn new(sample_rate: f32, event_rx: Receiver<AudioEvent>, scope_tx: Sender<Vec<f32>>) -> Self {
const {
assert!(
N >= 1,
"AudioState requires at least 1 synthesis channel (N >= 1)"
);
};
let channels: [AudioChannel; N] = std::array::from_fn(|_| AudioChannel::new());
let mut reverb = Reverb::new();
reverb.set_params(
channels[0].params.fx.reverb_size,
channels[0].params.fx.reverb_damping,
);
Self {
channels,
reverb,
chorus: Chorus::new(sample_rate),
delay: Delay::new(sample_rate),
drums: DrumMachine::new(sample_rate),
event_rx,
scope_tx,
scope_accum: Vec::with_capacity(SCOPE_BATCH * 2),
scope_dec_counter: 0,
sample_rate,
}
}
fn apply_reverb_params(&mut self) {
let fx = &self.channels[0].params.fx;
self.reverb.set_params(fx.reverb_size, fx.reverb_damping);
}
fn note_on(&mut self, ch: ChannelNo, midi: MidiNote) {
if let Some(channel) = self.channels.get_mut(ch.as_usize()) {
channel.note_on(midi, self.sample_rate);
}
}
fn note_off(&mut self, ch: ChannelNo, midi: MidiNote) {
if let Some(channel) = self.channels.get_mut(ch.as_usize()) {
channel.note_off(midi);
}
}
fn panic(&mut self) {
for channel in &mut self.channels {
channel.panic();
}
self.drums.panic();
}
fn drain_events(&mut self) {
while let Ok(event) = self.event_rx.try_recv() {
match event {
AudioEvent::NoteOn(midi) => self.note_on(ChannelNo::DEFAULT, midi),
AudioEvent::NoteOff(midi) => self.note_off(ChannelNo::DEFAULT, midi),
AudioEvent::Panic => self.panic(),
AudioEvent::LoadPatch(p) => {
self.channels[0].params = *p;
self.apply_reverb_params();
}
AudioEvent::Drum(hit) => self.drums.trigger(hit),
AudioEvent::NoteOnChannel(ch, midi) => self.note_on(ch, midi),
AudioEvent::NoteOffChannel(ch, midi) => self.note_off(ch, midi),
AudioEvent::LoadPatchChannel(ch, p) => {
if let Some(channel) = self.channels.get_mut(ch.as_usize()) {
channel.params = *p;
}
if ch == ChannelNo::DEFAULT {
self.apply_reverb_params();
}
}
}
}
}
fn process(&mut self, data: &mut [f32], hw_channels: usize) {
self.drain_events();
let sample_rate = self.sample_rate;
let reverb_mix = self.channels[0].params.fx.reverb_mix;
let chorus_params = self.channels[0].params.chorus.clone();
let delay_params = self.channels[0].params.delay.clone();
for frame in data.chunks_mut(hw_channels) {
let mix: f32 = self
.channels
.iter_mut()
.map(|ch| ch.process(sample_rate))
.sum::<f32>()
+ self.drums.process(sample_rate);
let mix = self.chorus.process(mix, &chorus_params);
let mix = self.delay.process(mix, &delay_params);
let sample = self.reverb.process(mix, reverb_mix) * MASTER_GAIN;
let sample = if sample.is_finite() {
sample.clamp(-1.0, 1.0)
} else {
std::hint::cold_path();
0.0
};
for ch in frame.iter_mut() {
*ch = sample;
}
self.scope_dec_counter += 1;
if self.scope_dec_counter >= SCOPE_DECIMATION {
self.scope_dec_counter = 0;
self.scope_accum.push(sample);
if self.scope_accum.len() >= SCOPE_BATCH {
let batch = std::mem::replace(
&mut self.scope_accum,
Vec::with_capacity(SCOPE_BATCH * 2),
);
let _ = self.scope_tx.try_send(batch);
}
}
}
}
}
pub fn setup_audio_n<const N: usize>()
-> Result<(cpal::Stream, Sender<AudioEvent>, Receiver<Vec<f32>>)> {
let (event_tx, event_rx) = bounded::<AudioEvent>(1024);
let (scope_tx, scope_rx) = bounded::<Vec<f32>>(SCOPE_CHANNEL_CAPACITY);
let host = cpal::default_host();
let device = host
.default_output_device()
.context("no default audio output device")?;
let config = device
.default_output_config()
.context("failed to query default output config")?;
#[allow(clippy::cast_precision_loss)]
let sample_rate = config.sample_rate() as f32;
let hw_channels = config.channels() as usize;
let stream_config: cpal::StreamConfig = config.into();
let mut audio_state = AudioState::<N>::new(sample_rate, event_rx, scope_tx);
let stream = device
.build_output_stream(
&stream_config,
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
audio_state.process(data, hw_channels);
},
|err| eprintln!("audio stream error: {err}"),
None,
)
.context("failed to build output stream")?;
stream.play().context("failed to start audio stream")?;
Ok((stream, event_tx, scope_rx))
}
pub fn setup_audio() -> Result<(cpal::Stream, Sender<AudioEvent>, Receiver<Vec<f32>>)> {
setup_audio_n::<NUM_CHANNELS>()
}