Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ A suite of tools used to read, modify, and manage MIDI-related systems

## Overview

`midix` provides users with human readable MIDI structures without invariant states. That is, the midi 1.0 specification has been strongly typed such that programatic commands built with this crate are not invariant.
`midix` provides users with human readable MIDI structures without invariant states. That is, the midi 1.0 specification has been strongly typed such that programatic commands built with this crate uphold invariants.

`midix` provides a parser ([`Reader`](crate::prelude::Reader)) to read events from `.mid` files.
calling [`Reader::read_event`](crate::prelude::Reader::read_event) will yield a [`FileEvent`](crate::prelude::FileEvent).
calling [`Reader::read_event`](crate::prelude::Reader::read_event) will yield a [`FileEvent`](crate::file::builder::event::FileEvent).

Additionally, `midix` provides the user with [`LiveEvent::from_bytes`](crate::events::LiveEvent), which will parse events from a live MIDI source.

Expand Down
2 changes: 1 addition & 1 deletion src/byte/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ pub trait MidiWriter {
}
*/

/// Copies the nightly only feature `as_array` for [T], but specifically for Cow.
/// Copies the nightly only feature `as_array` for `[T]`, but specifically for Cow.
pub trait CowExt {
/// Reinterpret this Cow as a reference to a static array
fn as_array<const N: usize>(&self) -> Option<&[u8; N]>;
Expand Down
41 changes: 20 additions & 21 deletions src/events/live.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ pub trait FromLiveEventBytes {
Self: Sized;
}

#[doc = r"
#[doc = r#"
An emittable message to/from a streaming MIDI device.

There is currently no `StreamReader` type, so this type is most often manually constructed.
"]
This is essentially the root message of all possible messages sent by a midi device.
"#]
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "bevy", derive(bevy::reflect::Reflect))]
pub enum LiveEvent<'a> {
Expand All @@ -65,7 +65,6 @@ impl LiveEvent<'_> {
_ => None,
}
}

// /// Returns the event as a set of bytes. These bytes are to be interpreted by a MIDI live stream
// pub fn to_bytes(&self) -> Vec<u8> {
// match self {
Expand Down Expand Up @@ -120,21 +119,21 @@ impl FromLiveEventBytes for LiveEvent<'_> {
}
}

// #[test]
// fn parse_note_on() {
// use crate::prelude::*;
// let message = [0b1001_0001, 0b0100_1000, 0b001_00001];
// let parsed = LiveEvent::from_bytes(&message).unwrap();
// //parsed: ChannelVoice(ChannelVoiceMessage { channel: Channel(1), message: NoteOn { key: Key(72), vel: Velocity(33) } })
#[test]
fn parse_note_on() {
use crate::prelude::*;
let message = [0b1001_0001, 0b0100_1000, 0b001_00001];
//parsed: ChannelVoice(ChannelVoiceMessage { channel: Channel(1), message: NoteOn { key: Key(72), vel: Velocity(33) } })
let parsed = LiveEvent::from_bytes(&message).unwrap();

// assert_eq!(
// parsed,
// LiveEvent::ChannelVoice(ChannelVoiceMessage::new(
// Channel::Two,
// VoiceEvent::NoteOn {
// key: Key::from_databyte(72).unwrap(),
// velocity: Velocity::new(33).unwrap()
// }
// ))
// );
// }
assert_eq!(
parsed,
LiveEvent::ChannelVoice(ChannelVoiceMessage::new(
Channel::Two,
VoiceEvent::NoteOn {
note: Note::from_databyte(72).unwrap(),
velocity: Velocity::new(33).unwrap()
}
))
);
}
3 changes: 0 additions & 3 deletions src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,5 @@
The "root" event types for live streams and files
"#]

mod file;
pub use file::*;

mod live;
pub use live::*;
120 changes: 4 additions & 116 deletions src/file_repr/chunk/header.rs → src/file/builder/chunk/header.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::{prelude::*, reader::ReaderError};
use crate::{
file::builder::{FormatType, RawFormat},
prelude::*,
};

#[doc = r#"
The header chunk at the beginning of the file specifies some basic information about
Expand Down Expand Up @@ -108,121 +111,6 @@ impl RawHeaderChunk {
}
}

/// The header timing type.
///
/// This is either the number of ticks per quarter note or
/// the alternative SMTPE format. See the [`RawHeaderChunk`] docs for more information.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "bevy", derive(bevy::reflect::Reflect))]
pub enum Timing {
/// The midi file's delta times are defined using a tick rate per quarter note
TicksPerQuarterNote(TicksPerQuarterNote),

/// The midi file's delta times are defined using an SMPTE and MIDI Time Code
Smpte(SmpteHeader),
}

/// A representation of the `tpqn` timing for a MIDI file
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
#[cfg_attr(feature = "bevy", derive(bevy::reflect::Reflect))]
pub struct TicksPerQuarterNote {
pub(crate) inner: [u8; 2],
}
impl TicksPerQuarterNote {
/// Returns the ticks per quarter note for the file.
pub const fn ticks_per_quarter_note(&self) -> u16 {
let v = u16::from_be_bytes(self.inner);
v & 0x7FFF
}
}

/// A representation of the `smpte` timing for a MIDI file
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
#[cfg_attr(feature = "bevy", derive(bevy::reflect::Reflect))]
pub struct SmpteHeader {
pub(crate) fps: SmpteFps,
pub(crate) ticks_per_frame: DataByte,
}

impl SmpteHeader {
fn new(bytes: [u8; 2]) -> Result<Self, ParseError> {
//first byte is known to be 1 when calling this
//Bits 14 thru 8 contain one of the four values -24, -25, -29, or -30
let byte = bytes[0] as i8;

let frame = match byte {
-24 => SmpteFps::TwentyFour,
-25 => SmpteFps::TwentyFive,
-29 => {
//drop frame (29.997)
SmpteFps::TwentyNine
}
-30 => SmpteFps::Thirty,
_ => return Err(ParseError::Smpte(SmpteError::HeaderFrameTime(byte))),
};
let ticks_per_frame = DataByte::new(bytes[1])?;
Ok(Self {
fps: frame,
ticks_per_frame,
})
}

/// Returns the frames per second
pub const fn fps(&self) -> SmpteFps {
self.fps
}

/// Returns the ticks per frame
pub const fn ticks_per_frame(&self) -> u8 {
self.ticks_per_frame.0
}
}

impl Timing {
/// The tickrate per quarter note defines what a "quarter note" means.
///
/// The leading bit of the u16 is disregarded, so 1-32767
pub const fn new_ticks_per_quarter_note(tpqn: u16) -> Self {
let msb = (tpqn >> 8) as u8;
let lsb = (tpqn & 0x00FF) as u8;
Self::TicksPerQuarterNote(TicksPerQuarterNote { inner: [msb, lsb] })
}

/// Define the timing in terms of fps and ticks per frame
pub const fn new_smpte(fps: SmpteFps, ticks_per_frame: DataByte) -> Self {
Self::Smpte(SmpteHeader {
fps,
ticks_per_frame,
})
}

pub(crate) fn read<'slc, 'r, R: MidiSource<'slc>>(
reader: &'r mut Reader<R>,
) -> ReadResult<Self> {
let bytes = reader.read_exact_size()?;
match bytes[0] >> 7 {
0 => {
//this is ticks per quarter_note
Ok(Timing::TicksPerQuarterNote(TicksPerQuarterNote {
inner: bytes,
}))
}
1 => Ok(Timing::Smpte(SmpteHeader::new(bytes).map_err(|e| {
ReaderError::new(reader.buffer_position(), e.into())
})?)),
t => Err(inv_data(reader, HeaderError::InvalidTiming(t))),
}
}
/// Returns Some if the midi timing is defined
/// as ticks per quarter note
pub const fn ticks_per_quarter_note(&self) -> Option<u16> {
match self {
Self::TicksPerQuarterNote(t) => Some(t.ticks_per_quarter_note()),
_ => None,
}
}
}

#[test]
fn ensure_timing_encoding_of_tpqn() {
assert_eq!(
Expand Down
65 changes: 65 additions & 0 deletions src/file/builder/chunk/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#![doc = r#"
Contains types for MIDI file chunks

# Overview

MIDI files are organized into chunks, each identified by a 4-character ASCII type identifier
followed by a 32-bit length field and then the chunk data. The Standard MIDI File (SMF)
specification defines two chunk types, though files may contain additional proprietary chunks.

MIDI defines anything that does not fall into the standard chunk types as unknown chunks,
which can be safely ignored or processed based on application needs.

## [`RawHeaderChunk`]

The header chunk (identified by "MThd") must be the first chunk in a MIDI file. This chunk
type contains meta information about the MIDI file, such as:

- [`RawFormat`](crate::file::builder::RawFormat), which identifies how tracks should be played
(single track, simultaneous tracks, or independent tracks) and the number of tracks in the file
- [`Timing`](crate::prelude::Timing), which defines how delta-ticks (timestamps) are to be
interpreted - either as ticks per quarter note or in SMPTE time code format

The header chunk always has a fixed length of 6 bytes.

## Track Chunks

Track chunks (identified by "MTrk") contain the actual MIDI events and timing information:

- [`TrackChunkHeader`] - Contains only the length in bytes of the track data
- [`RawTrackChunk`] - Contains the complete track data which can be parsed into a sequence
of [`TrackEvent`](crate::prelude::TrackEvent)s, each with delta-time and event data

Track chunks appear after the header chunk, and the number of track chunks should match
the track count specified in the header (though this is not strictly enforced by all
MIDI software).

## [`UnknownChunk`]

Any chunk with a type identifier other than "MThd" or "MTrk" is treated as an unknown chunk.
These chunks preserve their type identifier and data, allowing applications to either:
- Ignore them (the most common approach)
- Process them if they understand the proprietary format
- Preserve them when reading and writing files to maintain compatibility

# Example Structure

A typical MIDI file structure looks like:
```text
[Header Chunk: "MThd"]
[Track Chunk 1: "MTrk"]
[Track Chunk 2: "MTrk"]
...
[Track Chunk N: "MTrk"]
[Optional Unknown Chunks]
```
"#]

mod unknown_chunk;
pub use unknown_chunk::*;

mod header;
pub use header::*;

mod track;
pub use track::*;
File renamed without changes.
10 changes: 5 additions & 5 deletions src/events/file/chunk.rs → src/file/builder/event/chunk.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use crate::prelude::*;
use crate::file::builder::chunk::{RawHeaderChunk, RawTrackChunk, UnknownChunk};

#[doc = r#"
Reads the full length of all chunk types


This is different from [`FileEvent`] such that
[`FileEvent::TrackEvent`] is not used. Instead,
This is different from [`FileEvent`](crate::file::builder::event::FileEvent) such that
[`FileEvent::TrackEvent`](crate::file::builder::event::FileEvent::TrackEvent) is not used. Instead,
the full set of bytes from the identified track are yielded.
"#]
pub enum ChunkEvent<'a> {
Expand All @@ -23,7 +23,7 @@ pub enum ChunkEvent<'a> {
/// See [`UnknownChunk`] for a breakdown on layout
Unknown(UnknownChunk<'a>),
/// End of File
EOF,
Eof,
}

impl From<RawHeaderChunk> for ChunkEvent<'_> {
Expand All @@ -48,6 +48,6 @@ impl ChunkEvent<'_> {
/// True if the event is the end of a file
#[inline]
pub const fn is_eof(&self) -> bool {
matches!(self, Self::EOF)
matches!(self, Self::Eof)
}
}
15 changes: 12 additions & 3 deletions src/events/file/mod.rs → src/file/builder/event/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
use crate::prelude::*;
#![doc = r#"
Contains events that should be yielded when parsing a midi file.

You will may utilize these types when using a [`Reader`].
"#]

use crate::{
file::builder::chunk::{RawHeaderChunk, TrackChunkHeader, UnknownChunk},
prelude::*,
};

mod chunk;
pub use chunk::*;
Expand All @@ -10,7 +19,7 @@ This type is yielded by [`Reader::read_event`] and will be consumed by a Writer

# Overview

Except [`FileEvent::EOF`] Events can be placed into two categories
Except [`FileEvent::Eof`] Events can be placed into two categories

## Chunk Events

Expand Down Expand Up @@ -64,7 +73,7 @@ pub enum FileEvent<'a> {
TrackEvent(TrackEvent<'a>),

/// Yielded when no more bytes can be read
EOF,
Eof,
}

impl From<RawHeaderChunk> for FileEvent<'_> {
Expand Down
Loading
Loading