Skip to content

Commit

Permalink
WIP: MIDI: player
Browse files Browse the repository at this point in the history
  • Loading branch information
tsionyx committed Feb 22, 2024
1 parent 5942e23 commit 7ccad76
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 14 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ jobs:
with:
submodules: recursive

- name: Install deps
run: sudo apt-get -y install libasound2-dev

- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
Expand Down Expand Up @@ -107,6 +110,9 @@ jobs:
with:
submodules: recursive

- name: Install deps
run: sudo apt-get -y install libasound2-dev

- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ itertools = "0.12"

# MIDI stuff
midly = "0.5"
midir = "0.9"
102 changes: 88 additions & 14 deletions src/io/midi/codec.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
use std::{borrow::Cow, collections::HashMap, iter, path::Path};
use std::{
borrow::Cow,
collections::HashMap,
iter,
path::Path,
thread::sleep,
time::{Duration, Instant},
};

use itertools::Itertools as _;
use midly::{
num::u15, Format, Header, MetaMessage, MidiMessage, Smf, Timing, Track, TrackEvent,
num::u15, Format, Fps, Header, MetaMessage, MidiMessage, Smf, Timing, Track, TrackEvent,
TrackEventKind,
};

Expand All @@ -14,7 +21,7 @@ use crate::{
},
};

use super::{Channel, UserPatchMap};
use super::{transport::get_default_connection, Channel, UserPatchMap};

fn into_relative_time(track: AbsTimeTrack) -> Track<'static> {
track
Expand Down Expand Up @@ -166,18 +173,85 @@ impl Performance {
Ok(())
}

pub fn play() -> Result<(), AnyError> {
// TODO:
// 1. join all tracks into a single track with absolute time:
// a. convert to absolute time (remove TrackEnd)
// b. merge (https://hackage.haskell.org/package/HCodecs-0.5.2/docs/src/Codec.Midi.html#merge)
// d. add TrackEnd
// 2. convert absolute time into Instant points using the predefined TimeDiv
// https://hackage.haskell.org/package/HCodecs-0.5.2/docs/src/Codec.Midi.html#toRealTime
// 3. run a thread by sleeping in a loop until the new event is received
// https://github.com/insomnimus/nodi/blob/main/src/player.rs#L40
// 4. take care of stopping all notes on Ctrl-C:
pub fn play(self) -> Result<(), AnyError> {
// TODO: allow pause (see https://github.com/insomnimus/nodi/blob/main/src/player.rs)
let mut conn = get_default_connection()?;
let Smf { header, tracks } = self.into_midi(None)?;
let single_track = merge_tracks(tracks);
let sec_per_tick = tick_size(header.timing);
let real_time: AbsTimeTrack<Duration> = single_track
.into_iter()
.map(|(ticks, msg)| (ticks * sec_per_tick, msg))
.collect();

let start = Instant::now();
for (t, msg) in real_time {
let elapsed = start.elapsed();
if elapsed >= t {
if let Some(live) = msg.as_live_event() {
live.write_std(&mut conn)?;
}
} else {
sleep(sec_per_tick);
}
}
// TODO: take care of stopping all notes on Ctrl-C:
// https://github.com/insomnimus/nodi/blob/main/src/player.rs#L91
Ok(())
}
}

fn tick_size(timing: Timing) -> Duration {
let ticks_per_second = match timing {
Timing::Metrical(tick) => u32::from(u16::from(tick)) * BEATS_PER_SECOND,
Timing::Timecode(fps, sub) => {
let fps: u32 = match fps {
Fps::Fps24 => 24,
Fps::Fps25 => 25,
Fps::Fps29 => 29,
Fps::Fps30 => 30,
};
fps * u32::from(sub)
}
};

Duration::from_secs_f64(f64::from(ticks_per_second).recip())
}

fn to_absolute(track: Track, drop_track_end: bool) -> AbsTimeTrack {
track
.into_iter()
.filter(|t| !(drop_track_end && t.kind == TrackEventKind::Meta(MetaMessage::EndOfTrack)))
.scan(0, |acc, t| {
let abs_time = u32::from(t.delta) + *acc;
*acc = abs_time;
Some((abs_time, t.kind))
})
.collect()
}

/// Join all tracks into a single track with absolute time:
/// - convert to absolute time (remove TrackEnd)
/// - merge (https://hackage.haskell.org/package/HCodecs-0.5.2/docs/src/Codec.Midi.html#merge)
/// - add TrackEnd
fn merge_tracks(mut tracks: Vec<Track>) -> AbsTimeTrack {
if tracks.is_empty() {
return AbsTimeTrack::new();
}
let first = tracks.remove(0);
let first = to_absolute(first, true);

let mut single = tracks
.into_iter()
.map(|t| to_absolute(t, true))
.fold(first, |acc, track| {
acc.into_iter()
.merge_by(track, |(t1, _), (t2, _)| t1 < t2)
.collect()
});

if let Some((last, _)) = single.last() {
single.push((*last, TrackEventKind::Meta(MetaMessage::EndOfTrack)))
}
single
}
1 change: 1 addition & 0 deletions src/io/midi/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod codec;
mod transport;

use std::collections::HashMap;

Expand Down
52 changes: 52 additions & 0 deletions src/io/midi/transport.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use std::io::{ErrorKind, Write};

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

pub(super) fn get_default_port(out: &MidiOutput) -> Option<MidiOutputPort> {
let ports = out.ports();
if ports.is_empty() {
return None;
}

if ports.len() == 1 {
ports.into_iter().next()
} else {
let mut without_midi_through = ports.iter().filter(|p| {
out.port_name(p)
.map_or(false, |name| !name.contains("Midi Through"))
});

without_midi_through
.next()
.cloned()
.or_else(|| ports.into_iter().next())
}
}

type AnyError = Box<dyn std::error::Error>;

pub fn get_default_connection() -> Result<Connection, AnyError> {
let out = MidiOutput::new("musik output checker")?;
let port = get_default_port(&out).ok_or("Not found any MIDI output device")?;

let conn = out.connect(&port, "playing through musik")?;
Ok(Connection { inner: conn })
}

pub struct Connection {
inner: MidiOutputConnection,
}

impl Write for Connection {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let size = buf.len();
self.inner
.send(buf)
.map_err(|err| std::io::Error::new(ErrorKind::InvalidData, err))?;
Ok(size)
}

fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}

0 comments on commit 7ccad76

Please sign in to comment.