Skip to content

Commit 091d6e9

Browse files
authored
refactor: remove file_expr module, update docs, add tests and find breaking bug (#7)
* add MidiFile::for_each_track * separate readme from docs readme * Header -> MidiFileHeader * delete `MidiFileHeader`. Timing :) * refactor: remove file_repr module These types are now private * fix: update imports * fix: docs * update doctests * document smpte * add smpte tests * found bug
1 parent a6fb1eb commit 091d6e9

41 files changed

Lines changed: 1667 additions & 599 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ A suite of tools used to read, modify, and manage MIDI-related systems
77

88
## Overview
99

10-
`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.
10+
`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.
1111

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

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

src/byte/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ pub trait MidiWriter {
301301
}
302302
*/
303303

304-
/// Copies the nightly only feature `as_array` for [T], but specifically for Cow.
304+
/// Copies the nightly only feature `as_array` for `[T]`, but specifically for Cow.
305305
pub trait CowExt {
306306
/// Reinterpret this Cow as a reference to a static array
307307
fn as_array<const N: usize>(&self) -> Option<&[u8; N]>;

src/events/live.rs

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ pub trait FromLiveEventBytes {
3838
Self: Sized;
3939
}
4040

41-
#[doc = r"
41+
#[doc = r#"
4242
An emittable message to/from a streaming MIDI device.
4343
44-
There is currently no `StreamReader` type, so this type is most often manually constructed.
45-
"]
44+
This is essentially the root message of all possible messages sent by a midi device.
45+
"#]
4646
#[derive(Clone, Debug, PartialEq, Eq)]
4747
#[cfg_attr(feature = "bevy", derive(bevy::reflect::Reflect))]
4848
pub enum LiveEvent<'a> {
@@ -65,7 +65,6 @@ impl LiveEvent<'_> {
6565
_ => None,
6666
}
6767
}
68-
6968
// /// Returns the event as a set of bytes. These bytes are to be interpreted by a MIDI live stream
7069
// pub fn to_bytes(&self) -> Vec<u8> {
7170
// match self {
@@ -120,21 +119,21 @@ impl FromLiveEventBytes for LiveEvent<'_> {
120119
}
121120
}
122121

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

130-
// assert_eq!(
131-
// parsed,
132-
// LiveEvent::ChannelVoice(ChannelVoiceMessage::new(
133-
// Channel::Two,
134-
// VoiceEvent::NoteOn {
135-
// key: Key::from_databyte(72).unwrap(),
136-
// velocity: Velocity::new(33).unwrap()
137-
// }
138-
// ))
139-
// );
140-
// }
129+
assert_eq!(
130+
parsed,
131+
LiveEvent::ChannelVoice(ChannelVoiceMessage::new(
132+
Channel::Two,
133+
VoiceEvent::NoteOn {
134+
note: Note::from_databyte(72).unwrap(),
135+
velocity: Velocity::new(33).unwrap()
136+
}
137+
))
138+
);
139+
}

src/events/mod.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,5 @@
22
The "root" event types for live streams and files
33
"#]
44

5-
mod file;
6-
pub use file::*;
7-
85
mod live;
96
pub use live::*;
Lines changed: 4 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use crate::{prelude::*, reader::ReaderError};
1+
use crate::{
2+
file::builder::{FormatType, RawFormat},
3+
prelude::*,
4+
};
25

36
#[doc = r#"
47
The header chunk at the beginning of the file specifies some basic information about
@@ -108,121 +111,6 @@ impl RawHeaderChunk {
108111
}
109112
}
110113

111-
/// The header timing type.
112-
///
113-
/// This is either the number of ticks per quarter note or
114-
/// the alternative SMTPE format. See the [`RawHeaderChunk`] docs for more information.
115-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116-
#[cfg_attr(feature = "bevy", derive(bevy::reflect::Reflect))]
117-
pub enum Timing {
118-
/// The midi file's delta times are defined using a tick rate per quarter note
119-
TicksPerQuarterNote(TicksPerQuarterNote),
120-
121-
/// The midi file's delta times are defined using an SMPTE and MIDI Time Code
122-
Smpte(SmpteHeader),
123-
}
124-
125-
/// A representation of the `tpqn` timing for a MIDI file
126-
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
127-
#[cfg_attr(feature = "bevy", derive(bevy::reflect::Reflect))]
128-
pub struct TicksPerQuarterNote {
129-
pub(crate) inner: [u8; 2],
130-
}
131-
impl TicksPerQuarterNote {
132-
/// Returns the ticks per quarter note for the file.
133-
pub const fn ticks_per_quarter_note(&self) -> u16 {
134-
let v = u16::from_be_bytes(self.inner);
135-
v & 0x7FFF
136-
}
137-
}
138-
139-
/// A representation of the `smpte` timing for a MIDI file
140-
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
141-
#[cfg_attr(feature = "bevy", derive(bevy::reflect::Reflect))]
142-
pub struct SmpteHeader {
143-
pub(crate) fps: SmpteFps,
144-
pub(crate) ticks_per_frame: DataByte,
145-
}
146-
147-
impl SmpteHeader {
148-
fn new(bytes: [u8; 2]) -> Result<Self, ParseError> {
149-
//first byte is known to be 1 when calling this
150-
//Bits 14 thru 8 contain one of the four values -24, -25, -29, or -30
151-
let byte = bytes[0] as i8;
152-
153-
let frame = match byte {
154-
-24 => SmpteFps::TwentyFour,
155-
-25 => SmpteFps::TwentyFive,
156-
-29 => {
157-
//drop frame (29.997)
158-
SmpteFps::TwentyNine
159-
}
160-
-30 => SmpteFps::Thirty,
161-
_ => return Err(ParseError::Smpte(SmpteError::HeaderFrameTime(byte))),
162-
};
163-
let ticks_per_frame = DataByte::new(bytes[1])?;
164-
Ok(Self {
165-
fps: frame,
166-
ticks_per_frame,
167-
})
168-
}
169-
170-
/// Returns the frames per second
171-
pub const fn fps(&self) -> SmpteFps {
172-
self.fps
173-
}
174-
175-
/// Returns the ticks per frame
176-
pub const fn ticks_per_frame(&self) -> u8 {
177-
self.ticks_per_frame.0
178-
}
179-
}
180-
181-
impl Timing {
182-
/// The tickrate per quarter note defines what a "quarter note" means.
183-
///
184-
/// The leading bit of the u16 is disregarded, so 1-32767
185-
pub const fn new_ticks_per_quarter_note(tpqn: u16) -> Self {
186-
let msb = (tpqn >> 8) as u8;
187-
let lsb = (tpqn & 0x00FF) as u8;
188-
Self::TicksPerQuarterNote(TicksPerQuarterNote { inner: [msb, lsb] })
189-
}
190-
191-
/// Define the timing in terms of fps and ticks per frame
192-
pub const fn new_smpte(fps: SmpteFps, ticks_per_frame: DataByte) -> Self {
193-
Self::Smpte(SmpteHeader {
194-
fps,
195-
ticks_per_frame,
196-
})
197-
}
198-
199-
pub(crate) fn read<'slc, 'r, R: MidiSource<'slc>>(
200-
reader: &'r mut Reader<R>,
201-
) -> ReadResult<Self> {
202-
let bytes = reader.read_exact_size()?;
203-
match bytes[0] >> 7 {
204-
0 => {
205-
//this is ticks per quarter_note
206-
Ok(Timing::TicksPerQuarterNote(TicksPerQuarterNote {
207-
inner: bytes,
208-
}))
209-
}
210-
1 => Ok(Timing::Smpte(SmpteHeader::new(bytes).map_err(|e| {
211-
ReaderError::new(reader.buffer_position(), e.into())
212-
})?)),
213-
t => Err(inv_data(reader, HeaderError::InvalidTiming(t))),
214-
}
215-
}
216-
/// Returns Some if the midi timing is defined
217-
/// as ticks per quarter note
218-
pub const fn ticks_per_quarter_note(&self) -> Option<u16> {
219-
match self {
220-
Self::TicksPerQuarterNote(t) => Some(t.ticks_per_quarter_note()),
221-
_ => None,
222-
}
223-
}
224-
}
225-
226114
#[test]
227115
fn ensure_timing_encoding_of_tpqn() {
228116
assert_eq!(

src/file/builder/chunk/mod.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#![doc = r#"
2+
Contains types for MIDI file chunks
3+
4+
# Overview
5+
6+
MIDI files are organized into chunks, each identified by a 4-character ASCII type identifier
7+
followed by a 32-bit length field and then the chunk data. The Standard MIDI File (SMF)
8+
specification defines two chunk types, though files may contain additional proprietary chunks.
9+
10+
MIDI defines anything that does not fall into the standard chunk types as unknown chunks,
11+
which can be safely ignored or processed based on application needs.
12+
13+
## [`RawHeaderChunk`]
14+
15+
The header chunk (identified by "MThd") must be the first chunk in a MIDI file. This chunk
16+
type contains meta information about the MIDI file, such as:
17+
18+
- [`RawFormat`](crate::file::builder::RawFormat), which identifies how tracks should be played
19+
(single track, simultaneous tracks, or independent tracks) and the number of tracks in the file
20+
- [`Timing`](crate::prelude::Timing), which defines how delta-ticks (timestamps) are to be
21+
interpreted - either as ticks per quarter note or in SMPTE time code format
22+
23+
The header chunk always has a fixed length of 6 bytes.
24+
25+
## Track Chunks
26+
27+
Track chunks (identified by "MTrk") contain the actual MIDI events and timing information:
28+
29+
- [`TrackChunkHeader`] - Contains only the length in bytes of the track data
30+
- [`RawTrackChunk`] - Contains the complete track data which can be parsed into a sequence
31+
of [`TrackEvent`](crate::prelude::TrackEvent)s, each with delta-time and event data
32+
33+
Track chunks appear after the header chunk, and the number of track chunks should match
34+
the track count specified in the header (though this is not strictly enforced by all
35+
MIDI software).
36+
37+
## [`UnknownChunk`]
38+
39+
Any chunk with a type identifier other than "MThd" or "MTrk" is treated as an unknown chunk.
40+
These chunks preserve their type identifier and data, allowing applications to either:
41+
- Ignore them (the most common approach)
42+
- Process them if they understand the proprietary format
43+
- Preserve them when reading and writing files to maintain compatibility
44+
45+
# Example Structure
46+
47+
A typical MIDI file structure looks like:
48+
```text
49+
[Header Chunk: "MThd"]
50+
[Track Chunk 1: "MTrk"]
51+
[Track Chunk 2: "MTrk"]
52+
...
53+
[Track Chunk N: "MTrk"]
54+
[Optional Unknown Chunks]
55+
```
56+
"#]
57+
58+
mod unknown_chunk;
59+
pub use unknown_chunk::*;
60+
61+
mod header;
62+
pub use header::*;
63+
64+
mod track;
65+
pub use track::*;
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
use crate::prelude::*;
1+
use crate::file::builder::chunk::{RawHeaderChunk, RawTrackChunk, UnknownChunk};
22

33
#[doc = r#"
44
Reads the full length of all chunk types
55
66
7-
This is different from [`FileEvent`] such that
8-
[`FileEvent::TrackEvent`] is not used. Instead,
7+
This is different from [`FileEvent`](crate::file::builder::event::FileEvent) such that
8+
[`FileEvent::TrackEvent`](crate::file::builder::event::FileEvent::TrackEvent) is not used. Instead,
99
the full set of bytes from the identified track are yielded.
1010
"#]
1111
pub enum ChunkEvent<'a> {
@@ -23,7 +23,7 @@ pub enum ChunkEvent<'a> {
2323
/// See [`UnknownChunk`] for a breakdown on layout
2424
Unknown(UnknownChunk<'a>),
2525
/// End of File
26-
EOF,
26+
Eof,
2727
}
2828

2929
impl From<RawHeaderChunk> for ChunkEvent<'_> {
@@ -48,6 +48,6 @@ impl ChunkEvent<'_> {
4848
/// True if the event is the end of a file
4949
#[inline]
5050
pub const fn is_eof(&self) -> bool {
51-
matches!(self, Self::EOF)
51+
matches!(self, Self::Eof)
5252
}
5353
}
Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
use crate::prelude::*;
1+
#![doc = r#"
2+
Contains events that should be yielded when parsing a midi file.
3+
4+
You will may utilize these types when using a [`Reader`].
5+
"#]
6+
7+
use crate::{
8+
file::builder::chunk::{RawHeaderChunk, TrackChunkHeader, UnknownChunk},
9+
prelude::*,
10+
};
211

312
mod chunk;
413
pub use chunk::*;
@@ -10,7 +19,7 @@ This type is yielded by [`Reader::read_event`] and will be consumed by a Writer
1019
1120
# Overview
1221
13-
Except [`FileEvent::EOF`] Events can be placed into two categories
22+
Except [`FileEvent::Eof`] Events can be placed into two categories
1423
1524
## Chunk Events
1625
@@ -64,7 +73,7 @@ pub enum FileEvent<'a> {
6473
TrackEvent(TrackEvent<'a>),
6574

6675
/// Yielded when no more bytes can be read
67-
EOF,
76+
Eof,
6877
}
6978

7079
impl From<RawHeaderChunk> for FileEvent<'_> {

0 commit comments

Comments
 (0)