Skip to main content

sim/
music_stack.rs

1use std::convert::Infallible;
2use std::fmt;
3
4use sim_kernel::{Diagnostic, Severity};
5use sim_lib_midi_core::{
6    MemoryMidiSource, MetaEvent, MidiPayload, PumpError, TrackedMidiEvent, pump,
7};
8use sim_lib_midi_smf::SmfFile;
9use sim_lib_music_core::{MusicObject, Score, Time};
10use sim_lib_music_lower::{LowerError, LowerOpts, lower_score};
11use sim_lib_pitch_core::Pitch;
12use sim_lib_sound_bridge::{
13    BridgeOptions, MidiToSoundBridge, ScheduledTone, SoundBridgeError, TimbreBank,
14};
15use sim_lib_sound_core::Frequency;
16use sim_lib_sound_render::{PcmRenderer, SoundRenderError};
17use sim_lib_sound_tuning::{PitchClassN, SoundTuningError, Tuning};
18
19/// Options controlling how a score is lowered to MIDI and bridged to sound.
20#[derive(Clone, Debug, Default, PartialEq)]
21pub struct MusicStackRenderOpts {
22    /// Options for lowering a score to a MIDI file.
23    pub lower: LowerOpts,
24    /// Options for bridging MIDI events to scheduled sound tones.
25    pub bridge: BridgeOptions,
26}
27
28/// Result of rendering a score: the lowered MIDI, scheduled tones, PCM samples,
29/// and any diagnostics collected along the way.
30#[derive(Clone, Debug, PartialEq)]
31pub struct RenderScoreReport {
32    /// MIDI file the score lowered to.
33    pub smf: SmfFile,
34    /// Tones scheduled from the MIDI events.
35    pub tones: Vec<ScheduledTone>,
36    /// Rendered PCM audio samples.
37    pub samples: Vec<f32>,
38    /// Diagnostics gathered during rendering.
39    pub diagnostics: Vec<Diagnostic>,
40}
41
42/// Error raised at one of the stages of the music rendering stack.
43#[derive(Debug)]
44pub enum MusicStackError {
45    /// The score failed preflight: some notes cannot lower to MIDI.
46    Preflight {
47        /// Diagnostics explaining why preflight failed.
48        diagnostics: Vec<Diagnostic>,
49    },
50    /// Lowering the score to MIDI failed.
51    Lower(LowerError),
52    /// Pumping MIDI events through the sound bridge failed.
53    Pump(PumpError<Infallible, SoundBridgeError>),
54    /// The MIDI-to-sound bridge failed.
55    Bridge(SoundBridgeError),
56    /// Rendering scheduled tones to PCM failed.
57    Render(SoundRenderError),
58}
59
60impl fmt::Display for MusicStackError {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        match self {
63            Self::Preflight { .. } => f.write_str("score contains notes that cannot lower to MIDI"),
64            Self::Lower(error) => error.fmt(f),
65            Self::Pump(PumpError::Source(_)) => {
66                f.write_str("unexpected in-memory MIDI source failure")
67            }
68            Self::Pump(PumpError::Sink(error)) => error.fmt(f),
69            Self::Bridge(error) => error.fmt(f),
70            Self::Render(error) => error.fmt(f),
71        }
72    }
73}
74
75impl std::error::Error for MusicStackError {}
76
77impl From<LowerError> for MusicStackError {
78    fn from(value: LowerError) -> Self {
79        Self::Lower(value)
80    }
81}
82
83impl From<PumpError<Infallible, SoundBridgeError>> for MusicStackError {
84    fn from(value: PumpError<Infallible, SoundBridgeError>) -> Self {
85        Self::Pump(value)
86    }
87}
88
89impl From<SoundBridgeError> for MusicStackError {
90    fn from(value: SoundBridgeError) -> Self {
91        Self::Bridge(value)
92    }
93}
94
95impl From<SoundRenderError> for MusicStackError {
96    fn from(value: SoundRenderError) -> Self {
97        Self::Render(value)
98    }
99}
100
101/// Renders a score to PCM samples with default options.
102pub fn render_score(
103    score: &Score,
104    bank: &TimbreBank,
105    tuning: &dyn Tuning,
106    pcm: &PcmRenderer,
107) -> Result<Vec<f32>, MusicStackError> {
108    Ok(render_score_report(score, bank, tuning, pcm)?.samples)
109}
110
111/// Renders a score with default options, returning the full render report.
112pub fn render_score_report(
113    score: &Score,
114    bank: &TimbreBank,
115    tuning: &dyn Tuning,
116    pcm: &PcmRenderer,
117) -> Result<RenderScoreReport, MusicStackError> {
118    render_score_with_opts_report(score, bank, tuning, pcm, &MusicStackRenderOpts::default())
119}
120
121/// Renders a score with explicit options, returning the full render report.
122pub fn render_score_with_opts_report(
123    score: &Score,
124    bank: &TimbreBank,
125    tuning: &dyn Tuning,
126    pcm: &PcmRenderer,
127    opts: &MusicStackRenderOpts,
128) -> Result<RenderScoreReport, MusicStackError> {
129    let diagnostics = preflight_score(score);
130    if diagnostics
131        .iter()
132        .any(|diag| diag.severity == Severity::Error)
133    {
134        return Err(MusicStackError::Preflight { diagnostics });
135    }
136    let smf = lower_score(score, &opts.lower)?;
137    render_smf_with_opts_report(&smf, bank, tuning, pcm, &opts.bridge)
138}
139
140/// Renders an already-lowered MIDI file to PCM with explicit bridge options.
141pub fn render_smf_with_opts_report(
142    smf: &SmfFile,
143    bank: &TimbreBank,
144    tuning: &dyn Tuning,
145    pcm: &PcmRenderer,
146    bridge_opts: &BridgeOptions,
147) -> Result<RenderScoreReport, MusicStackError> {
148    let merged = smf.merged_events();
149    let mut diagnostics = collect_midi_diagnostics(smf, &merged);
150    let events = merged.into_iter().map(|tracked| tracked.event).collect();
151    let mut source = MemoryMidiSource::new(smf.tpq, events);
152    let mut bridge = MidiToSoundBridge::new(
153        smf.tpq,
154        bank.clone(),
155        Box::new(FrozenTuning::from_tuning(tuning)),
156        bridge_opts.clone(),
157    )?;
158    let _ = pump(&mut source, &mut bridge)?;
159    if bridge.stolen_voice_count() > 0 {
160        diagnostics.push(warning(format!(
161            "voice stealing: {} voice(s) were stolen by the bridge polyphony limit",
162            bridge.stolen_voice_count()
163        )));
164    }
165    let tones = bridge.drain_tones();
166    let samples = pcm.render_mix(&tones);
167    if let Some(peak) = peak_abs_sample(&samples)
168        && peak > 1.0
169    {
170        diagnostics.push(warning(format!(
171            "audio clipping: render peak {:.3} exceeds PCM full scale",
172            peak
173        )));
174    }
175    Ok(RenderScoreReport {
176        smf: smf.clone(),
177        tones,
178        samples,
179        diagnostics,
180    })
181}
182
183fn preflight_score(score: &Score) -> Vec<Diagnostic> {
184    let mut atoms = Vec::new();
185    score.body.voices(Time::from_integer(0), &mut atoms);
186    atoms
187        .into_iter()
188        .filter_map(|timed| match timed.atom {
189            sim_lib_music_core::AtomRef::Note(note) if note.pitch.to_midi().is_none() => {
190                Some(Diagnostic::error(format!(
191                    "pitch clipping: {:?} at onset {} cannot lower to MIDI",
192                    note.pitch, timed.onset
193                )))
194            }
195            _ => None,
196        })
197        .collect()
198}
199
200fn collect_midi_diagnostics(smf: &SmfFile, merged: &[TrackedMidiEvent]) -> Vec<Diagnostic> {
201    let unknown_meta = merged
202        .iter()
203        .filter(|tracked| {
204            matches!(
205                tracked.event.payload,
206                MidiPayload::Meta(MetaEvent::Other(_))
207            )
208        })
209        .count();
210    let quantized = merged
211        .iter()
212        .filter(|tracked| tracked.event.time.tpq != smf.tpq)
213        .count();
214    let mut diagnostics = Vec::new();
215    if unknown_meta > 0 {
216        diagnostics.push(warning(format!(
217            "unknown MIDI meta: {} event(s) preserved but ignored by the sound bridge",
218            unknown_meta
219        )));
220    }
221    if quantized > 0 {
222        diagnostics.push(warning(format!(
223            "tick quantization: {} event(s) required TPQ rebasing into {} TPQ",
224            quantized, smf.tpq
225        )));
226    }
227    diagnostics
228}
229
230fn peak_abs_sample(samples: &[f32]) -> Option<f32> {
231    samples
232        .iter()
233        .map(|sample| sample.abs())
234        .max_by(|left, right| left.total_cmp(right))
235}
236
237fn warning(message: String) -> Diagnostic {
238    Diagnostic {
239        severity: Severity::Warning,
240        message,
241        source: None,
242        span: None,
243        code: None,
244        related: Vec::new(),
245    }
246}
247
248#[derive(Clone, Debug, PartialEq)]
249struct FrozenTuning {
250    name: &'static str,
251    reference: (Pitch, Frequency),
252    divisions: u32,
253    midi_frequencies: [Frequency; 128],
254}
255
256impl FrozenTuning {
257    fn from_tuning(tuning: &dyn Tuning) -> Self {
258        let mut midi_frequencies = [Frequency(440.0); 128];
259        for (midi, slot) in midi_frequencies.iter_mut().enumerate() {
260            *slot = tuning.frequency_of(Pitch::from_midi(midi as u8));
261        }
262        Self {
263            name: tuning.name(),
264            reference: tuning.reference(),
265            divisions: tuning.divisions(),
266            midi_frequencies,
267        }
268    }
269}
270
271impl Tuning for FrozenTuning {
272    fn name(&self) -> &'static str {
273        self.name
274    }
275
276    fn reference(&self) -> (Pitch, Frequency) {
277        self.reference
278    }
279
280    fn frequency_of(&self, pitch: Pitch) -> Frequency {
281        match pitch.to_midi() {
282            Some(midi) => self.midi_frequencies[midi as usize],
283            None => self
284                .reference
285                .1
286                .shift_cents(f64::from(pitch.semitone() - self.reference.0.semitone()) * 100.0),
287        }
288    }
289
290    fn pitch_of(&self, frequency: Frequency) -> Pitch {
291        self.midi_frequencies
292            .iter()
293            .enumerate()
294            .min_by(|(_, left), (_, right)| {
295                frequency
296                    .cents_above(**left)
297                    .abs()
298                    .total_cmp(&frequency.cents_above(**right).abs())
299            })
300            .map(|(midi, _)| Pitch::from_midi(midi as u8))
301            .unwrap_or(self.reference.0)
302    }
303
304    fn divisions(&self) -> u32 {
305        self.divisions
306    }
307
308    fn frequency_of_degree(
309        &self,
310        degree: PitchClassN,
311        octave: i16,
312    ) -> Result<Frequency, SoundTuningError> {
313        let pitch = self.pitch_from_degree(degree, octave)?;
314        Ok(self.frequency_of(pitch))
315    }
316}