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#[derive(Clone, Debug, Default, PartialEq)]
21pub struct MusicStackRenderOpts {
22 pub lower: LowerOpts,
24 pub bridge: BridgeOptions,
26}
27
28#[derive(Clone, Debug, PartialEq)]
31pub struct RenderScoreReport {
32 pub smf: SmfFile,
34 pub tones: Vec<ScheduledTone>,
36 pub samples: Vec<f32>,
38 pub diagnostics: Vec<Diagnostic>,
40}
41
42#[derive(Debug)]
44pub enum MusicStackError {
45 Preflight {
47 diagnostics: Vec<Diagnostic>,
49 },
50 Lower(LowerError),
52 Pump(PumpError<Infallible, SoundBridgeError>),
54 Bridge(SoundBridgeError),
56 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
101pub 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
111pub 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
121pub 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
140pub 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}