use std::time::Duration;
use sim_lib_midi_core::{
CC_ALL_NOTES_OFF, CC_BANK_SELECT_MSB, CC_EXPRESSION_MSB, CC_PAN_MSB, CC_SUSTAIN_PEDAL,
CC_VOLUME_MSB, ChannelMessage, MetaEvent, MidiEvent, MidiPayload, MidiSink, TickTime, U14,
};
use sim_lib_pitch_core::Pitch;
use sim_lib_sound_tuning::{Tuning, render_pitch_with_tuning};
use crate::{
BridgeChannelState, BridgeOptions, ScheduledTone, SoundBridgeError, TimbreBank, Voice,
VoicePhase, VoicePool, current_timbre, tone_gain,
};
pub struct MidiToSoundBridge {
tpq: u32,
options: BridgeOptions,
bank: TimbreBank,
tuning: Box<dyn Tuning>,
clock: Duration,
last_tick: TickTime,
us_per_quarter: u32,
channels: [BridgeChannelState; 16],
voices: VoicePool,
emitted: Vec<ScheduledTone>,
}
impl MidiToSoundBridge {
pub fn new(
tpq: u32,
bank: TimbreBank,
tuning: Box<dyn Tuning>,
options: BridgeOptions,
) -> Result<Self, SoundBridgeError> {
Ok(Self {
tpq,
voices: VoicePool::new(options.polyphony_limit)?,
options,
bank,
tuning,
clock: Duration::ZERO,
last_tick: TickTime::new(0, tpq).expect("validated tpq"),
us_per_quarter: 500_000,
channels: std::array::from_fn(|_| BridgeChannelState::default()),
emitted: Vec::new(),
})
}
pub fn options(&self) -> &BridgeOptions {
&self.options
}
pub fn bank(&self) -> &TimbreBank {
&self.bank
}
pub fn voice_pool(&self) -> &VoicePool {
&self.voices
}
pub fn drain_tones(&mut self) -> Vec<ScheduledTone> {
self.drain_pending();
std::mem::take(&mut self.emitted)
}
pub fn stolen_voice_count(&self) -> usize {
self.voices.stolen_voice_count()
}
fn advance_to(&mut self, time: TickTime) -> Result<(), SoundBridgeError> {
let rebased = if time.tpq == self.tpq {
time
} else {
time.quantize(self.tpq)
};
if rebased.ticks < self.last_tick.ticks {
return Err(SoundBridgeError::NonMonotonicTime);
}
let delta_ticks = rebased.ticks - self.last_tick.ticks;
let delta_quarters = delta_ticks as f64 / f64::from(self.tpq);
let delta_secs = delta_quarters * f64::from(self.us_per_quarter) / 1_000_000.0;
self.clock += Duration::from_secs_f64(delta_secs.max(0.0));
self.last_tick = rebased;
self.voices.reap_finished(self.clock);
self.drain_pending();
Ok(())
}
fn handle_channel_message(&mut self, message: &ChannelMessage) {
match *message {
ChannelMessage::NoteOn { ch, key, vel } if vel.0 == 0 => {
let sustain = self.channels[ch.0 as usize].sustain;
self.voices.note_off(ch.0, key.0, self.clock, sustain);
}
ChannelMessage::NoteOn { ch, key, vel } => {
let state = self.channels[ch.0 as usize].clone();
let pitch = Pitch::from_midi(key.0);
let frequency = render_pitch_with_tuning(pitch, self.tuning.as_ref());
let frequency = frequency.shift_cents(state.pitch_bend_cents);
let voice = Voice {
channel: ch.0,
key: key.0,
started_at: self.clock,
release_at: None,
release_end: None,
frequency,
amplitude_gain: tone_gain(&state, vel.0),
pan: state.pan,
phase: VoicePhase::Held,
timbre: current_timbre(&self.bank, &state).clone(),
};
self.voices.note_on(voice, self.clock);
}
ChannelMessage::NoteOff { ch, key, .. } => {
let sustain = self.channels[ch.0 as usize].sustain;
self.voices.note_off(ch.0, key.0, self.clock, sustain);
}
ChannelMessage::ControlChange { ch, cc, value } => {
let state = &mut self.channels[ch.0 as usize];
match cc {
CC_BANK_SELECT_MSB => state.bank_msb = value.0,
CC_VOLUME_MSB => state.volume = f64::from(value.0) / 127.0,
CC_PAN_MSB => state.pan = (f32::from(value.0) - 64.0) / 63.0,
CC_EXPRESSION_MSB => state.expression = f64::from(value.0) / 127.0,
CC_SUSTAIN_PEDAL => {
let sustain = value.0 >= 64;
if state.sustain && !sustain {
self.voices.release_channel_sustain(ch.0, self.clock);
}
state.sustain = sustain;
}
CC_ALL_NOTES_OFF => {
let sustain = state.sustain;
for key in 0..=127 {
self.voices.note_off(ch.0, key, self.clock, sustain);
}
}
_ => {}
}
}
ChannelMessage::ProgramChange { ch, program } => {
self.channels[ch.0 as usize].program = program.0;
}
ChannelMessage::PitchBend { ch, value } => {
let cents = pitch_bend_to_cents(value, self.options.bend_range_cents);
let state = &mut self.channels[ch.0 as usize];
let delta = cents - state.pitch_bend_cents;
state.pitch_bend_cents = cents;
self.voices.transpose_channel_cents(ch.0, delta);
}
ChannelMessage::PolyAftertouch { .. } | ChannelMessage::ChanAftertouch { .. } => {}
}
}
fn drain_pending(&mut self) {
self.emitted.extend(self.voices.drain_emitted());
}
}
impl MidiSink for MidiToSoundBridge {
type Err = SoundBridgeError;
fn tpq(&self) -> u32 {
self.tpq
}
fn write(&mut self, event: &MidiEvent) -> Result<(), Self::Err> {
self.advance_to(event.time)?;
match &event.payload {
MidiPayload::Channel(message) => self.handle_channel_message(message),
MidiPayload::Meta(MetaEvent::Tempo { us_per_quarter }) => {
self.us_per_quarter = *us_per_quarter;
}
MidiPayload::Meta(_) | MidiPayload::SysEx(_) | MidiPayload::Raw(_) => {}
}
Ok(())
}
fn flush(&mut self) -> Result<(), Self::Err> {
self.voices.flush_all(self.clock);
self.drain_pending();
Ok(())
}
}
fn pitch_bend_to_cents(value: U14, bend_range_cents: f64) -> f64 {
let normalized = (f64::from(value.0) - 8192.0) / 8192.0;
normalized * bend_range_cents
}