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::{SmfError, 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 Smf(SmfError),
54 NonMetricalTiming,
57 Pump(PumpError<Infallible, SoundBridgeError>),
59 Bridge(SoundBridgeError),
61 Render(SoundRenderError),
63}
64
65impl fmt::Display for MusicStackError {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 match self {
68 Self::Preflight { .. } => f.write_str("score contains notes that cannot lower to MIDI"),
69 Self::Lower(error) => error.fmt(f),
70 Self::Smf(error) => error.fmt(f),
71 Self::NonMetricalTiming => {
72 f.write_str("the music render stack requires metrical MIDI timing")
73 }
74 Self::Pump(PumpError::Source(_)) => {
75 f.write_str("unexpected in-memory MIDI source failure")
76 }
77 Self::Pump(PumpError::Sink(error)) => error.fmt(f),
78 Self::Bridge(error) => error.fmt(f),
79 Self::Render(error) => error.fmt(f),
80 }
81 }
82}
83
84impl std::error::Error for MusicStackError {}
85
86impl From<LowerError> for MusicStackError {
87 fn from(value: LowerError) -> Self {
88 Self::Lower(value)
89 }
90}
91
92impl From<SmfError> for MusicStackError {
93 fn from(value: SmfError) -> Self {
94 Self::Smf(value)
95 }
96}
97
98impl From<PumpError<Infallible, SoundBridgeError>> for MusicStackError {
99 fn from(value: PumpError<Infallible, SoundBridgeError>) -> Self {
100 Self::Pump(value)
101 }
102}
103
104impl From<SoundBridgeError> for MusicStackError {
105 fn from(value: SoundBridgeError) -> Self {
106 Self::Bridge(value)
107 }
108}
109
110impl From<SoundRenderError> for MusicStackError {
111 fn from(value: SoundRenderError) -> Self {
112 Self::Render(value)
113 }
114}
115
116pub fn render_score(
118 score: &Score,
119 bank: &TimbreBank,
120 tuning: &dyn Tuning,
121 pcm: &PcmRenderer,
122) -> Result<Vec<f32>, MusicStackError> {
123 Ok(render_score_report(score, bank, tuning, pcm)?.samples)
124}
125
126pub fn render_score_report(
128 score: &Score,
129 bank: &TimbreBank,
130 tuning: &dyn Tuning,
131 pcm: &PcmRenderer,
132) -> Result<RenderScoreReport, MusicStackError> {
133 render_score_with_opts_report(score, bank, tuning, pcm, &MusicStackRenderOpts::default())
134}
135
136pub fn render_score_with_opts_report(
138 score: &Score,
139 bank: &TimbreBank,
140 tuning: &dyn Tuning,
141 pcm: &PcmRenderer,
142 opts: &MusicStackRenderOpts,
143) -> Result<RenderScoreReport, MusicStackError> {
144 let diagnostics = preflight_score(score);
145 if diagnostics
146 .iter()
147 .any(|diag| diag.severity == Severity::Error)
148 {
149 return Err(MusicStackError::Preflight { diagnostics });
150 }
151 let smf = lower_score(score, &opts.lower)?;
152 render_smf_with_opts_report(&smf, bank, tuning, pcm, &opts.bridge)
153}
154
155pub fn render_smf_with_opts_report(
157 smf: &SmfFile,
158 bank: &TimbreBank,
159 tuning: &dyn Tuning,
160 pcm: &PcmRenderer,
161 bridge_opts: &BridgeOptions,
162) -> Result<RenderScoreReport, MusicStackError> {
163 let merged = smf.merged_events()?;
164 let ticks_per_quarter = smf
165 .ticks_per_quarter()
166 .ok_or(MusicStackError::NonMetricalTiming)?;
167 let mut diagnostics = collect_midi_diagnostics(&merged, ticks_per_quarter);
168 let events = merged.into_iter().map(|tracked| tracked.event).collect();
169 let mut source = MemoryMidiSource::new(ticks_per_quarter, events);
170 let mut bridge = MidiToSoundBridge::new(
171 ticks_per_quarter,
172 bank.clone(),
173 Box::new(FrozenTuning::from_tuning(tuning)),
174 bridge_opts.clone(),
175 )?;
176 let _ = pump(&mut source, &mut bridge)?;
177 if bridge.stolen_voice_count() > 0 {
178 diagnostics.push(warning(format!(
179 "voice stealing: {} voice(s) were stolen by the bridge polyphony limit",
180 bridge.stolen_voice_count()
181 )));
182 }
183 let tones = bridge.drain_tones();
184 let samples = pcm.render_mix(&tones);
185 if let Some(peak) = peak_abs_sample(&samples)
186 && peak > 1.0
187 {
188 diagnostics.push(warning(format!(
189 "audio clipping: render peak {:.3} exceeds PCM full scale",
190 peak
191 )));
192 }
193 Ok(RenderScoreReport {
194 smf: smf.clone(),
195 tones,
196 samples,
197 diagnostics,
198 })
199}
200
201fn preflight_score(score: &Score) -> Vec<Diagnostic> {
202 let mut atoms = Vec::new();
203 score.body.voices(Time::from_integer(0), &mut atoms);
204 atoms
205 .into_iter()
206 .filter_map(|timed| match timed.atom {
207 sim_lib_music_core::AtomRef::Note(note) if note.pitch.to_midi().is_none() => {
208 Some(Diagnostic::error(format!(
209 "pitch clipping: {:?} at onset {} cannot lower to MIDI",
210 note.pitch, timed.onset
211 )))
212 }
213 _ => None,
214 })
215 .collect()
216}
217
218fn collect_midi_diagnostics(
219 merged: &[TrackedMidiEvent],
220 ticks_per_quarter: u32,
221) -> Vec<Diagnostic> {
222 let unknown_meta = merged
223 .iter()
224 .filter(|tracked| {
225 matches!(
226 tracked.event.payload,
227 MidiPayload::Meta(MetaEvent::Other(_))
228 )
229 })
230 .count();
231 let quantized = merged
232 .iter()
233 .filter(|tracked| tracked.event.time.tpq != ticks_per_quarter)
234 .count();
235 let mut diagnostics = Vec::new();
236 if unknown_meta > 0 {
237 diagnostics.push(warning(format!(
238 "unknown MIDI meta: {} event(s) preserved but ignored by the sound bridge",
239 unknown_meta
240 )));
241 }
242 if quantized > 0 {
243 diagnostics.push(warning(format!(
244 "tick quantization: {} event(s) required TPQ rebasing into {} TPQ",
245 quantized, ticks_per_quarter
246 )));
247 }
248 diagnostics
249}
250
251fn peak_abs_sample(samples: &[f32]) -> Option<f32> {
252 samples
253 .iter()
254 .map(|sample| sample.abs())
255 .max_by(|left, right| left.total_cmp(right))
256}
257
258fn warning(message: String) -> Diagnostic {
259 Diagnostic {
260 severity: Severity::Warning,
261 message,
262 source: None,
263 span: None,
264 code: None,
265 related: Vec::new(),
266 }
267}
268
269#[derive(Clone, Debug, PartialEq)]
270struct FrozenTuning {
271 name: &'static str,
272 reference: (Pitch, Frequency),
273 divisions: u32,
274 midi_frequencies: [Frequency; 128],
275}
276
277impl FrozenTuning {
278 fn from_tuning(tuning: &dyn Tuning) -> Self {
279 let mut midi_frequencies = [Frequency(440.0); 128];
280 for (midi, slot) in midi_frequencies.iter_mut().enumerate() {
281 *slot = tuning.frequency_of(Pitch::from_midi(midi as u8));
282 }
283 Self {
284 name: tuning.name(),
285 reference: tuning.reference(),
286 divisions: tuning.divisions(),
287 midi_frequencies,
288 }
289 }
290}
291
292impl Tuning for FrozenTuning {
293 fn name(&self) -> &'static str {
294 self.name
295 }
296
297 fn reference(&self) -> (Pitch, Frequency) {
298 self.reference
299 }
300
301 fn frequency_of(&self, pitch: Pitch) -> Frequency {
302 match pitch.to_midi() {
303 Some(midi) => self.midi_frequencies[midi as usize],
304 None => self
305 .reference
306 .1
307 .shift_cents(f64::from(pitch.semitone() - self.reference.0.semitone()) * 100.0),
308 }
309 }
310
311 fn pitch_of(&self, frequency: Frequency) -> Pitch {
312 self.midi_frequencies
313 .iter()
314 .enumerate()
315 .min_by(|(_, left), (_, right)| {
316 frequency
317 .cents_above(**left)
318 .abs()
319 .total_cmp(&frequency.cents_above(**right).abs())
320 })
321 .map(|(midi, _)| Pitch::from_midi(midi as u8))
322 .unwrap_or(self.reference.0)
323 }
324
325 fn divisions(&self) -> u32 {
326 self.divisions
327 }
328
329 fn frequency_of_degree(
330 &self,
331 degree: PitchClassN,
332 octave: i16,
333 ) -> Result<Frequency, SoundTuningError> {
334 let pitch = self.pitch_from_degree(degree, octave)?;
335 Ok(self.frequency_of(pitch))
336 }
337}