bbx_midi/
stream.rs

1//! Real-time MIDI input streaming via midir.
2
3use std::{
4    error::Error,
5    io::{Write, stdin, stdout},
6    sync::{mpsc, mpsc::Sender},
7    thread,
8    thread::JoinHandle,
9};
10
11use midir::{Ignore, MidiInput, MidiInputPort};
12
13use crate::message::{MidiMessage, MidiMessageStatus};
14
15/// A real-time MIDI input stream with message filtering.
16///
17/// Connects to a MIDI input port and forwards matching messages
18/// to a callback function via a channel.
19pub struct MidiInputStream {
20    tx: Sender<MidiMessage>,
21    filters: Vec<MidiMessageStatus>,
22}
23
24impl MidiInputStream {
25    /// Create a new MIDI input stream with optional status filters.
26    ///
27    /// # Arguments
28    ///
29    /// * `filters` - Message types to accept (empty = all messages)
30    /// * `message_handler` - Callback invoked for each matching message
31    pub fn new(filters: Vec<MidiMessageStatus>, message_handler: fn(MidiMessage) -> ()) -> Self {
32        let (tx, rx) = mpsc::channel::<MidiMessage>();
33        thread::spawn(move || {
34            loop {
35                message_handler(rx.recv().unwrap());
36            }
37        });
38        MidiInputStream { tx, filters }
39    }
40}
41
42impl MidiInputStream {
43    /// Initialize and start the MIDI input stream.
44    ///
45    /// Prompts the user to select a MIDI port if multiple are available.
46    /// Returns a handle to the spawned thread.
47    pub fn init(self) -> JoinHandle<()> {
48        println!("Creating new MIDI input stream");
49        let mut midi_in = MidiInput::new("Reading MIDI input").unwrap();
50        midi_in.ignore(Ignore::None);
51
52        let in_ports = midi_in.ports();
53        let in_port: Option<MidiInputPort> = match in_ports.len() {
54            0 => None,
55            1 => {
56                println!(
57                    "Choosing the only available MIDI input port:\n{}",
58                    midi_in.port_name(&in_ports[0]).unwrap()
59                );
60                Some(in_ports[0].clone())
61            }
62            _ => {
63                println!("\nAvailable MIDI input ports:");
64                for (idx, port) in in_ports.iter().enumerate() {
65                    println!("{}: {}", idx, midi_in.port_name(port).unwrap());
66                }
67                println!("\nPlease select input port: ");
68                stdout().flush().unwrap();
69
70                let mut input = String::new();
71                stdin().read_line(&mut input).unwrap();
72                Some(
73                    in_ports
74                        .get(input.trim().parse::<usize>().unwrap())
75                        .ok_or("Invalid input port selected")
76                        .unwrap()
77                        .clone(),
78                )
79            }
80        };
81        thread::spawn(move || match self.create_midi_input_stream(midi_in, in_port.unwrap()) {
82            Ok(_) => (),
83            Err(err) => println!("Error : {err}"),
84        })
85    }
86
87    fn create_midi_input_stream(
88        self,
89        midi_in: MidiInput,
90        in_port: MidiInputPort,
91    ) -> std::result::Result<(), Box<dyn Error>> {
92        println!("\nOpening MIDI input stream for port");
93        let in_port_name = midi_in.port_name(&in_port)?;
94        let _connection = midi_in.connect(
95            &in_port,
96            "midir-read-input",
97            move |_stamp, message_bytes, _| {
98                let message = MidiMessage::from(message_bytes);
99                if self.is_passed_through_filters(&message) {
100                    self.tx.send(message).unwrap();
101                } else {
102                    // Message was "filtered" - do nothing
103                }
104            },
105            (),
106        )?;
107
108        println!("Connection open, reading MIDI input from '{in_port_name}' (press enter to exit) ...");
109
110        let mut input = String::new();
111        input.clear();
112        stdin().read_line(&mut input).unwrap();
113        println!("Closing connection");
114
115        Ok(())
116    }
117
118    fn is_passed_through_filters(&self, message: &MidiMessage) -> bool {
119        if self.filters.is_empty() {
120            true
121        } else {
122            self.filters.contains(&message.get_status())
123        }
124    }
125}