1use std::collections::{BTreeSet, HashMap};
16use std::error::Error;
17use std::fs::{self, File};
18use std::io::Read;
19use std::path::PathBuf;
20use std::sync::{
21 atomic::{AtomicBool, AtomicUsize, Ordering},
22 Arc,
23};
24use std::time::{Duration, SystemTime};
25
26use atomic_float::AtomicF32;
27use bon::Builder;
28use nodi::{
29 midly::{MetaMessage, MidiMessage, Smf, Timing, TrackEventKind},
30 timers::TimeFormatError,
31};
32use ringbuf::{traits::*, HeapCons, HeapProd, HeapRb};
33use rustysynth::{SoundFont, Synthesizer, SynthesizerSettings};
34
35pub struct Player {
37 sheet_receiver: HeapCons<Option<Arc<MidiSheet>>>,
38 track_filter_listener: HeapCons<TrackFilter>,
39 tempo_rate: Arc<AtomicF32>,
40 volume: Arc<AtomicF32>,
41 note_off_all_listener: HeapCons<bool>,
42 is_playing: Arc<AtomicBool>,
43 position: Arc<AtomicUsize>,
44 previous_position: usize,
45 settings: Settings,
46 sheet: Option<Arc<MidiSheet>>,
47 track_filter: TrackFilter,
48 synthesizer: Synthesizer,
49 tick_clock: u32, }
51
52impl Player {
53 pub fn new(
55 soundfont: &str,
56 settings: Settings,
57 ) -> Result<(Self, PlayerController), Box<dyn Error>> {
58 let sf_file = fs::read(soundfont)?;
59 let sf = SoundFont::new(&mut sf_file.as_slice())?;
60 let sf = Arc::new(sf);
61 let synthesizer = Synthesizer::new(&sf, &settings.clone().into())?;
62 let rb = HeapRb::new(1);
63 let (sheet_sender, sheet_receiver) = rb.split();
64 let rb = HeapRb::new(64);
65 let (track_filter_sender, track_filter_listener) = rb.split();
66 let rb = HeapRb::new(1);
67 let (note_off_all_sender, note_off_all_listener) = rb.split();
68 let is_playing = Arc::new(AtomicBool::new(false));
69 let position = Arc::new(AtomicUsize::new(0));
70 let sample_rate = settings.sample_rate;
71 let tempo_rate = Arc::new(AtomicF32::new(1.0));
72 let volume = Arc::new(AtomicF32::new(1.0));
73
74 Ok((
75 Self {
76 is_playing: is_playing.clone(),
77 position: position.clone(),
78 tempo_rate: tempo_rate.clone(),
79 volume: volume.clone(),
80 sheet_receiver,
81 track_filter_listener,
82 note_off_all_listener,
83 settings,
84 synthesizer,
85 sheet: None,
86 track_filter: TrackFilter::new(0),
87 tick_clock: 0,
88 previous_position: 0,
89 },
90 PlayerController {
91 is_playing,
92 position,
93 tempo_rate,
94 volume,
95 sheet_length: Default::default(),
96 sheet: None,
97 sheet_sender,
98 track_filter_sender,
99 track_filter: TrackFilter::new(0),
100 note_off_all_sender,
101 cache: Cache::new(sample_rate),
102 },
103 ))
104 }
105
106 pub fn settings(&self) -> &Settings {
108 &self.settings
109 }
110
111 pub fn render(&mut self, left: &mut [f32], right: &mut [f32]) {
113 if left.len() != right.len() {
114 panic!("left and right channel buffer size cannot be different");
115 }
116
117 if let Some(should_note_off) = self.note_off_all_listener.try_pop() {
118 if should_note_off {
119 self.synthesizer.note_off_all(false);
120 self.synthesizer.render(left, right);
121 return;
122 }
123 }
124
125 if !self.is_playing.load(Ordering::Relaxed) {
126 self.synthesizer.render(left, right);
127 return;
128 }
129
130 if let Some(sheet) = self.sheet_receiver.try_pop() {
131 self.sheet = sheet;
132 }
133 if let Some(track_filter) = self.track_filter_listener.try_pop() {
134 self.track_filter = track_filter;
135 }
136
137 if let Some(sheet) = &self.sheet {
138 self.synthesizer
139 .set_master_volume(self.volume.load(Ordering::Relaxed));
140 for _ in 0..left.len() {
141 let position = self.position.load(Ordering::Relaxed);
142 if position == sheet.pulses.len() {
143 self.is_playing.store(false, Ordering::Relaxed);
144 self.synthesizer.note_off_all(false);
145 return;
146 }
147
148 if position != self.previous_position {
150 self.tick_clock = 0;
151 self.previous_position = position;
152 }
153
154 let pulse = &sheet.pulses[position];
155
156 if self.tick_clock == 0 {
157 for event in &pulse.events {
158 if !self.track_filter.allows(event.track_index) {
159 continue;
160 }
161 self.synthesizer.process_midi_message(
162 event.channel,
163 event.command,
164 event.data1,
165 event.data2,
166 );
167 }
168 }
169
170 self.tick_clock += 1;
171
172 let pulse_duration = (pulse.duration as f32
173 * self.tempo_rate.load(Ordering::Relaxed))
174 .round() as u32;
175
176 if self.tick_clock == pulse_duration && position < sheet.pulses.len() {
177 self.position.store(position + 1, Ordering::Relaxed);
178 }
179 }
180
181 self.synthesizer.render(left, right);
182 }
183 }
184}
185
186pub struct PlayerController {
188 is_playing: Arc<AtomicBool>,
189 position: Arc<AtomicUsize>,
190 tempo_rate: Arc<AtomicF32>,
191 volume: Arc<AtomicF32>,
192 sheet_length: Arc<AtomicUsize>,
194 sheet: Option<Arc<MidiSheet>>,
195 sheet_sender: HeapProd<Option<Arc<MidiSheet>>>,
196 track_filter_sender: HeapProd<TrackFilter>,
197 track_filter: TrackFilter,
198 note_off_all_sender: HeapProd<bool>,
199 cache: Cache,
200}
201
202impl PlayerController {
203 pub fn play(&self) -> bool {
207 if self.sheet.is_none() {
208 self.is_playing.store(false, Ordering::SeqCst);
209 return false;
210 }
211
212 let position = self.position();
213
214 if self.is_playing() && position < 1.0 {
215 return true;
216 }
217
218 if position == 1.0 {
219 self.is_playing.store(false, Ordering::Relaxed);
220 return false;
221 }
222
223 self.is_playing.store(true, Ordering::Relaxed);
224
225 true
226 }
227
228 pub fn is_playing(&self) -> bool {
230 self.is_playing.load(Ordering::SeqCst)
231 }
232
233 pub fn stop(&mut self) {
235 if self.is_playing() {
236 self.is_playing.store(false, Ordering::SeqCst);
237 self.note_off_all();
238 }
239 }
240
241 pub fn set_position_ticks(&self, value: u64) {
246 let length = self.sheet_length.load(Ordering::Relaxed);
247 if length == 0 {
248 return;
249 }
250
251 let tick = value.min(length as u64) as usize;
252 self.position.store(tick, Ordering::Relaxed);
253 }
254
255 pub fn position_ticks(&self) -> u64 {
257 self.position.load(Ordering::Relaxed) as u64
258 }
259
260 pub fn total_ticks(&self) -> u64 {
262 self.sheet_length.load(Ordering::Relaxed) as u64
263 }
264
265 pub fn set_position(&self, value: f64) {
269 let total_ticks = self.total_ticks();
270 if total_ticks == 0 {
271 return;
272 }
273
274 let position = value.max(0.0).min(1.0);
275 let tick = (total_ticks as f64 * position) as u64;
276 self.set_position_ticks(tick);
277 }
278
279 pub fn position(&self) -> f64 {
281 let total_ticks = self.total_ticks();
282 if total_ticks == 0 {
283 return 0.0;
284 }
285
286 let position = self.position_ticks() as f64;
287 (position / total_ticks as f64).max(0.0).min(1.0)
288 }
289
290 pub fn new_position_observer(&self) -> PositionObserver {
292 PositionObserver {
293 position: self.position.clone(),
294 length: self.sheet_length.clone(),
295 }
296 }
297
298 pub fn set_tempo(&mut self, tempo: f32) {
300 if let Some(sheet) = &mut self.sheet {
301 self.tempo_rate
302 .store(sheet.tempo / tempo, Ordering::Relaxed);
303 }
304 }
305
306 pub fn tempo(&self) -> Option<f32> {
310 self.sheet
311 .as_ref()
312 .map(|s| s.tempo / self.tempo_rate.load(Ordering::SeqCst))
313 }
314
315 pub fn set_volume(&mut self, volume: f32) {
317 self.volume.store(volume.max(0.0), Ordering::Relaxed);
318 }
319
320 pub fn volume(&self) -> f32 {
322 self.volume.load(Ordering::Relaxed)
323 }
324
325 pub fn duration(&self) -> Duration {
327 self.sheet
328 .as_ref()
329 .map(|s| s.duration())
330 .unwrap_or_default()
331 }
332
333 pub fn track_infos(&self) -> Vec<MidiTrackInfo> {
335 self.sheet
336 .as_ref()
337 .map(|sheet| sheet.track_infos.clone())
338 .unwrap_or_default()
339 }
340
341 pub fn track_count(&self) -> usize {
343 self.sheet
344 .as_ref()
345 .map(|sheet| sheet.track_infos.len())
346 .unwrap_or(0)
347 }
348
349 pub fn set_track_muted(&mut self, track_index: usize, muted: bool) -> bool {
353 let changed = self.track_filter.set_muted(track_index, muted);
354 if changed {
355 self.publish_track_filter();
356 self.note_off_all();
357 }
358 changed
359 }
360
361 pub fn is_track_muted(&self, track_index: usize) -> Option<bool> {
365 self.track_filter.is_muted(track_index)
366 }
367
368 pub fn clear_track_mutes(&mut self) -> bool {
372 let changed = self.track_filter.clear_mutes();
373 if changed {
374 self.publish_track_filter();
375 self.note_off_all();
376 }
377 changed
378 }
379
380 pub fn set_track_solo(&mut self, track_index: usize, soloed: bool) -> bool {
384 let changed = self.track_filter.set_solo(track_index, soloed);
385 if changed {
386 self.publish_track_filter();
387 self.note_off_all();
388 }
389 changed
390 }
391
392 pub fn is_track_solo(&self, track_index: usize) -> Option<bool> {
396 self.track_filter.is_solo(track_index)
397 }
398
399 pub fn clear_track_solos(&mut self) -> bool {
403 let changed = self.track_filter.clear_solos();
404 if changed {
405 self.publish_track_filter();
406 self.note_off_all();
407 }
408 changed
409 }
410
411 pub fn set_file(&mut self, path: Option<impl Into<PathBuf>>) -> Result<(), Box<dyn Error>> {
416 match path {
417 Some(path) => self.open_file(path),
418 None => {
419 self.offload_file();
420 Ok(())
421 }
422 }
423 }
424
425 fn offload_file(&mut self) {
426 self.stop();
427 self.sheet_length.store(0, Ordering::SeqCst);
428 self.sheet_sender
429 .try_push(None)
430 .expect("ringbuf producer must be big enough to handle new files");
431 self.sheet = None;
432 self.track_filter = TrackFilter::new(0);
433 self.publish_track_filter();
434 self.tempo_rate.store(1.0, Ordering::Relaxed);
435 self.set_position(0.0);
436 }
437
438 fn open_file(&mut self, path: impl Into<PathBuf>) -> Result<(), Box<dyn Error>> {
439 self.stop();
440 let sheet = self.cache.open(&path.into())?;
441 self.sheet_length.store(sheet.total_ticks, Ordering::SeqCst);
442 self.sheet_sender
443 .try_push(Some(sheet.clone()))
444 .expect("ringbuf producer must be big enough to handle new files");
445 self.sheet = Some(sheet);
446 self.track_filter = TrackFilter::new(self.track_count());
447 self.publish_track_filter();
448 self.tempo_rate.store(1.0, Ordering::Relaxed);
449 self.set_position(0.0);
450
451 Ok(())
452 }
453
454 pub fn note_off_all(&mut self) {
456 self.note_off_all_sender
457 .try_push(true)
458 .expect("ringbuf must be big enough for sending note off all message");
459 }
460
461 fn publish_track_filter(&mut self) {
462 let _ = self.track_filter_sender.try_push(self.track_filter.clone());
463 }
464}
465
466#[derive(Debug, Clone)]
467struct TrackFilter {
468 muted: Vec<bool>,
469 soloed: Vec<bool>,
470 solo_count: usize,
471}
472
473impl TrackFilter {
474 fn new(track_count: usize) -> Self {
475 Self {
476 muted: vec![false; track_count],
477 soloed: vec![false; track_count],
478 solo_count: 0,
479 }
480 }
481
482 fn set_muted(&mut self, track_index: usize, muted: bool) -> bool {
483 let Some(current) = self.muted.get_mut(track_index) else {
484 return false;
485 };
486 if *current == muted {
487 return false;
488 }
489
490 *current = muted;
491 true
492 }
493
494 fn is_muted(&self, track_index: usize) -> Option<bool> {
495 self.muted.get(track_index).copied()
496 }
497
498 fn clear_mutes(&mut self) -> bool {
499 let mut changed = false;
500 for muted in &mut self.muted {
501 if *muted {
502 *muted = false;
503 changed = true;
504 }
505 }
506 changed
507 }
508
509 fn set_solo(&mut self, track_index: usize, soloed: bool) -> bool {
510 let Some(current) = self.soloed.get_mut(track_index) else {
511 return false;
512 };
513 if *current == soloed {
514 return false;
515 }
516
517 *current = soloed;
518 if soloed {
519 self.solo_count += 1;
520 } else {
521 self.solo_count = self.solo_count.saturating_sub(1);
522 }
523 true
524 }
525
526 fn is_solo(&self, track_index: usize) -> Option<bool> {
527 self.soloed.get(track_index).copied()
528 }
529
530 fn clear_solos(&mut self) -> bool {
531 if self.solo_count == 0 {
532 return false;
533 }
534
535 for soloed in &mut self.soloed {
536 *soloed = false;
537 }
538 self.solo_count = 0;
539 true
540 }
541
542 fn allows(&self, track_index: usize) -> bool {
543 let muted = self.muted.get(track_index).copied().unwrap_or(false);
544 let soloed = self.soloed.get(track_index).copied().unwrap_or(false);
545 if self.solo_count > 0 {
546 soloed && !muted
547 } else {
548 !muted
549 }
550 }
551}
552
553struct Cache {
554 sample_rate: u32,
555 map: HashMap<PathBuf, Arc<MidiSheet>>,
556}
557
558impl Cache {
559 fn new(sample_rate: u32) -> Self {
560 Self {
561 sample_rate,
562 map: HashMap::new(),
563 }
564 }
565
566 fn open(&mut self, path: &PathBuf) -> Result<Arc<MidiSheet>, Box<dyn Error>> {
567 let file = File::open(path)?;
568
569 match self.map.get(path) {
570 Some(s) => {
571 if file.metadata()?.modified()? == s.modified {
572 Ok(s.clone())
573 } else {
574 self.upsert(path, file)
575 }
576 }
577
578 None => self.upsert(path, file),
579 }
580 }
581
582 fn upsert(&mut self, path: &PathBuf, file: File) -> Result<Arc<MidiSheet>, Box<dyn Error>> {
583 let sheet = Arc::new(MidiSheet::new(file, self.sample_rate)?);
584 self.map.insert(path.clone(), sheet.clone());
585
586 Ok(sheet)
587 }
588}
589
590#[derive(Debug, Clone)]
592pub struct PositionObserver {
593 position: Arc<AtomicUsize>,
594 length: Arc<AtomicUsize>,
595}
596
597impl PositionObserver {
598 pub fn get(&self) -> f32 {
600 let length = self.length.load(Ordering::Relaxed);
601 if length == 0 {
602 return 0.0;
603 }
604
605 self.position.load(Ordering::Relaxed) as f32 / length as f32
606 }
607
608 pub fn ticks(&self) -> u64 {
610 self.position.load(Ordering::Relaxed) as u64
611 }
612
613 pub fn total_ticks(&self) -> u64 {
615 self.length.load(Ordering::Relaxed) as u64
616 }
617}
618
619#[allow(missing_docs)]
621#[derive(Builder, Clone, Debug)]
622pub struct Settings {
623 #[builder(default = 44100)]
624 pub sample_rate: u32,
625 #[builder(default = 64)]
626 pub block_size: u32,
627 #[builder(default = 512)]
628 pub audio_buffer_size: u32,
629 #[builder(default = 64)]
630 pub max_polyphony: u8,
631 #[builder(default = true)]
632 pub enable_effects: bool,
633}
634
635impl From<Settings> for SynthesizerSettings {
636 fn from(settings: Settings) -> Self {
637 let mut result = SynthesizerSettings::new(settings.sample_rate as i32);
639 result.block_size = settings.block_size as usize;
640 result.maximum_polyphony = settings.max_polyphony as usize;
641 result.enable_reverb_and_chorus = settings.enable_effects;
642
643 result
644 }
645}
646
647#[derive(Debug, Clone, PartialEq, Eq)]
649pub struct MidiTrackInfo {
650 pub index: usize,
652 pub name: Option<String>,
654 pub channels: Vec<u8>,
656 pub programs: Vec<u8>,
658}
659
660#[derive(Debug, Clone)]
661struct MidiSheet {
662 sample_rate: u32,
663 tempo: f32,
664 pulses: Vec<Pulse>,
666 total_ticks: usize,
667 track_infos: Vec<MidiTrackInfo>,
668 modified: SystemTime,
669}
670
671impl MidiSheet {
672 fn new(mut file: File, sample_rate: u32) -> Result<Self, Box<dyn Error>> {
673 let modified = file.metadata()?.modified()?;
674 let mut buf = Vec::new();
675 file.read_to_end(&mut buf)?;
676 let Smf { header, tracks } = Smf::parse(&buf)?;
677 let ppqn = match header.timing {
678 Timing::Metrical(n) => u16::from(n),
679 _ => return Err(TimeFormatError.into()),
680 };
681 let mut tick_events: HashMap<u64, Vec<RawMidiEvent>> = HashMap::new();
682 let mut tempo_changes: HashMap<u64, u32> = HashMap::new();
683 let mut max_tick = 0_u64;
684 let mut track_infos = Vec::with_capacity(tracks.len());
685
686 for (track_index, track) in tracks.iter().enumerate() {
687 let mut absolute_tick = 0_u64;
688 let mut name = None;
689 let mut channels = BTreeSet::new();
690 let mut programs = BTreeSet::new();
691
692 for event in track {
693 absolute_tick = absolute_tick.saturating_add(u64::from(event.delta.as_int()));
694 max_tick = max_tick.max(absolute_tick);
695
696 match event.kind {
697 TrackEventKind::Midi { channel, message } => {
698 channels.insert(channel.as_int() + 1);
699 if let MidiMessage::ProgramChange { program } = message {
700 programs.insert(program.as_int());
701 }
702 tick_events.entry(absolute_tick).or_default().push(
703 RawMidiEvent::from_track_event(track_index, channel.as_int(), message),
704 );
705 }
706 TrackEventKind::Meta(MetaMessage::Tempo(tempo)) => {
707 tempo_changes.insert(absolute_tick, tempo.as_int());
708 }
709 TrackEventKind::Meta(MetaMessage::TrackName(track_name)) if name.is_none() => {
710 let decoded = String::from_utf8_lossy(track_name).trim().to_string();
711 if !decoded.is_empty() {
712 name = Some(decoded);
713 }
714 }
715 _ => {}
716 }
717 }
718
719 track_infos.push(MidiTrackInfo {
720 index: track_index,
721 name,
722 channels: channels.into_iter().collect(),
723 programs: programs.into_iter().collect(),
724 });
725 }
726
727 let total_ticks_u64 = if tracks.is_empty() {
728 0
729 } else {
730 max_tick.saturating_add(1)
731 };
732 let total_ticks = usize::try_from(total_ticks_u64)
733 .map_err(|_| "MIDI timeline is too large for this platform")?;
734
735 let initial_tempo = tempo_changes
736 .iter()
737 .min_by_key(|(tick, _tempo)| **tick)
738 .map(|(_tick, tempo)| *tempo)
739 .unwrap_or(500_000);
740 let tempo = us_per_beat_to_bpm(initial_tempo);
741 let mut duration =
742 Pulse::duration_in_samples(u64::from(initial_tempo), ppqn as u64, sample_rate as u64);
743 let mut pulses = Vec::with_capacity(total_ticks);
744
745 for tick in 0..total_ticks {
746 if let Some(tempo) = tempo_changes.get(&(tick as u64)) {
747 duration =
748 Pulse::duration_in_samples(u64::from(*tempo), ppqn as u64, sample_rate as u64);
749 }
750
751 pulses.push(Pulse {
752 duration,
753 events: tick_events.remove(&(tick as u64)).unwrap_or_default(),
754 });
755 }
756
757 Ok(Self {
758 sample_rate,
759 pulses,
760 total_ticks,
761 tempo,
762 track_infos,
763 modified,
764 })
765 }
766
767 fn duration(&self) -> Duration {
768 let duration: u64 = self.pulses.iter().map(|p| p.duration as u64).sum();
769 let duration = (duration as f64 / self.sample_rate as f64) * 1_000_000.0;
770
771 Duration::from_micros(duration as u64)
772 }
773}
774
775fn us_per_beat_to_bpm(uspb: u32) -> f32 {
776 60.0 / uspb as f32 * 1_000_000.0
777}
778
779#[derive(Debug, Clone)]
780struct Pulse {
781 duration: u32,
783 events: Vec<RawMidiEvent>,
784}
785
786impl Pulse {
787 fn duration_in_samples(tempo_us: u64, ppqn: u64, sample_rate: u64) -> u32 {
788 let numerator = (tempo_us * sample_rate) as f64;
789 let denominator = (ppqn * 1_000_000) as f64;
790 (numerator / denominator).round() as u32
791 }
792}
793
794#[derive(Debug, Clone, Copy)]
795struct RawMidiEvent {
796 track_index: usize,
797 channel: i32, command: i32,
799 data1: i32,
800 data2: i32,
801}
802
803impl RawMidiEvent {
804 fn from_track_event(track_index: usize, channel: u8, message: MidiMessage) -> Self {
805 let channel = i32::from(channel);
806
807 let (command, data1, data2) = match message {
808 MidiMessage::NoteOn { key, vel } => (0x90, key.as_int() as i32, vel.as_int() as i32),
809 MidiMessage::NoteOff { key, vel } => (0x80, key.as_int() as i32, vel.as_int() as i32),
810 MidiMessage::Aftertouch { key, vel } => {
811 (0xA0, key.as_int() as i32, vel.as_int() as i32)
812 }
813 MidiMessage::Controller { controller, value } => {
814 (0xB0, controller.as_int() as i32, value.as_int() as i32)
815 }
816 MidiMessage::ProgramChange { program } => (0xC0, program.as_int() as i32, 0),
817 MidiMessage::ChannelAftertouch { vel } => (0xD0, vel.as_int() as i32, 0),
818 MidiMessage::PitchBend { bend } => {
819 let midi_value = bend.as_int() as i32 + 8192;
821
822 let lsb = midi_value & 0x7F;
824 let msb = (midi_value >> 7) & 0x7F;
825
826 (0xE0, lsb, msb)
827 }
828 };
829
830 Self {
831 track_index,
832 channel,
833 command,
834 data1,
835 data2,
836 }
837 }
838}
839
840#[cfg(test)]
841mod tests {
842 use super::TrackFilter;
843
844 #[test]
845 fn mute_and_solo_rules_follow_daw_semantics() {
846 let mut filter = TrackFilter::new(3);
847
848 assert!(filter.allows(0));
849 assert!(filter.allows(1));
850 assert!(filter.allows(2));
851
852 assert!(filter.set_muted(1, true));
853 assert!(filter.allows(0));
854 assert!(!filter.allows(1));
855 assert!(filter.allows(2));
856
857 assert!(filter.set_solo(2, true));
858 assert!(!filter.allows(0));
859 assert!(!filter.allows(1));
860 assert!(filter.allows(2));
861
862 assert!(filter.set_muted(2, true));
863 assert!(!filter.allows(2));
864
865 assert!(filter.clear_solos());
866 assert!(filter.allows(0));
867 assert!(!filter.allows(1));
868 assert!(!filter.allows(2));
869 }
870
871 #[test]
872 fn out_of_bounds_track_changes_are_ignored() {
873 let mut filter = TrackFilter::new(2);
874
875 assert!(!filter.set_muted(4, true));
876 assert!(!filter.set_solo(4, true));
877 assert_eq!(filter.is_muted(4), None);
878 assert_eq!(filter.is_solo(4), None);
879 }
880}