lyrica 0.2.1

Phantasmically simple MIDI file handling
Documentation
//! Phantasmically simple MIDI file handling.
//!
//! Quoth [Merriam-Webster](https://www.merriam-webster.com/dictionary/phantasmic):
//! > Definition of phantasm
//! >
//! > 1: a product of fantasy: such as
//! >
//! >   a: delusive appearance : ILLUSION
//!
//! This crate provides the illusion of MIDI being really easy to work with
//! rather than a pain in your rear. As a tradeoff, it's fairly inflexible, and
//! only works with MIDI files.
//!
//! Here's what Lyrica can do with your MIDI files:
//! * Play
//! * Pause!
//! * Loop!!
//! * Switch to a different file!!!
//!
//! ... all without blocking the thread!
//! [Now how much do you think Lyrica is worth? ***Don't answer!***](https://www.youtube.com/watch?v=DgJS2tQPGKQ)
//!
//! ## Example
//! ```rust,ignore(filesystem-access)
//! use lyrica::{MidiFile, MidiOutput, MidiPlayer}
//! use std::{fs, thread, time::Duration};
//!
//! let midi_output = MidiOutput::new("lyrica-example")?;
//! // NOTE: it's entirely possible that there could be no ports at all (eg. on Wine).
//! // This is glossed over for the sake of exposition.
//! let port = midi_output.ports()[0];
//! let connection = midi_output.connect(&port, "lyrica-example")?;
//! let midi_bytes = fs::read("phantom_ensemble.midi")?;
//! let midi_file = MidiFile::from_bytes(&midi_bytes)?;
//! let mut player = MidiPlayer::new(connection);
//! player.set_midi_file(midi_file)?;
//!
//! while !player.is_finished() {
//!     player.update()?;
//! }
//! ```

#![warn(clippy::single_char_lifetime_names)]

mod events;
pub mod file;
pub mod player;

use midir::SendError;
use midly::{
    live::LiveEvent,
    num::{u4, u7},
    Error as MidlyError, MidiMessage,
};
use thiserror::Error;

pub use midir::{MidiOutput, MidiOutputConnection, MidiOutputPort};

pub use crate::{file::MidiFile, player::MidiPlayer};

const ALL_SOUND_OFF_CC: u7 = u7::new(123);

#[derive(Debug, Error)]
#[non_exhaustive]
pub enum LyricaError {
    #[error("couldn't send MIDI message: {0}")]
    SendFailed(#[from] SendError),
    #[error("failed to parse standard MIDI file: {0}")]
    ParsingFailed(#[source] MidlyError),
}

/// Sends an [All Sound Off](http://midi.teragonaudio.com/tech/midispec/ntnoff.htm)
/// message to all channels.
fn all_sound_off(connection: &mut MidiOutputConnection) -> Result<(), LyricaError> {
    let mut event_bytes = Vec::new();

    for i in 0..16 {
        let event = LiveEvent::Midi {
            channel: u4::new(i),
            message: MidiMessage::Controller {
                controller: ALL_SOUND_OFF_CC,
                value: u7::new(0),
            },
        };

        event
            .write_std(&mut event_bytes)
            .expect("failed to write MIDI event to buffer");

        connection.send(&event_bytes)?;
        event_bytes.clear();
    }

    Ok(())
}