mseq 3.0.0

Framework for building MIDI sequencers, with clock and transport synchronization.
Documentation
use log::{debug, trace};
use midir::{Ignore, MidiInput, MidiOutput};
use mseq_core::{InputQueue, MidiMessage, MidiOut};
use promptly::{ReadlineError, prompt_default};
use std::sync::{Arc, Condvar, Mutex};
use thiserror::Error;

const CLOCK: u8 = 0xf8;
const START: u8 = 0xfa;
const CONTINUE: u8 = 0xfb;
const STOP: u8 = 0xfc;
const NOTE_ON: u8 = 0x90;
const NOTE_OFF: u8 = 0x80;
const CC: u8 = 0xB0;
const PC: u8 = 0xC0;
const PITCH_BEND: u8 = 0xE0;

#[derive(Error, Debug)]
pub enum MidiError {
    #[error("Init error: {0}")]
    Init(#[from] midir::InitError),
    #[error("Connect error: {0}")]
    OutConnect(#[from] midir::ConnectError<MidiOutput>),
    #[error("Connect error: {0}")]
    InConnect(#[from] midir::ConnectError<MidiInput>),
    #[error("Send error: {0}")]
    Send(#[from] midir::SendError),
    #[error("Read line [{}: {}]", file!(), line!())]
    ReadLine(#[from] ReadlineError),
    #[error("Invalid port number selected")]
    PortNumber(),
    #[error("No midi output found")]
    NoOutput(),
    #[error("No midi input found")]
    NoInput(),
}

pub struct StdMidiOut(midir::MidiOutputConnection);

impl StdMidiOut {
    pub(crate) fn new(port: Option<u32>) -> Result<Self, MidiError> {
        let midi_out = MidiOutput::new("out")?;
        let out_ports = midi_out.ports();

        let out_port = if let Some(p) = port {
            match out_ports.get(p as usize) {
                None => return Err(MidiError::PortNumber()),
                Some(x) => x,
            }
        } else {
            match out_ports.len() {
                0 => return Err(MidiError::NoOutput()),
                1 => {
                    println!(
                        "Choosing the only available output port: {}",
                        midi_out.port_name(&out_ports[0]).unwrap()
                    );
                    &out_ports[0]
                }
                _ => {
                    println!("\nAvailable output ports:");
                    for (i, p) in out_ports.iter().enumerate() {
                        println!("{}: {}", i, midi_out.port_name(p).unwrap());
                    }

                    let port_number: usize = prompt_default("Select output port", 0)?;
                    match out_ports.get(port_number) {
                        None => return Err(MidiError::PortNumber()),
                        Some(x) => x,
                    }
                }
            }
        };

        let conn = midi_out.connect(out_port, "output connection")?;
        Ok(Self(conn))
    }
}

impl MidiOut for StdMidiOut {
    type Error = MidiError;
    fn send_start(&mut self) -> Result<(), MidiError> {
        debug!("SendStart");
        self.0.send(&[START])?;
        Ok(())
    }

    fn send_continue(&mut self) -> Result<(), MidiError> {
        debug!("SendContinue");
        self.0.send(&[CONTINUE])?;
        Ok(())
    }

    fn send_stop(&mut self) -> Result<(), MidiError> {
        debug!("SendStop");
        self.0.send(&[STOP])?;
        Ok(())
    }

    fn send_clock(&mut self) -> Result<(), MidiError> {
        trace!("SendClock");
        self.0.send(&[CLOCK])?;
        Ok(())
    }

    fn send_note_on(&mut self, channel_id: u8, note: u8, velocity: u8) -> Result<(), MidiError> {
        debug!("NoteOn[channel_id: {channel_id}, note: {note}, v: {velocity}]",);
        self.0
            .send(&[NOTE_ON | (channel_id - 1), note & 0x7F, velocity & 0x7F])?;
        Ok(())
    }

    fn send_note_off(&mut self, channel_id: u8, note: u8) -> Result<(), MidiError> {
        debug!("NoteOff[{channel_id}, n: {note}]");
        self.0
            .send(&[NOTE_OFF | (channel_id - 1), note & 0x7F, 0])?;
        Ok(())
    }

    fn send_cc(&mut self, channel_id: u8, parameter: u8, value: u8) -> Result<(), MidiError> {
        debug!("SendCC[channel_id: {channel_id}, parameter: {parameter}, value: {value}]");
        self.0
            .send(&[CC | (channel_id - 1), parameter & 0x7F, value & 0x7F])?;
        Ok(())
    }

    fn send_pc(&mut self, channel_id: u8, value: u8) -> Result<(), MidiError> {
        debug!("SendPC[channel_id: {channel_id}, value: {value}]");
        self.0.send(&[PC | (channel_id - 1), value & 0x7F])?;
        Ok(())
    }
    fn send_pitch_bend(&mut self, channel_id: u8, value: u16) -> Result<(), Self::Error> {
        debug!("SendPitchBend[channel_id: {channel_id}, value: {value}]");
        self.0.send(&[
            PITCH_BEND | (channel_id - 1),
            (value & 0x7F) as u8,
            ((value >> 7) & 0x7F) as u8,
        ])?;
        Ok(())
    }
}

/// A MIDI message queue paired with the condvar the producer (the midir input
/// callback) notifies on every push, so a consumer can park until work arrives.
#[derive(Clone)]
pub(crate) struct NotifyQueue {
    pub queue: Arc<Mutex<InputQueue>>,
    pub condvar: Arc<Condvar>,
}

impl NotifyQueue {
    fn new() -> Self {
        Self {
            queue: Arc::new(Mutex::new(InputQueue::new())),
            condvar: Arc::new(Condvar::new()),
        }
    }

    /// Push a message and wake the waiting consumer.
    fn push(&self, message: MidiMessage) {
        self.queue.lock().unwrap().push_back(message);
        self.condvar.notify_all();
    }
}

pub(crate) struct InConnection {
    pub message: NotifyQueue,
    pub slave_system: Option<NotifyQueue>,
    _connection: midir::MidiInputConnection<(NotifyQueue, Option<NotifyQueue>)>,
}

/// MIDI input connection parameters.
#[derive(Clone)]
pub struct MidiInParam {
    /// An enum that is used to specify what kind of MIDI messages should be ignored when receiving messages.
    pub ignore: Ignore,
    /// MIDI port id used to receive the midi messages. If set to `None`, information about the MIDI ports
    /// will be displayed and the input port will be asked to the user with a prompt.
    ///
    /// When using several inputs, prefer specifying explicit port ids: with multiple inputs left to
    /// `None`, the user is prompted once per input, and if a single port is available every such input
    /// would auto-bind to that same port.
    pub port: Option<u32>,
    /// Boolean flag to select the sequencer mode.
    /// If set to `true`, the sequencer will run in **slave mode**, synchronizing to external MIDI clock and transport messages.
    /// If set to `false`, the sequencer will run in **master mode**, generating its own MIDI clock and transport messages.
    ///
    /// When several inputs set this flag, only the first one (by position) is used as the clock and
    /// transport source; the others are treated as message-only inputs and a warning is logged.
    pub slave: bool,
}

pub(crate) fn connect(
    input_id: usize,
    params: MidiInParam,
    is_slave: bool,
) -> Result<InConnection, MidiError> {
    let mut midi_in = MidiInput::new("in")?;
    midi_in.ignore(params.ignore);

    // Find port
    let in_ports = midi_in.ports();

    let in_port = if let Some(p) = params.port {
        match in_ports.get(p as usize) {
            None => return Err(MidiError::PortNumber()),
            Some(x) => x,
        }
    } else {
        match in_ports.len() {
            0 => return Err(MidiError::NoInput()),
            1 => {
                println!(
                    "Choosing the only available intput port: {}",
                    midi_in.port_name(&in_ports[0]).unwrap()
                );
                &in_ports[0]
            }
            _ => {
                println!("\nAvailable input ports:");
                for (i, p) in in_ports.iter().enumerate() {
                    println!("{}: {}", i, midi_in.port_name(p).unwrap());
                }

                let port_number: usize =
                    prompt_default(format!("Select input port for input {input_id}"), 0)?;
                match in_ports.get(port_number) {
                    None => return Err(MidiError::PortNumber()),
                    Some(x) => x,
                }
            }
        }
    };

    let message = NotifyQueue::new();
    let slave_system = if is_slave {
        Some(NotifyQueue::new())
    } else {
        None
    };

    let input = (message.clone(), slave_system.clone());

    let _connection = midi_in.connect(
        in_port,
        "midir-read-input",
        move |_, message, input| {
            let m = MidiMessage::parse(message);
            if let Some(m) = m {
                if m.is_transport() {
                    // Transport messages are only consumed from the slave clock source;
                    // for any other input they are dropped.
                    if let Some(slave) = &input.1 {
                        slave.push(m);
                    }
                } else {
                    input.0.push(m);
                }
            }
        },
        input,
    )?;

    Ok(InConnection {
        message,
        slave_system,
        _connection,
    })
}