Skip to main content

midi_player/
player.rs

1//! GUI-independent player implementation.
2//!
3//! It contains the player itself [`Player`] and its controller [`PlayerController`].
4//!
5//! [`Player`] is responsible for rendering, and can be moved to the audio thread to fill the audio
6//! buffers with samples.
7//!
8//! [`PlayerController`] is supposed to be shared with another thread (i.e. GUI) to control the
9//! player from.
10//!
11//! The API is straightforward. You just call [`Player::new`], which initializes a [`Player`] and
12//! [`PlayerController`]. You should run [`Player::render`] within the audio loop and you can
13//! control the player using the initialized controller.
14
15use 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
35/// The player engine. This type is responsible for rendering and the playback.
36pub 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, // clock in samples within a single tick
50}
51
52impl Player {
53    /// Returns a tuple with `Self` and `PlayerController`.
54    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    /// Get the settings.
107    pub fn settings(&self) -> &Settings {
108        &self.settings
109    }
110
111    /// The render function which is supposed to be used within the audio loop.
112    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                // in case the position has been changed by the controller we reset the clock
149                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
186/// This type allows you to control the player from one thread, while rendering it on another.
187pub struct PlayerController {
188    is_playing: Arc<AtomicBool>,
189    position: Arc<AtomicUsize>,
190    tempo_rate: Arc<AtomicF32>,
191    volume: Arc<AtomicF32>,
192    /// The sheet length in timeline ticks.
193    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    /// Start the playback.
204    ///
205    /// Returns `true` if started or already playing; `false` otherwise.
206    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    /// Returns `true` when playback is active.
229    pub fn is_playing(&self) -> bool {
230        self.is_playing.load(Ordering::SeqCst)
231    }
232
233    /// Stop the playback.
234    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    /// Set the playing position in timeline ticks.
242    ///
243    /// Values outside the valid range are clamped to `[0, total_ticks()]`. Will take effect only if
244    /// a file is opened and it is not empty.
245    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    /// Get the current playback position in timeline ticks.
256    pub fn position_ticks(&self) -> u64 {
257        self.position.load(Ordering::Relaxed) as u64
258    }
259
260    /// Get the total number of timeline ticks in the loaded file.
261    pub fn total_ticks(&self) -> u64 {
262        self.sheet_length.load(Ordering::Relaxed) as u64
263    }
264
265    /// Set the playing position in normalized range `[0.0, 1.0]`.
266    ///
267    /// Will take effect only if a file is opened and it is not empty.
268    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    /// Get normalized playing position (i.e. in range [0.0, 1.0]).
280    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    /// Initialize a new [`PositionObserver`].
291    pub fn new_position_observer(&self) -> PositionObserver {
292        PositionObserver {
293            position: self.position.clone(),
294            length: self.sheet_length.clone(),
295        }
296    }
297
298    /// Set the tempo (in beats per minute).
299    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    /// Get the tempo (in beats per minute).
307    ///
308    /// Returns `None` if file is not set.
309    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    /// Set master volume.
316    pub fn set_volume(&mut self, volume: f32) {
317        self.volume.store(volume.max(0.0), Ordering::Relaxed);
318    }
319
320    /// Get master volume.
321    pub fn volume(&self) -> f32 {
322        self.volume.load(Ordering::Relaxed)
323    }
324
325    /// Get file duration.
326    pub fn duration(&self) -> Duration {
327        self.sheet
328            .as_ref()
329            .map(|s| s.duration())
330            .unwrap_or_default()
331    }
332
333    /// Returns metadata for all tracks in the loaded file.
334    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    /// Returns the number of tracks in the loaded file.
342    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    /// Mute or unmute a track by its index.
350    ///
351    /// Returns `true` if the state changed.
352    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    /// Returns whether a track is muted.
362    ///
363    /// Returns `None` if `track_index` is out of bounds.
364    pub fn is_track_muted(&self, track_index: usize) -> Option<bool> {
365        self.track_filter.is_muted(track_index)
366    }
367
368    /// Clears mute state for all tracks.
369    ///
370    /// Returns `true` if any state changed.
371    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    /// Solo or unsolo a track by its index.
381    ///
382    /// Returns `true` if the state changed.
383    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    /// Returns whether a track is soloed.
393    ///
394    /// Returns `None` if `track_index` is out of bounds.
395    pub fn is_track_solo(&self, track_index: usize) -> Option<bool> {
396        self.track_filter.is_solo(track_index)
397    }
398
399    /// Clears solo state for all tracks.
400    ///
401    /// Returns `true` if any state changed.
402    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    /// Set a MIDI file.
412    ///
413    /// The parameter is `Option<&str>`, where `Some` value is actual path and `None` is for
414    /// offloading.
415    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    /// Send note off message for all notes (aka Panic)
455    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/// This type can be used to watch the playback position and update the GUI.
591#[derive(Debug, Clone)]
592pub struct PositionObserver {
593    position: Arc<AtomicUsize>,
594    length: Arc<AtomicUsize>,
595}
596
597impl PositionObserver {
598    /// Get the normalized playback position in range `[0.0, 1.0]`.
599    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    /// Get the current playback position in timeline ticks.
609    pub fn ticks(&self) -> u64 {
610        self.position.load(Ordering::Relaxed) as u64
611    }
612
613    /// Get the total number of timeline ticks in the loaded file.
614    pub fn total_ticks(&self) -> u64 {
615        self.length.load(Ordering::Relaxed) as u64
616    }
617}
618
619/// The player settings.
620#[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        // SynthesizerSettings is a non-exhaustive struct, so struct expressions not allowed
638        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/// MIDI track metadata extracted from the loaded file.
648#[derive(Debug, Clone, PartialEq, Eq)]
649pub struct MidiTrackInfo {
650    /// Zero-based track index.
651    pub index: usize,
652    /// Optional track name from MIDI meta events.
653    pub name: Option<String>,
654    /// Distinct MIDI channels used by this track (1-based, range `1..=16`).
655    pub channels: Vec<u8>,
656    /// Distinct MIDI program numbers used by this track.
657    pub programs: Vec<u8>,
658}
659
660#[derive(Debug, Clone)]
661struct MidiSheet {
662    sample_rate: u32,
663    tempo: f32,
664    // One pulse per MIDI tick (nodi::Moment).
665    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 is in samples
782    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, // it's i32 for compatibility with rustysynth
798    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                // Adjust the bend value from [-8192, +8191] to [0, 16383]
820                let midi_value = bend.as_int() as i32 + 8192;
821
822                // Extract LSB and MSB data bytes
823                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}