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::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::{Format, MidiMessage, Smf, Timing},
30    timers::TimeFormatError,
31    Event as NodiEvent, MidiEvent, Moment, Sheet,
32};
33use ringbuf::{traits::*, HeapCons, HeapProd, HeapRb};
34use rustysynth::{SoundFont, Synthesizer, SynthesizerSettings};
35
36/// The player engine. This type is responsible for rendering and the playback.
37pub struct Player {
38    sheet_receiver: HeapCons<Option<Arc<MidiSheet>>>,
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    synthesizer: Synthesizer,
48    tick_clock: u32, // clock in samples within a single tick
49}
50
51impl Player {
52    /// Returns a tuple with `Self` and `PlayerController`.
53    pub fn new(
54        soundfont: &str,
55        settings: Settings,
56    ) -> Result<(Self, PlayerController), Box<dyn Error>> {
57        let sf_file = fs::read(soundfont)?;
58        let sf = SoundFont::new(&mut sf_file.as_slice())?;
59        let sf = Arc::new(sf);
60        let synthesizer = Synthesizer::new(&sf, &settings.clone().into())?;
61        let rb = HeapRb::new(1);
62        let (sheet_sender, sheet_receiver) = rb.split();
63        let rb = HeapRb::new(1);
64        let (note_off_all_sender, note_off_all_listener) = rb.split();
65        let is_playing = Arc::new(AtomicBool::new(false));
66        let position = Arc::new(AtomicUsize::new(0));
67        let sample_rate = settings.sample_rate;
68        let tempo_rate = Arc::new(AtomicF32::new(1.0));
69        let volume = Arc::new(AtomicF32::new(1.0));
70
71        Ok((
72            Self {
73                is_playing: is_playing.clone(),
74                position: position.clone(),
75                tempo_rate: tempo_rate.clone(),
76                volume: volume.clone(),
77                sheet_receiver,
78                note_off_all_listener,
79                settings,
80                synthesizer,
81                sheet: None,
82                tick_clock: 0,
83                previous_position: 0,
84            },
85            PlayerController {
86                is_playing,
87                position,
88                tempo_rate,
89                volume,
90                sheet_length: Default::default(),
91                sheet: None,
92                sheet_sender,
93                note_off_all_sender,
94                cache: Cache::new(sample_rate),
95            },
96        ))
97    }
98
99    /// Get the settings.
100    pub fn settings(&self) -> &Settings {
101        &self.settings
102    }
103
104    /// The render function which is supposed to be used within the audio loop.
105    pub fn render(&mut self, left: &mut [f32], right: &mut [f32]) {
106        if left.len() != right.len() {
107            panic!("left and right channel buffer size cannot be different");
108        }
109
110        if let Some(should_note_off) = self.note_off_all_listener.try_pop() {
111            if should_note_off {
112                self.synthesizer.note_off_all(false);
113                self.synthesizer.render(left, right);
114                return;
115            }
116        }
117
118        if !self.is_playing.load(Ordering::Relaxed) {
119            self.synthesizer.render(left, right);
120            return;
121        }
122
123        if let Some(sheet) = self.sheet_receiver.try_pop() {
124            self.sheet = sheet;
125        }
126
127        if let Some(sheet) = &self.sheet {
128            self.synthesizer
129                .set_master_volume(self.volume.load(Ordering::Relaxed));
130            for _ in 0..left.len() {
131                let position = self.position.load(Ordering::Relaxed);
132                if position == sheet.pulses.len() {
133                    self.is_playing.store(false, Ordering::Relaxed);
134                    self.synthesizer.note_off_all(false);
135                    return;
136                }
137
138                // in case the position has been changed by the controller we reset the clock
139                if position != self.previous_position {
140                    self.tick_clock = 0;
141                    self.previous_position = position;
142                }
143
144                let pulse = &sheet.pulses[position];
145
146                if self.tick_clock == 0 {
147                    for event in &pulse.events {
148                        self.synthesizer.process_midi_message(
149                            event.channel,
150                            event.command,
151                            event.data1,
152                            event.data2,
153                        );
154                    }
155                }
156
157                self.tick_clock += 1;
158
159                let pulse_duration = (pulse.duration as f32
160                    * self.tempo_rate.load(Ordering::Relaxed))
161                .round() as u32;
162
163                if self.tick_clock == pulse_duration && position < sheet.pulses.len() {
164                    self.position.store(position + 1, Ordering::Relaxed);
165                }
166            }
167
168            self.synthesizer.render(left, right);
169        }
170    }
171}
172
173/// This type allows you to control the player from one thread, while rendering it on another.
174pub struct PlayerController {
175    is_playing: Arc<AtomicBool>,
176    position: Arc<AtomicUsize>,
177    tempo_rate: Arc<AtomicF32>,
178    volume: Arc<AtomicF32>,
179    /// The sheet length in timeline ticks.
180    sheet_length: Arc<AtomicUsize>,
181    sheet: Option<Arc<MidiSheet>>,
182    sheet_sender: HeapProd<Option<Arc<MidiSheet>>>,
183    note_off_all_sender: HeapProd<bool>,
184    cache: Cache,
185}
186
187impl PlayerController {
188    /// Start the playback.
189    ///
190    /// Returns `true` if started or already playing; `false` otherwise.
191    pub fn play(&self) -> bool {
192        if self.sheet.is_none() {
193            self.is_playing.store(false, Ordering::SeqCst);
194            return false;
195        }
196
197        let position = self.position();
198
199        if self.is_playing() && position < 1.0 {
200            return true;
201        }
202
203        if position == 1.0 {
204            self.is_playing.store(false, Ordering::Relaxed);
205            return false;
206        }
207
208        self.is_playing.store(true, Ordering::Relaxed);
209
210        true
211    }
212
213    ///
214    pub fn is_playing(&self) -> bool {
215        self.is_playing.load(Ordering::SeqCst)
216    }
217
218    /// Stop the playback.
219    pub fn stop(&mut self) {
220        if self.is_playing() {
221            self.is_playing.store(false, Ordering::SeqCst);
222            self.note_off_all();
223        }
224    }
225
226    /// Set the playing position in timeline ticks.
227    ///
228    /// Values outside the valid range are clamped to `[0, total_ticks()]`. Will take effect only if
229    /// a file is opened and it is not empty.
230    pub fn set_position_ticks(&self, value: u64) {
231        let length = self.sheet_length.load(Ordering::Relaxed);
232        if length == 0 {
233            return;
234        }
235
236        let tick = value.min(length as u64) as usize;
237        self.position.store(tick, Ordering::Relaxed);
238    }
239
240    /// Get the current playback position in timeline ticks.
241    pub fn position_ticks(&self) -> u64 {
242        self.position.load(Ordering::Relaxed) as u64
243    }
244
245    /// Get the total number of timeline ticks in the loaded file.
246    pub fn total_ticks(&self) -> u64 {
247        self.sheet_length.load(Ordering::Relaxed) as u64
248    }
249
250    /// Set the playing position in normalized range `[0.0, 1.0]`.
251    ///
252    /// Will take effect only if a file is opened and it is not empty.
253    pub fn set_position(&self, value: f64) {
254        let total_ticks = self.total_ticks();
255        if total_ticks == 0 {
256            return;
257        }
258
259        let position = value.max(0.0).min(1.0);
260        let tick = (total_ticks as f64 * position) as u64;
261        self.set_position_ticks(tick);
262    }
263
264    /// Get normalized playing position (i.e. in range [0.0, 1.0]).
265    pub fn position(&self) -> f64 {
266        let total_ticks = self.total_ticks();
267        if total_ticks == 0 {
268            return 0.0;
269        }
270
271        let position = self.position_ticks() as f64;
272        (position / total_ticks as f64).max(0.0).min(1.0)
273    }
274
275    /// Initialize a new [`PositionObserver`].
276    pub fn new_position_observer(&self) -> PositionObserver {
277        PositionObserver {
278            position: self.position.clone(),
279            length: self.sheet_length.clone(),
280        }
281    }
282
283    /// Set the tempo (in beats per minute).
284    pub fn set_tempo(&mut self, tempo: f32) {
285        if let Some(sheet) = &mut self.sheet {
286            self.tempo_rate
287                .store(sheet.tempo / tempo, Ordering::Relaxed);
288        }
289    }
290
291    /// Get the tempo (in beats per minute).
292    ///
293    /// Returns `None` if file is not set.
294    pub fn tempo(&self) -> Option<f32> {
295        self.sheet
296            .as_ref()
297            .map(|s| s.tempo / self.tempo_rate.load(Ordering::SeqCst))
298    }
299
300    /// Set master volume.
301    pub fn set_volume(&mut self, volume: f32) {
302        self.volume.store(volume.max(0.0), Ordering::Relaxed);
303    }
304
305    /// Get master volume.
306    pub fn volume(&self) -> f32 {
307        self.volume.load(Ordering::Relaxed)
308    }
309
310    /// Get file duration.
311    pub fn duration(&self) -> Duration {
312        self.sheet
313            .as_ref()
314            .map(|s| s.duration())
315            .unwrap_or_default()
316    }
317
318    /// Set a MIDI file.
319    ///
320    /// The parameter is `Option<&str>`, where `Some` value is actual path and `None` is for
321    /// offloading.
322    pub fn set_file(&mut self, path: Option<impl Into<PathBuf>>) -> Result<(), Box<dyn Error>> {
323        match path {
324            Some(path) => self.open_file(path),
325            None => {
326                self.offload_file();
327                Ok(())
328            }
329        }
330    }
331
332    fn offload_file(&mut self) {
333        self.stop();
334        self.sheet_length.store(0, Ordering::SeqCst);
335        self.sheet_sender
336            .try_push(None)
337            .expect("ringbuf producer must be big enough to handle new files");
338        self.sheet = None;
339        self.tempo_rate.store(1.0, Ordering::Relaxed);
340        self.set_position(0.0);
341    }
342
343    fn open_file(&mut self, path: impl Into<PathBuf>) -> Result<(), Box<dyn Error>> {
344        self.stop();
345        let sheet = self.cache.open(&path.into())?;
346        self.sheet_length.store(sheet.total_ticks, Ordering::SeqCst);
347        self.sheet_sender
348            .try_push(Some(sheet.clone()))
349            .expect("ringbuf producer must be big enough to handle new files");
350        self.sheet = Some(sheet);
351        self.tempo_rate.store(1.0, Ordering::Relaxed);
352        self.set_position(0.0);
353
354        Ok(())
355    }
356
357    /// Send note off message for all notes (aka Panic)
358    pub fn note_off_all(&mut self) {
359        self.note_off_all_sender
360            .try_push(true)
361            .expect("ringbuf must be big enough for sending note off all message");
362    }
363}
364
365struct Cache {
366    sample_rate: u32,
367    map: HashMap<PathBuf, Arc<MidiSheet>>,
368}
369
370impl Cache {
371    fn new(sample_rate: u32) -> Self {
372        Self {
373            sample_rate,
374            map: HashMap::new(),
375        }
376    }
377
378    fn open(&mut self, path: &PathBuf) -> Result<Arc<MidiSheet>, Box<dyn Error>> {
379        let file = File::open(path)?;
380
381        match self.map.get(path) {
382            Some(s) => {
383                if file.metadata()?.modified()? == s.modified {
384                    Ok(s.clone())
385                } else {
386                    self.upsert(path, file)
387                }
388            }
389
390            None => self.upsert(path, file),
391        }
392    }
393
394    fn upsert(&mut self, path: &PathBuf, file: File) -> Result<Arc<MidiSheet>, Box<dyn Error>> {
395        let sheet = Arc::new(MidiSheet::new(file, self.sample_rate)?);
396        self.map.insert(path.clone(), sheet.clone());
397
398        Ok(sheet)
399    }
400}
401
402/// This type can be used to watch the playback position and update the GUI.
403#[derive(Debug, Clone)]
404pub struct PositionObserver {
405    position: Arc<AtomicUsize>,
406    length: Arc<AtomicUsize>,
407}
408
409impl PositionObserver {
410    /// Get the normalized playback position in range `[0.0, 1.0]`.
411    pub fn get(&self) -> f32 {
412        let length = self.length.load(Ordering::Relaxed);
413        if length == 0 {
414            return 0.0;
415        }
416
417        self.position.load(Ordering::Relaxed) as f32 / length as f32
418    }
419
420    /// Get the current playback position in timeline ticks.
421    pub fn ticks(&self) -> u64 {
422        self.position.load(Ordering::Relaxed) as u64
423    }
424
425    /// Get the total number of timeline ticks in the loaded file.
426    pub fn total_ticks(&self) -> u64 {
427        self.length.load(Ordering::Relaxed) as u64
428    }
429}
430
431/// The player settings.
432#[allow(missing_docs)]
433#[derive(Builder, Clone, Debug)]
434pub struct Settings {
435    #[builder(default = 44100)]
436    pub sample_rate: u32,
437    #[builder(default = 64)]
438    pub block_size: u32,
439    #[builder(default = 512)]
440    pub audio_buffer_size: u32,
441    #[builder(default = 64)]
442    pub max_polyphony: u8,
443    #[builder(default = true)]
444    pub enable_effects: bool,
445}
446
447impl From<Settings> for SynthesizerSettings {
448    fn from(settings: Settings) -> Self {
449        // SynthesizerSettings is a non-exhaustive struct, so struct expressions not allowed
450        let mut result = SynthesizerSettings::new(settings.sample_rate as i32);
451        result.block_size = settings.block_size as usize;
452        result.maximum_polyphony = settings.max_polyphony as usize;
453        result.enable_reverb_and_chorus = settings.enable_effects;
454
455        result
456    }
457}
458
459#[derive(Debug, Clone)]
460struct MidiSheet {
461    sample_rate: u32,
462    tempo: f32,
463    // One pulse per MIDI tick (nodi::Moment).
464    pulses: Vec<Pulse>,
465    total_ticks: usize,
466    modified: SystemTime,
467}
468
469impl MidiSheet {
470    fn new(mut file: File, sample_rate: u32) -> Result<Self, Box<dyn Error>> {
471        let modified = file.metadata()?.modified()?;
472        let mut buf = Vec::new();
473        file.read_to_end(&mut buf)?;
474        let Smf { header, tracks } = Smf::parse(&buf)?;
475        let ppqn = match header.timing {
476            Timing::Metrical(n) => u16::from(n),
477            _ => return Err(TimeFormatError.into()),
478        };
479
480        let sheet = match header.format {
481            Format::SingleTrack | Format::Sequential => Sheet::sequential(&tracks),
482            Format::Parallel => Sheet::parallel(&tracks),
483        };
484        let total_ticks = sheet.len();
485
486        let mut duration = Pulse::duration_in_samples(500_000, ppqn as u64, sample_rate as u64);
487        let tempo = sheet
488            .iter()
489            .flat_map(|m| &m.events)
490            .find(|v| matches!(v, NodiEvent::Tempo(_)))
491            .map(|v| match v {
492                NodiEvent::Tempo(tempo) => us_per_beat_to_bpm(*tempo),
493                _ => unreachable!(),
494            })
495            .unwrap_or(120.0f32);
496
497        let pulses: Vec<Pulse> = sheet
498            .iter()
499            .map(|moment| Pulse::from_moment(moment, &mut duration, ppqn, sample_rate))
500            .collect();
501        debug_assert_eq!(pulses.len(), total_ticks);
502
503        Ok(Self {
504            sample_rate,
505            pulses,
506            total_ticks,
507            tempo,
508            modified,
509        })
510    }
511
512    fn duration(&self) -> Duration {
513        let duration: u64 = self.pulses.iter().map(|p| p.duration as u64).sum();
514        let duration = (duration as f64 / self.sample_rate as f64) * 1_000_000.0;
515
516        Duration::from_micros(duration as u64)
517    }
518}
519
520fn us_per_beat_to_bpm(uspb: u32) -> f32 {
521    60.0 / uspb as f32 * 1_000_000.0
522}
523
524#[derive(Debug, Clone)]
525struct Pulse {
526    // duration is in samples
527    duration: u32,
528    events: Vec<RawMidiEvent>,
529}
530
531impl Pulse {
532    // if moment contains tempo change, new duration is calculated and set, otherwise
533    // `initial_duration` is set as duration
534    fn from_moment(moment: &Moment, duration: &mut u32, ppqn: u16, sample_rate: u32) -> Self {
535        moment.events.iter().fold(
536            Pulse {
537                // we define default tempo to 120 BPM (or 500_000 us per beat)
538                duration: *duration,
539                events: vec![],
540            },
541            |mut result, event| {
542                match event {
543                    NodiEvent::Midi(event) => result.events.push((*event).into()),
544                    NodiEvent::Tempo(tempo) => {
545                        *duration = Self::duration_in_samples(
546                            *tempo as u64,
547                            ppqn as u64,
548                            sample_rate as u64,
549                        );
550                        result.duration = *duration;
551                    }
552                    _ => (),
553                }
554                result
555            },
556        )
557    }
558
559    fn duration_in_samples(tempo_us: u64, ppqn: u64, sample_rate: u64) -> u32 {
560        let numerator = (tempo_us * sample_rate) as f64;
561        let denominator = (ppqn * 1_000_000) as f64;
562        (numerator / denominator).round() as u32
563    }
564}
565
566#[derive(Debug, Clone, Copy)]
567struct RawMidiEvent {
568    channel: i32, // it's i32 for compatibility with rustysynth
569    command: i32,
570    data1: i32,
571    data2: i32,
572}
573
574impl From<MidiEvent> for RawMidiEvent {
575    fn from(event: MidiEvent) -> Self {
576        let channel = event.channel.as_int() as i32;
577
578        let (command, data1, data2) = match event.message {
579            MidiMessage::NoteOn { key, vel } => (0x90, key.as_int() as i32, vel.as_int() as i32),
580            MidiMessage::NoteOff { key, vel } => (0x80, key.as_int() as i32, vel.as_int() as i32),
581            MidiMessage::Aftertouch { key, vel } => {
582                (0xA0, key.as_int() as i32, vel.as_int() as i32)
583            }
584            MidiMessage::Controller { controller, value } => {
585                (0xB0, controller.as_int() as i32, value.as_int() as i32)
586            }
587            MidiMessage::ProgramChange { program } => (0xC0, program.as_int() as i32, 0),
588            MidiMessage::ChannelAftertouch { vel } => (0xD0, vel.as_int() as i32, 0),
589            MidiMessage::PitchBend { bend } => {
590                // Adjust the bend value from [-8192, +8191] to [0, 16383]
591                let midi_value = bend.as_int() as i32 + 8192;
592
593                // Extract LSB and MSB data bytes
594                let lsb = midi_value & 0x7F;
595                let msb = (midi_value >> 7) & 0x7F;
596
597                (0xE0, lsb, msb)
598            }
599        };
600
601        Self {
602            channel,
603            command,
604            data1,
605            data2,
606        }
607    }
608}