//! Explicit sound roles and controller gestures, compiled away from the audio thread. use crate::{err, Meter, Note, Phrase, Result, MAX_LENGTH, MAX_PLAYBACK_NOTES, PPQ}; use serde::{Deserialize, Serialize}; use std::f64::consts::TAU; pub const MAX_PERFORMANCE_EVENTS: usize = 5120; pub const MAX_FRAME_EVENTS: usize = 64; #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] pub enum RoleMode { #[default] Solo, Alternating, CallResponse, Interlock, } impl RoleMode { pub const ALL: [Self; 4] = [ Self::Solo, Self::Alternating, Self::CallResponse, Self::Interlock, ]; pub fn name(self) -> &'static str { match self { Self::Solo => "One voice", Self::Alternating => "Alternate events", Self::CallResponse => "Question and answer", Self::Interlock => "Strong beats and replies", } } /// Which of the two voices plays note `index`. Question/answer alternates bars; /// strong beats follow the meter's pulse. pub fn role(self, index: usize, n: &Note, meter: Meter) -> usize { match self { Self::Solo => 0, Self::Alternating => index % 2, Self::CallResponse => ((n.start / meter.bar()) % 2) as usize, Self::Interlock => usize::from(!meter.strong(n.start)), } } } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub struct CurvePoint { pub position: u16, pub value: u8, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ControllerCurve { pub enabled: bool, pub cc: u8, pub rest: u8, pub points: Vec, } impl Default for ControllerCurve { fn default() -> Self { Self { enabled: false, cc: 1, rest: 0, points: vec![ CurvePoint { position: 0, value: 0, }, CurvePoint { position: 1000, value: 127, }, ], } } } impl ControllerCurve { pub fn validate(&self) -> Result<()> { if self.cc == 0 || self.cc >= 120 || [32, 64, 66, 69].contains(&self.cc) || self.rest > 127 { return Err(err("Choose a continuous controller in 1–119 (excluding bank/hold pedals) and a valid rest value.")); } if !(2..=16).contains(&self.points.len()) || self.points.first().unwrap().position != 0 || self.points.last().unwrap().position != 1000 || self.points.iter().any(|p| p.value > 127) || self .points .windows(2) .any(|p| p[0].position >= p[1].position) { return Err(err("A controller curve needs 2–16 ordered points from 0 to 1000, with MIDI values 0–127.")); } Ok(()) } pub fn value_at(&self, position: u16) -> u8 { for pair in self.points.windows(2) { if position <= pair[1].position { let p = position.saturating_sub(pair[0].position) as f64 / (pair[1].position - pair[0].position) as f64; return (pair[0].value as f64 + (pair[1].value as f64 - pair[0].value as f64) * p) .round() .clamp(0., 127.) as u8; } } self.points.last().map_or(0, |p| p.value) } pub fn shape(&mut self, preset: usize) { self.points = match preset { 1 => vec![(0, 127), (1000, 0)], 2 => vec![(0, 0), (500, 127), (1000, 0)], 3 => vec![ (0, 0), (125, 127), (250, 0), (375, 127), (500, 0), (625, 127), (750, 0), (875, 127), (1000, 0), ], _ => vec![(0, 0), (1000, 127)], } .into_iter() .map(|(position, value)| CurvePoint { position, value }) .collect(); } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct VoiceProfile { pub name: String, pub channel: u8, pub transpose: i8, pub gate: u16, pub velocity: u16, pub attack_ms: u16, pub release_ms: u16, pub controller: ControllerCurve, } impl Default for VoiceProfile { fn default() -> Self { Self { name: "Primary".into(), channel: 0, transpose: 0, gate: 100, velocity: 100, attack_ms: 0, release_ms: 0, controller: ControllerCurve::default(), } } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct PerformanceSettings { pub mode: RoleMode, pub voices: [VoiceProfile; 2], pub compensated_export: bool, } impl Default for PerformanceSettings { fn default() -> Self { Self { mode: RoleMode::Solo, voices: [ VoiceProfile::default(), VoiceProfile { name: "Answer".into(), channel: 1, ..VoiceProfile::default() }, ], compensated_export: false, } } } impl PerformanceSettings { pub fn validate(&self) -> Result<()> { if self.mode != RoleMode::Solo && self.voices[0].channel == self.voices[1].channel { return Err(err("Use separate MIDI channels for the two sound roles.")); } for v in &self.voices { if v.name.chars().count() > 48 || v.channel > 15 || !(-24..=24).contains(&v.transpose) || !(10..=200).contains(&v.gate) || !(1..=200).contains(&v.velocity) || v.attack_ms > 500 || v.release_ms > 2000 { return Err(err("Invalid patch profile: check channel, register offset, gate, velocity, and timing limits.")); } v.controller.validate()?; } Ok(()) } } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] pub enum MessageKind { #[default] Off, Control, On, } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct MidiMessage { pub kind: MessageKind, pub channel: u8, pub key: u8, pub value: u8, pub rest: u8, } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct TimedMessage { pub tick: i32, pub role: u8, pub message: MidiMessage, pub next_cycle: bool, } #[derive(Debug, Clone)] pub struct Performance { pub length: u32, pub lead_in: u32, pub events: Vec, } impl Performance { pub fn compile( phrase: &Phrase, settings: &PerformanceSettings, bpm: f64, compensate: bool, meter: Meter, ) -> Result { phrase.validate(MAX_PLAYBACK_NOTES)?; settings.validate()?; if !bpm.is_finite() || !(30.0..=300.0).contains(&bpm) { return Err(err("Tempo must be 30–300 BPM.")); } let to_ticks = |ms: u16| (ms as f64 * bpm * PPQ as f64 / 60000.).round() as i32; let mut events = Vec::new(); let mut lead_in = 0; for role in 0..2 { if role == 1 && settings.mode == RoleMode::Solo { continue; } let profile = &settings.voices[role]; let attack = if compensate { to_ticks(profile.attack_ms) } else { 0 }; let release = if compensate { to_ticks(profile.release_ms) } else { 0 }; let routed: Vec<_> = phrase .notes .iter() .enumerate() .filter(|(i, n)| settings.mode.role(*i, n, meter) == role) .map(|(_, n)| n) .collect(); if routed.is_empty() && !profile.controller.enabled { continue; } lead_in = lead_in.max(attack as u32); for (index, n) in routed.iter().enumerate() { let pitch = if n.keeps_pitch() { n.pitch as i16 } else { n.pitch as i16 + profile.transpose as i16 }; if !(0..=127).contains(&pitch) { return Err(err( "A sound-role register offset exceeds MIDI pitch limits.", )); } let on = n.start as i32 - attack; let duration = if n.keeps_rhythm() { n.duration } else { (n.duration as u64 * profile.gate as u64 / 100).max(1) as u32 }; let wrap = routed.first().map_or(phrase.length as i32, |first| { phrase.length as i32 + (first.start as i32 - attack).min(0) }); let next = routed .get(index + 1) .map_or(wrap, |n| n.start as i32 - attack); let off = (n.start as i32 + duration as i32 - release) .min(next) .min(phrase.length as i32) .max(on + 1); let velocity = if n.locked { n.velocity } else { (n.velocity as u32 * profile.velocity as u32 / 100).clamp(1, 127) as u8 }; events.push(TimedMessage { tick: on, role: role as u8, message: MidiMessage { kind: MessageKind::On, channel: profile.channel, key: pitch as u8, value: velocity, rest: 0, }, next_cycle: false, }); events.push(TimedMessage { tick: off, role: role as u8, message: MidiMessage { kind: MessageKind::Off, channel: profile.channel, key: pitch as u8, value: 0, rest: 0, }, next_cycle: false, }); } if profile.controller.enabled { let curve = &profile.controller; let mut points = std::collections::BTreeMap::new(); for tick in (0..=phrase.length).step_by((PPQ / 8) as usize) { points.insert( tick as i32, curve.value_at((tick as u64 * 1000 / phrase.length as u64) as u16), ); } for p in &curve.points { points.insert( (p.position as u64 * phrase.length as u64 / 1000) as i32, p.value, ); } if attack > 0 { points.insert(-attack, curve.value_at(0)); } for (tick, value) in points { events.push(TimedMessage { tick, role: role as u8, message: MidiMessage { kind: MessageKind::Control, channel: profile.channel, key: curve.cc, value, rest: curve.rest, }, next_cycle: false, }); } } } // Negative early triggers are played once during warm-up, and before each subsequent wrap. let wrapped: Vec<_> = events .iter() .filter(|e| e.tick < 0) .map(|e| TimedMessage { tick: e.tick + phrase.length as i32, next_cycle: true, ..*e }) .collect(); // Very short synthetic clips cannot represent their own warm-up as one loop. if lead_in >= phrase.length { return Err(err("Attack compensation exceeds this phrase's length. Reduce it or use a longer phrase.")); } events.extend(wrapped); events.sort_by_key(|e| (e.tick, e.message.kind, e.message.channel, e.message.key)); if events.len() > MAX_PERFORMANCE_EVENTS { return Err(err("Performance exceeds the bounded event limit. Simplify the phrase or controller curve.")); } Ok(Self { length: phrase.length, lead_in, events, }) } pub fn clip(&self) -> Result { if self.events.len() > MAX_PERFORMANCE_EVENTS || self.length == 0 || self.length > MAX_LENGTH || self.lead_in >= self.length { return Err(err("Invalid performance clip.")); } let mut per_tick = std::collections::BTreeMap::new(); for event in &self.events { if event.role > 1 || event.message.channel > 15 || event.message.key > 127 || event.message.value > 127 || event.message.rest > 127 || event.message.kind == MessageKind::On && event.message.value == 0 || event.tick < -(self.lead_in as i32) || event.tick > self.length as i32 || event.next_cycle && event.tick < 0 { return Err(err("Invalid scheduled MIDI event.")); } *per_tick.entry(event.tick).or_insert(0) += 1; } if per_tick.values().any(|&count| count > 16) || self.events.windows(2).any(|p| p[0].tick > p[1].tick) { return Err(err("Unsorted or excessively dense performance events.")); } let mut clip = PerformanceClip { events: [TimedMessage::default(); MAX_PERFORMANCE_EVENTS], count: self.events.len(), length: self.length, lead_in: self.lead_in, }; clip.events[..clip.count].copy_from_slice(&self.events); Ok(clip) } } #[derive(Clone, Copy)] pub struct PerformanceClip { events: [TimedMessage; MAX_PERFORMANCE_EVENTS], count: usize, pub length: u32, pub lead_in: u32, } pub struct EventBatch { pub events: [Option; MAX_FRAME_EVENTS], pub len: usize, } impl Default for EventBatch { fn default() -> Self { Self { events: [None; MAX_FRAME_EVENTS], len: 0, } } } impl EventBatch { pub fn push(&mut self, message: MidiMessage) { assert!(self.len < MAX_FRAME_EVENTS, "bounded MIDI batch overflow"); self.events[self.len] = Some(message); self.len += 1; } } #[derive(Clone, Copy, Default)] struct Voice { held: Option<(u8, u8)>, frequency: f64, phase: f64, gain: f64, level: f64, } impl Voice { fn sample(&mut self, rate: f64, role: usize) -> f32 { let target = if self.held.is_some() { 1. } else { 0. }; let speed = if target > self.gain { 1. / (0.005 * rate) } else { 1. / (0.012 * rate) }; self.gain = if target > self.gain { (self.gain + speed).min(target) } else { (self.gain - speed).max(target) }; let sine = self.phase.sin(); let triangle = 2. / std::f64::consts::PI * sine.asin(); let wave = if role == 0 { sine * 0.8 + triangle * 0.2 } else { triangle * 0.65 + (self.phase * 2.).sin() * 0.35 }; self.phase = (self.phase + TAU * self.frequency / rate) % TAU; (wave * self.gain * 0.1 * self.level) as f32 } } pub struct PerformancePlayer { clip: PerformanceClip, tick: f64, index: usize, cycle: u64, playing: bool, looping: bool, sample_rate: f64, voices: [Voice; 2], controls: [Option; 2], } impl Default for PerformancePlayer { fn default() -> Self { Self { clip: PerformanceClip { events: [TimedMessage::default(); MAX_PERFORMANCE_EVENTS], count: 0, length: 0, lead_in: 0, }, tick: 0., index: 0, cycle: 0, playing: false, looping: false, sample_rate: 44100., voices: [Voice::default(); 2], controls: [None; 2], } } } impl PerformancePlayer { pub fn set_sample_rate(&mut self, rate: f64) -> Result<()> { if !rate.is_finite() || !(8000.0..=384000.0).contains(&rate) { return Err(err("Unsupported sample rate.")); } self.sample_rate = rate; self.stop(); Ok(()) } pub fn set_looping(&mut self, value: bool) { self.looping = value; } pub fn is_playing(&self) -> bool { self.playing } pub fn length(&self) -> u32 { self.clip.length } pub fn tick(&self) -> f64 { self.tick } pub fn absolute_tick(&self) -> f64 { self.cycle as f64 * self.clip.length as f64 + self.tick } pub fn play(&mut self, clip: PerformanceClip) { self.stop(); self.clip = clip; self.tick = -(clip.lead_in as f64); self.playing = true; } pub fn stop(&mut self) { self.playing = false; self.tick = 0.; self.index = 0; self.cycle = 0; self.voices = [Voice::default(); 2]; self.controls = [None; 2]; } fn dispatch_until(&mut self, tick: f64, batch: &mut EventBatch) { while self.index < self.clip.count && self.clip.events[self.index].tick as f64 <= tick { let event = self.clip.events[self.index]; self.index += 1; if event.next_cycle && !self.looping { continue; } let voice = &mut self.voices[event.role as usize]; match event.message.kind { MessageKind::On => { voice.held = Some((event.message.key, event.message.value)); voice.level = event.message.value as f64 / 127.; voice.frequency = 440. * 2.0_f64.powf((event.message.key as f64 - 69.) / 12.); voice.phase = 0.; } MessageKind::Off => { if voice.held.is_some_and(|n| n.0 == event.message.key) { voice.held = None; } } MessageKind::Control => self.controls[event.role as usize] = Some(event.message), } batch.push(event.message); } } pub fn current_messages(&self) -> EventBatch { let mut batch = EventBatch::default(); for role in 0..2 { if let Some(message) = self.controls[role] { batch.push(message); } if let Some((key, value)) = self.voices[role].held { let channel = self.clip.events[..self.clip.count] .iter() .find(|e| e.role as usize == role && e.message.kind == MessageKind::On) .map_or(role as u8, |e| e.message.channel); batch.push(MidiMessage { kind: MessageKind::On, channel, key, value, rest: 0, }); } } batch } pub fn next_frame(&mut self, bpm: f64) -> (f32, EventBatch) { let mut batch = EventBatch::default(); if !self.playing { return (0., batch); } if self.tick >= self.clip.length as f64 { self.dispatch_until(self.clip.length as f64, &mut batch); if self.looping { self.tick -= self.clip.length as f64; self.cycle = self.cycle.wrapping_add(1); self.index = self.clip.events[..self.clip.count].partition_point(|e| e.tick < 0); } else { self.playing = false; return (0., batch); } } self.dispatch_until(self.tick, &mut batch); let sample = self .voices .iter_mut() .enumerate() .map(|(role, v)| v.sample(self.sample_rate, role)) .sum(); self.tick += (if bpm.is_finite() { bpm.clamp(30., 300.) } else { 140. }) * PPQ as f64 / (60. * self.sample_rate); (sample, batch) } }