├── .gitattributes ├── .gitignore ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── main.yml ├── LICENSE ├── midi.zig ├── midi ├── file.zig ├── encode.zig ├── decode.zig └── test.zig └── example └── midi_file_to_text_stream.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | *.zig text eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache 2 | zig-out 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [Hejsil] 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "11:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | 8 | jobs: 9 | test: 10 | strategy: 11 | matrix: 12 | step: [examples, test] 13 | optimize: [Debug, ReleaseSmall, ReleaseSafe, ReleaseFast] 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | - uses: goto-bus-stop/setup-zig@v2 20 | with: 21 | version: master 22 | - run: zig build ${{ matrix.step }} -Doptimize=${{ matrix.optimize }} 23 | lint: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: goto-bus-stop/setup-zig@v2 28 | with: 29 | version: master 30 | - run: zig fmt --check . 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Jimmi Holst Christensen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /midi.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const mem = std.mem; 4 | 5 | const midi = @This(); 6 | 7 | pub const decode = @import("midi/decode.zig"); 8 | pub const encode = @import("midi/encode.zig"); 9 | pub const file = @import("midi/file.zig"); 10 | 11 | pub const File = file.File; 12 | 13 | test { 14 | _ = @import("midi/test.zig"); 15 | std.testing.refAllDecls(@This()); 16 | } 17 | 18 | pub const Message = struct { 19 | status: u7, 20 | values: [2]u7, 21 | 22 | pub fn kind(message: Message) Kind { 23 | const _kind: u3 = @truncate(message.status >> 4); 24 | const _channel: u4 = @truncate(message.status); 25 | return switch (_kind) { 26 | 0x0 => Kind.NoteOff, 27 | 0x1 => Kind.NoteOn, 28 | 0x2 => Kind.PolyphonicKeyPressure, 29 | 0x3 => Kind.ControlChange, 30 | 0x4 => Kind.ProgramChange, 31 | 0x5 => Kind.ChannelPressure, 32 | 0x6 => Kind.PitchBendChange, 33 | 0x7 => switch (_channel) { 34 | 0x0 => Kind.ExclusiveStart, 35 | 0x1 => Kind.MidiTimeCodeQuarterFrame, 36 | 0x2 => Kind.SongPositionPointer, 37 | 0x3 => Kind.SongSelect, 38 | 0x6 => Kind.TuneRequest, 39 | 0x7 => Kind.ExclusiveEnd, 40 | 0x8 => Kind.TimingClock, 41 | 0xA => Kind.Start, 42 | 0xB => Kind.Continue, 43 | 0xC => Kind.Stop, 44 | 0xE => Kind.ActiveSensing, 45 | 0xF => Kind.Reset, 46 | 47 | 0x4, 0x5, 0x9, 0xD => Kind.Undefined, 48 | }, 49 | }; 50 | } 51 | 52 | pub fn channel(message: Message) ?u4 { 53 | const _kind = message.kind(); 54 | const _channel: u4 = @truncate(message.status); 55 | switch (_kind) { 56 | // Channel events 57 | .NoteOff, 58 | .NoteOn, 59 | .PolyphonicKeyPressure, 60 | .ControlChange, 61 | .ProgramChange, 62 | .ChannelPressure, 63 | .PitchBendChange, 64 | => return _channel, 65 | 66 | // System events 67 | .ExclusiveStart, 68 | .MidiTimeCodeQuarterFrame, 69 | .SongPositionPointer, 70 | .SongSelect, 71 | .TuneRequest, 72 | .ExclusiveEnd, 73 | .TimingClock, 74 | .Start, 75 | .Continue, 76 | .Stop, 77 | .ActiveSensing, 78 | .Reset, 79 | => return null, 80 | 81 | .Undefined => return null, 82 | } 83 | } 84 | 85 | pub fn value(message: Message) u14 { 86 | // TODO: Is this the right order according to the midi spec? 87 | return @as(u14, message.values[0]) << 7 | message.values[1]; 88 | } 89 | 90 | pub fn setValue(message: *Message, v: u14) void { 91 | message.values = .{ 92 | @truncate(v >> 7), 93 | @truncate(v), 94 | }; 95 | } 96 | 97 | pub const Kind = enum { 98 | // Channel events 99 | NoteOff, 100 | NoteOn, 101 | PolyphonicKeyPressure, 102 | ControlChange, 103 | ProgramChange, 104 | ChannelPressure, 105 | PitchBendChange, 106 | 107 | // System events 108 | ExclusiveStart, 109 | MidiTimeCodeQuarterFrame, 110 | SongPositionPointer, 111 | SongSelect, 112 | TuneRequest, 113 | ExclusiveEnd, 114 | TimingClock, 115 | Start, 116 | Continue, 117 | Stop, 118 | ActiveSensing, 119 | Reset, 120 | 121 | Undefined, 122 | }; 123 | }; 124 | -------------------------------------------------------------------------------- /midi/file.zig: -------------------------------------------------------------------------------- 1 | const decode = @import("./decode.zig"); 2 | const midi = @import("../midi.zig"); 3 | const std = @import("std"); 4 | 5 | const io = std.io; 6 | const mem = std.mem; 7 | 8 | test { 9 | std.testing.refAllDecls(@This()); 10 | } 11 | 12 | pub const Header = struct { 13 | chunk: Chunk, 14 | format: u16, 15 | tracks: u16, 16 | division: u16, 17 | 18 | pub const size = 6; 19 | }; 20 | 21 | pub const Chunk = struct { 22 | kind: [4]u8, 23 | len: u32, 24 | 25 | pub const file_header = "MThd"; 26 | pub const track_header = "MTrk"; 27 | }; 28 | 29 | pub const MetaEvent = struct { 30 | kind_byte: u8, 31 | len: u28, 32 | 33 | pub fn kind(event: MetaEvent) Kind { 34 | return switch (event.kind_byte) { 35 | 0x00 => .SequenceNumber, 36 | 0x01 => .TextEvent, 37 | 0x02 => .CopyrightNotice, 38 | 0x03 => .TrackName, 39 | 0x04 => .InstrumentName, 40 | 0x05 => .Luric, 41 | 0x06 => .Marker, 42 | 0x20 => .MidiChannelPrefix, 43 | 0x2F => .EndOfTrack, 44 | 0x51 => .SetTempo, 45 | 0x54 => .SmpteOffset, 46 | 0x58 => .TimeSignature, 47 | 0x59 => .KeySignature, 48 | 0x7F => .SequencerSpecificMetaEvent, 49 | else => .Undefined, 50 | }; 51 | } 52 | 53 | pub const Kind = enum { 54 | Undefined, 55 | SequenceNumber, 56 | TextEvent, 57 | CopyrightNotice, 58 | TrackName, 59 | InstrumentName, 60 | Luric, 61 | Marker, 62 | CuePoint, 63 | MidiChannelPrefix, 64 | EndOfTrack, 65 | SetTempo, 66 | SmpteOffset, 67 | TimeSignature, 68 | KeySignature, 69 | SequencerSpecificMetaEvent, 70 | }; 71 | }; 72 | 73 | pub const TrackEvent = struct { 74 | delta_time: u28, 75 | kind: Kind, 76 | 77 | pub const Kind = union(enum) { 78 | MidiEvent: midi.Message, 79 | MetaEvent: MetaEvent, 80 | }; 81 | }; 82 | 83 | pub const File = struct { 84 | format: u16, 85 | division: u16, 86 | header_data: []const u8 = &[_]u8{}, 87 | chunks: []const FileChunk = &[_]FileChunk{}, 88 | 89 | pub const FileChunk = struct { 90 | kind: [4]u8, 91 | bytes: []const u8, 92 | }; 93 | 94 | pub fn deinit(file: File, allocator: mem.Allocator) void { 95 | for (file.chunks) |chunk| 96 | allocator.free(chunk.bytes); 97 | allocator.free(file.chunks); 98 | allocator.free(file.header_data); 99 | } 100 | }; 101 | 102 | pub const TrackIterator = struct { 103 | stream: io.FixedBufferStream([]const u8), 104 | last_event: ?TrackEvent = null, 105 | 106 | pub fn init(bytes: []const u8) TrackIterator { 107 | return .{ .stream = io.fixedBufferStream(bytes) }; 108 | } 109 | 110 | pub const Result = struct { 111 | event: TrackEvent, 112 | data: []const u8, 113 | }; 114 | 115 | pub fn next(it: *TrackIterator) ?Result { 116 | const s = it.stream.inStream(); 117 | const event = decode.trackEvent(s, it.last_event) catch return null; 118 | it.last_event = event; 119 | 120 | const start = it.stream.pos; 121 | 122 | const end = switch (event.kind) { 123 | .MetaEvent => |meta_event| blk: { 124 | it.stream.pos += meta_event.len; 125 | break :blk it.stream.pos; 126 | }, 127 | .MidiEvent => |midi_event| blk: { 128 | if (midi_event.kind() == .ExclusiveStart) { 129 | while ((try s.readByte()) != 0xF7) {} 130 | break :blk it.stream.pos - 1; 131 | } 132 | break :blk it.stream.pos; 133 | }, 134 | }; 135 | 136 | return Result{ 137 | .event = event, 138 | .data = s.buffer[start..end], 139 | }; 140 | } 141 | }; 142 | -------------------------------------------------------------------------------- /midi/encode.zig: -------------------------------------------------------------------------------- 1 | const midi = @import("../midi.zig"); 2 | const std = @import("std"); 3 | 4 | const debug = std.debug; 5 | const io = std.io; 6 | const math = std.math; 7 | const mem = std.mem; 8 | 9 | const encode = @This(); 10 | 11 | test { 12 | std.testing.refAllDecls(@This()); 13 | } 14 | 15 | pub fn message(writer: anytype, last_message: ?midi.Message, msg: midi.Message) !void { 16 | if (msg.channel() == null or last_message == null or msg.status != last_message.?.status) { 17 | try writer.writeByte((1 << 7) | @as(u8, msg.status)); 18 | } 19 | 20 | switch (msg.kind()) { 21 | .ExclusiveStart, 22 | .TuneRequest, 23 | .ExclusiveEnd, 24 | .TimingClock, 25 | .Start, 26 | .Continue, 27 | .Stop, 28 | .ActiveSensing, 29 | .Reset, 30 | .Undefined, 31 | => {}, 32 | .ProgramChange, 33 | .ChannelPressure, 34 | .MidiTimeCodeQuarterFrame, 35 | .SongSelect, 36 | => { 37 | try writer.writeByte(msg.values[0]); 38 | }, 39 | .NoteOff, 40 | .NoteOn, 41 | .PolyphonicKeyPressure, 42 | .ControlChange, 43 | .PitchBendChange, 44 | .SongPositionPointer, 45 | => { 46 | try writer.writeByte(msg.values[0]); 47 | try writer.writeByte(msg.values[1]); 48 | }, 49 | } 50 | } 51 | 52 | pub fn chunkToBytes(_chunk: midi.file.Chunk) [8]u8 { 53 | var res: [8]u8 = undefined; 54 | @memcpy(res[0..4], &_chunk.kind); 55 | mem.writeInt(u32, res[4..8], _chunk.len, .big); 56 | return res; 57 | } 58 | 59 | pub fn fileHeaderToBytes(header: midi.file.Header) [14]u8 { 60 | var res: [14]u8 = undefined; 61 | @memcpy(res[0..8], &chunkToBytes(header.chunk)); 62 | mem.writeInt(u16, res[8..10], header.format, .big); 63 | mem.writeInt(u16, res[10..12], header.tracks, .big); 64 | mem.writeInt(u16, res[12..14], header.division, .big); 65 | return res; 66 | } 67 | 68 | pub fn int(writer: anytype, i: u28) !void { 69 | var tmp = i; 70 | var is_first = true; 71 | var buf: [4]u8 = undefined; 72 | var fbs = io.fixedBufferStream(&buf); 73 | const w = fbs.writer(); 74 | 75 | // TODO: Can we find a way to not encode this in reverse order and then flipping the bytes? 76 | while (tmp != 0 or is_first) : (is_first = false) { 77 | w.writeByte(@as(u7, @truncate(tmp)) | (@as(u8, 1 << 7) * @intFromBool(!is_first))) catch 78 | unreachable; 79 | tmp >>= 7; 80 | } 81 | mem.reverse(u8, fbs.getWritten()); 82 | try writer.writeAll(fbs.getWritten()); 83 | } 84 | 85 | pub fn metaEvent(writer: anytype, event: midi.file.MetaEvent) !void { 86 | try writer.writeByte(event.kind_byte); 87 | try int(writer, event.len); 88 | } 89 | 90 | pub fn trackEvent(writer: anytype, last_event: ?midi.file.TrackEvent, event: midi.file.TrackEvent) !void { 91 | const last_midi_event = if (last_event) |e| switch (e.kind) { 92 | .MidiEvent => |m| m, 93 | .MetaEvent => null, 94 | } else null; 95 | 96 | try int(writer, event.delta_time); 97 | switch (event.kind) { 98 | .MetaEvent => |meta| { 99 | try writer.writeByte(0xFF); 100 | try metaEvent(writer, meta); 101 | }, 102 | .MidiEvent => |msg| try message(writer, last_midi_event, msg), 103 | } 104 | } 105 | 106 | pub fn file(writer: anytype, f: midi.File) !void { 107 | try writer.writeAll(&encode.fileHeaderToBytes(.{ 108 | .chunk = .{ 109 | .kind = midi.file.Chunk.file_header.*, 110 | .len = @intCast(midi.file.Header.size + f.header_data.len), 111 | }, 112 | .format = f.format, 113 | .tracks = @intCast(f.chunks.len), 114 | .division = f.division, 115 | })); 116 | try writer.writeAll(f.header_data); 117 | 118 | for (f.chunks) |c| { 119 | try writer.writeAll(&encode.chunkToBytes(.{ 120 | .kind = c.kind, 121 | .len = @intCast(c.bytes.len), 122 | })); 123 | try writer.writeAll(c.bytes); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /example/midi_file_to_text_stream.zig: -------------------------------------------------------------------------------- 1 | const midi = @import("midi"); 2 | const std = @import("std"); 3 | 4 | pub fn main() !void { 5 | const stdin = std.io.getStdIn().reader(); 6 | 7 | const header = try midi.decode.fileHeader(stdin); 8 | std.debug.print("file_header:\n", .{}); 9 | std.debug.print(" chunk_type: {s}\n", .{header.chunk.kind}); 10 | std.debug.print(" chunk_len: {}\n", .{header.chunk.len}); 11 | std.debug.print(" format: {}\n", .{header.format}); 12 | std.debug.print(" tracks: {}\n", .{header.tracks}); 13 | std.debug.print(" division: {}\n", .{header.division}); 14 | 15 | // The midi standard says that we should respect the headers size, even if it 16 | // is bigger than nessesary. We therefor need to figure out what to do with the 17 | // extra bytes in the header. This example will just skip them. 18 | try stdin.skipBytes(header.chunk.len - midi.file.Header.size, .{}); 19 | 20 | while (true) { 21 | const chunk = midi.decode.chunk(stdin) catch |err| switch (err) { 22 | error.EndOfStream => break, 23 | else => |e| return e, 24 | }; 25 | 26 | std.debug.print("chunk:\n", .{}); 27 | std.debug.print(" type: {s}\n", .{chunk.kind}); 28 | std.debug.print(" len: {}\n", .{chunk.len}); 29 | 30 | // If our chunk isn't a track header, we just skip it. 31 | if (!std.mem.eql(u8, &chunk.kind, midi.file.Chunk.track_header)) { 32 | try stdin.skipBytes(chunk.len, .{}); 33 | continue; 34 | } 35 | 36 | // To be decode midi correctly, we have to keep track of the last 37 | // event. Midi can compress midi event if the same kind of event 38 | // is repeated. 39 | var last_event: ?midi.file.TrackEvent = null; 40 | while (true) { 41 | const event = try midi.decode.trackEvent(stdin, last_event); 42 | last_event = event; 43 | 44 | std.debug.print(" {:>6}", .{event.delta_time}); 45 | switch (event.kind) { 46 | .MetaEvent => |meta_event| { 47 | var buf: [1024]u8 = undefined; 48 | const data = buf[0..meta_event.len]; 49 | try stdin.readNoEof(data); 50 | 51 | std.debug.print(" {s:>20} {:>6}", .{ metaEventKindToStr(meta_event.kind()), meta_event.len }); 52 | switch (meta_event.kind()) { 53 | .Luric, .InstrumentName, .TrackName => std.debug.print(" {s}\n", .{data}), 54 | .EndOfTrack => { 55 | std.debug.print("\n", .{}); 56 | break; 57 | }, 58 | else => std.debug.print("\n", .{}), 59 | } 60 | }, 61 | .MidiEvent => |midi_event| { 62 | std.debug.print(" {s:>20}", .{midiEventKindToStr(midi_event.kind())}); 63 | if (midi_event.channel()) |channel| 64 | std.debug.print(" {:>6}", .{channel}); 65 | std.debug.print(" {:>3} {:>3}\n", .{ midi_event.values[0], midi_event.values[1] }); 66 | 67 | if (midi_event.kind() == .ExclusiveStart) { 68 | while ((try stdin.readByte()) != 0xF7) {} 69 | } 70 | }, 71 | } 72 | } 73 | } 74 | } 75 | 76 | fn metaEventKindToStr(kind: midi.file.MetaEvent.Kind) []const u8 { 77 | return switch (kind) { 78 | .Undefined => "undef", 79 | .SequenceNumber => "seqnum", 80 | .TextEvent => "text", 81 | .CopyrightNotice => "copyright", 82 | .TrackName => "track_name", 83 | .InstrumentName => "instr_name", 84 | .Luric => "luric", 85 | .Marker => "marker", 86 | .CuePoint => "cue_point", 87 | .MidiChannelPrefix => "channel_prefix", 88 | .EndOfTrack => "eot", 89 | .SetTempo => "tempo", 90 | .SmpteOffset => "smpte_offset", 91 | .TimeSignature => "time_sig", 92 | .KeySignature => "key_sig", 93 | .SequencerSpecificMetaEvent => "seq_spec_meta_event", 94 | }; 95 | } 96 | 97 | fn midiEventKindToStr(kind: midi.Message.Kind) []const u8 { 98 | return switch (kind) { 99 | // Channel events 100 | .NoteOff => "note_off", 101 | .NoteOn => "note_on", 102 | .PolyphonicKeyPressure => "polykey_pressure", 103 | .ControlChange => "cntrl_change", 104 | .ProgramChange => "program_change", 105 | .ChannelPressure => "chnl_pressure", 106 | .PitchBendChange => "pitch_bend_change", 107 | 108 | // System events 109 | .ExclusiveStart => "excl_start", 110 | .MidiTimeCodeQuarterFrame => "midi_timecode_quater_frame", 111 | .SongPositionPointer => "song_pos_pointer", 112 | .SongSelect => "song_select", 113 | .TuneRequest => "tune_request", 114 | .ExclusiveEnd => "excl_end", 115 | .TimingClock => "timing_clock", 116 | .Start => "start", 117 | .Continue => "continue", 118 | .Stop => "stop", 119 | .ActiveSensing => "active_sens", 120 | .Reset => "reset", 121 | 122 | .Undefined => "undef", 123 | }; 124 | } 125 | -------------------------------------------------------------------------------- /midi/decode.zig: -------------------------------------------------------------------------------- 1 | const midi = @import("../midi.zig"); 2 | const std = @import("std"); 3 | 4 | const debug = std.debug; 5 | const io = std.io; 6 | const math = std.math; 7 | const mem = std.mem; 8 | 9 | const decode = @This(); 10 | 11 | test { 12 | std.testing.refAllDecls(@This()); 13 | } 14 | 15 | fn statusByte(b: u8) ?u7 { 16 | if (@as(u1, @truncate(b >> 7)) != 0) 17 | return @truncate(b); 18 | 19 | return null; 20 | } 21 | 22 | fn readDataByte(reader: anytype) !u7 { 23 | return math.cast(u7, try reader.readByte()) orelse return error.InvalidDataByte; 24 | } 25 | 26 | pub fn message(reader: anytype, last_message: ?midi.Message) !midi.Message { 27 | var first_byte: ?u8 = try reader.readByte(); 28 | const status_byte = if (statusByte(first_byte.?)) |status_byte| blk: { 29 | first_byte = null; 30 | break :blk status_byte; 31 | } else if (last_message) |m| blk: { 32 | if (m.channel() == null) 33 | return error.InvalidMessage; 34 | 35 | break :blk m.status; 36 | } else return error.InvalidMessage; 37 | 38 | const kind: u3 = @truncate(status_byte >> 4); 39 | const channel: u4 = @truncate(status_byte); 40 | switch (kind) { 41 | 0x0, 0x1, 0x2, 0x3, 0x6 => return midi.Message{ 42 | .status = status_byte, 43 | .values = [2]u7{ 44 | math.cast(u7, first_byte orelse try reader.readByte()) orelse 45 | return error.InvalidDataByte, 46 | try readDataByte(reader), 47 | }, 48 | }, 49 | 0x4, 0x5 => return midi.Message{ 50 | .status = status_byte, 51 | .values = [2]u7{ 52 | math.cast(u7, first_byte orelse try reader.readByte()) orelse 53 | return error.InvalidDataByte, 54 | 0, 55 | }, 56 | }, 57 | 0x7 => { 58 | debug.assert(first_byte == null); 59 | switch (channel) { 60 | 0x0, 0x6, 0x07, 0x8, 0xA, 0xB, 0xC, 0xE, 0xF => return midi.Message{ 61 | .status = status_byte, 62 | .values = [2]u7{ 0, 0 }, 63 | }, 64 | 0x1, 0x3 => return midi.Message{ 65 | .status = status_byte, 66 | .values = [2]u7{ 67 | try readDataByte(reader), 68 | 0, 69 | }, 70 | }, 71 | 0x2 => return midi.Message{ 72 | .status = status_byte, 73 | .values = [2]u7{ 74 | try readDataByte(reader), 75 | try readDataByte(reader), 76 | }, 77 | }, 78 | 79 | // Undefined 80 | 0x4, 0x5, 0x9, 0xD => return midi.Message{ 81 | .status = status_byte, 82 | .values = [2]u7{ 0, 0 }, 83 | }, 84 | } 85 | }, 86 | } 87 | } 88 | 89 | pub fn chunk(reader: anytype) !midi.file.Chunk { 90 | var buf: [8]u8 = undefined; 91 | try reader.readNoEof(&buf); 92 | return decode.chunkFromBytes(buf); 93 | } 94 | 95 | pub fn chunkFromBytes(bytes: [8]u8) midi.file.Chunk { 96 | return midi.file.Chunk{ 97 | .kind = bytes[0..4].*, 98 | .len = mem.readInt(u32, bytes[4..8], .big), 99 | }; 100 | } 101 | 102 | pub fn fileHeader(reader: anytype) !midi.file.Header { 103 | var buf: [14]u8 = undefined; 104 | try reader.readNoEof(&buf); 105 | return decode.fileHeaderFromBytes(buf); 106 | } 107 | 108 | pub fn fileHeaderFromBytes(bytes: [14]u8) !midi.file.Header { 109 | const _chunk = decode.chunkFromBytes(bytes[0..8].*); 110 | if (!mem.eql(u8, &_chunk.kind, midi.file.Chunk.file_header)) 111 | return error.InvalidFileHeader; 112 | if (_chunk.len < midi.file.Header.size) 113 | return error.InvalidFileHeader; 114 | 115 | return midi.file.Header{ 116 | .chunk = _chunk, 117 | .format = mem.readInt(u16, bytes[8..10], .big), 118 | .tracks = mem.readInt(u16, bytes[10..12], .big), 119 | .division = mem.readInt(u16, bytes[12..14], .big), 120 | }; 121 | } 122 | 123 | pub fn int(reader: anytype) !u28 { 124 | var res: u28 = 0; 125 | while (true) { 126 | const b = try reader.readByte(); 127 | const is_last = @as(u1, @truncate(b >> 7)) == 0; 128 | const value: u7 = @truncate(b); 129 | res = try math.mul(u28, res, math.maxInt(u7) + 1); 130 | res = try math.add(u28, res, value); 131 | 132 | if (is_last) 133 | return res; 134 | } 135 | } 136 | 137 | pub fn metaEvent(reader: anytype) !midi.file.MetaEvent { 138 | return midi.file.MetaEvent{ 139 | .kind_byte = try reader.readByte(), 140 | .len = try decode.int(reader), 141 | }; 142 | } 143 | 144 | pub fn trackEvent(reader: anytype, last_event: ?midi.file.TrackEvent) !midi.file.TrackEvent { 145 | var peek_reader = io.peekStream(1, reader); 146 | var in_reader = peek_reader.reader(); 147 | 148 | const delta_time = try decode.int(&in_reader); 149 | const first_byte = try in_reader.readByte(); 150 | if (first_byte == 0xFF) { 151 | return midi.file.TrackEvent{ 152 | .delta_time = delta_time, 153 | .kind = midi.file.TrackEvent.Kind{ .MetaEvent = try decode.metaEvent(&in_reader) }, 154 | }; 155 | } 156 | 157 | const last_midi_event = if (last_event) |e| switch (e.kind) { 158 | .MidiEvent => |m| m, 159 | .MetaEvent => null, 160 | } else null; 161 | 162 | peek_reader.putBackByte(first_byte) catch unreachable; 163 | return midi.file.TrackEvent{ 164 | .delta_time = delta_time, 165 | .kind = midi.file.TrackEvent.Kind{ .MidiEvent = try decode.message(&in_reader, last_midi_event) }, 166 | }; 167 | } 168 | 169 | /// Decodes a midi file from a reader. Caller owns the returned value 170 | /// (see: `midi.File.deinit`). 171 | pub fn file(reader: anytype, allocator: mem.Allocator) !midi.File { 172 | var chunks = std.ArrayList(midi.File.FileChunk).init(allocator); 173 | errdefer { 174 | for (chunks.items) |c| 175 | allocator.free(c.bytes); 176 | chunks.deinit(); 177 | } 178 | 179 | const header = try decode.fileHeader(reader); 180 | const header_data = try allocator.alloc(u8, header.chunk.len - midi.file.Header.size); 181 | errdefer allocator.free(header_data); 182 | 183 | try reader.readNoEof(header_data); 184 | while (true) { 185 | const c = decode.chunk(reader) catch |err| switch (err) { 186 | error.EndOfStream => break, 187 | else => |e| return e, 188 | }; 189 | 190 | const chunk_bytes = try allocator.alloc(u8, c.len); 191 | errdefer allocator.free(chunk_bytes); 192 | try reader.readNoEof(chunk_bytes); 193 | try chunks.append(.{ 194 | .kind = c.kind, 195 | .bytes = chunk_bytes, 196 | }); 197 | } 198 | 199 | return midi.File{ 200 | .format = header.format, 201 | .division = header.division, 202 | .header_data = header_data, 203 | .chunks = try chunks.toOwnedSlice(), 204 | }; 205 | } 206 | -------------------------------------------------------------------------------- /midi/test.zig: -------------------------------------------------------------------------------- 1 | const midi = @import("../midi.zig"); 2 | const std = @import("std"); 3 | 4 | const io = std.io; 5 | const mem = std.mem; 6 | const testing = std.testing; 7 | 8 | const decode = midi.decode; 9 | const encode = midi.encode; 10 | const file = midi.file; 11 | 12 | test { 13 | std.testing.refAllDecls(@This()); 14 | } 15 | 16 | test "midi.decode/encode.message" { 17 | try testMessage("\x80\x00\x00" ++ 18 | "\x7F\x7F" ++ 19 | "\x8F\x7F\x7F", &[_]midi.Message{ 20 | midi.Message{ 21 | .status = 0x00, 22 | .values = [2]u7{ 0x0, 0x0 }, 23 | }, 24 | midi.Message{ 25 | .status = 0x00, 26 | .values = [2]u7{ 0x7F, 0x7F }, 27 | }, 28 | midi.Message{ 29 | .status = 0x0F, 30 | .values = [2]u7{ 0x7F, 0x7F }, 31 | }, 32 | }); 33 | try testMessage("\x90\x00\x00" ++ 34 | "\x7F\x7F" ++ 35 | "\x9F\x7F\x7F", &[_]midi.Message{ 36 | midi.Message{ 37 | .status = 0x10, 38 | .values = [2]u7{ 0x0, 0x0 }, 39 | }, 40 | midi.Message{ 41 | .status = 0x10, 42 | .values = [2]u7{ 0x7F, 0x7F }, 43 | }, 44 | midi.Message{ 45 | .status = 0x1F, 46 | .values = [2]u7{ 0x7F, 0x7F }, 47 | }, 48 | }); 49 | try testMessage("\xA0\x00\x00" ++ 50 | "\x7F\x7F" ++ 51 | "\xAF\x7F\x7F", &[_]midi.Message{ 52 | midi.Message{ 53 | .status = 0x20, 54 | .values = [2]u7{ 0x0, 0x0 }, 55 | }, 56 | midi.Message{ 57 | .status = 0x20, 58 | .values = [2]u7{ 0x7F, 0x7F }, 59 | }, 60 | midi.Message{ 61 | .status = 0x2F, 62 | .values = [2]u7{ 0x7F, 0x7F }, 63 | }, 64 | }); 65 | try testMessage("\xB0\x00\x00" ++ 66 | "\x77\x7F" ++ 67 | "\xBF\x77\x7F", &[_]midi.Message{ 68 | midi.Message{ 69 | .status = 0x30, 70 | .values = [2]u7{ 0x0, 0x0 }, 71 | }, 72 | midi.Message{ 73 | .status = 0x30, 74 | .values = [2]u7{ 0x77, 0x7F }, 75 | }, 76 | midi.Message{ 77 | .status = 0x3F, 78 | .values = [2]u7{ 0x77, 0x7F }, 79 | }, 80 | }); 81 | try testMessage("\xC0\x00" ++ 82 | "\x7F" ++ 83 | "\xCF\x7F", &[_]midi.Message{ 84 | midi.Message{ 85 | .status = 0x40, 86 | .values = [2]u7{ 0x0, 0x0 }, 87 | }, 88 | midi.Message{ 89 | .status = 0x40, 90 | .values = [2]u7{ 0x7F, 0x0 }, 91 | }, 92 | midi.Message{ 93 | .status = 0x4F, 94 | .values = [2]u7{ 0x7F, 0x0 }, 95 | }, 96 | }); 97 | try testMessage("\xD0\x00" ++ 98 | "\x7F" ++ 99 | "\xDF\x7F", &[_]midi.Message{ 100 | midi.Message{ 101 | .status = 0x50, 102 | .values = [2]u7{ 0x0, 0x0 }, 103 | }, 104 | midi.Message{ 105 | .status = 0x50, 106 | .values = [2]u7{ 0x7F, 0x0 }, 107 | }, 108 | midi.Message{ 109 | .status = 0x5F, 110 | .values = [2]u7{ 0x7F, 0x0 }, 111 | }, 112 | }); 113 | try testMessage("\xE0\x00\x00" ++ 114 | "\x7F\x7F" ++ 115 | "\xEF\x7F\x7F", &[_]midi.Message{ 116 | midi.Message{ 117 | .status = 0x60, 118 | .values = [2]u7{ 0x0, 0x0 }, 119 | }, 120 | midi.Message{ 121 | .status = 0x60, 122 | .values = [2]u7{ 0x7F, 0x7F }, 123 | }, 124 | midi.Message{ 125 | .status = 0x6F, 126 | .values = [2]u7{ 0x7F, 0x7F }, 127 | }, 128 | }); 129 | try testMessage("\xF0\xF0", &[_]midi.Message{ 130 | midi.Message{ 131 | .status = 0x70, 132 | .values = [2]u7{ 0x0, 0x0 }, 133 | }, 134 | midi.Message{ 135 | .status = 0x70, 136 | .values = [2]u7{ 0x0, 0x0 }, 137 | }, 138 | }); 139 | try testMessage("\xF1\x00" ++ 140 | "\xF1\x0F" ++ 141 | "\xF1\x70" ++ 142 | "\xF1\x7F", &[_]midi.Message{ 143 | midi.Message{ 144 | .status = 0x71, 145 | .values = [2]u7{ 0x0, 0x0 }, 146 | }, 147 | midi.Message{ 148 | .status = 0x71, 149 | .values = [2]u7{ 0x0F, 0x0 }, 150 | }, 151 | midi.Message{ 152 | .status = 0x71, 153 | .values = [2]u7{ 0x70, 0x0 }, 154 | }, 155 | midi.Message{ 156 | .status = 0x71, 157 | .values = [2]u7{ 0x7F, 0x0 }, 158 | }, 159 | }); 160 | try testMessage("\xF2\x00\x00" ++ 161 | "\xF2\x7F\x7F", &[_]midi.Message{ 162 | midi.Message{ 163 | .status = 0x72, 164 | .values = [2]u7{ 0x0, 0x0 }, 165 | }, 166 | midi.Message{ 167 | .status = 0x72, 168 | .values = [2]u7{ 0x7F, 0x7F }, 169 | }, 170 | }); 171 | try testMessage("\xF3\x00" ++ 172 | "\xF3\x7F", &[_]midi.Message{ 173 | midi.Message{ 174 | .status = 0x73, 175 | .values = [2]u7{ 0x0, 0x0 }, 176 | }, 177 | midi.Message{ 178 | .status = 0x73, 179 | .values = [2]u7{ 0x7F, 0x0 }, 180 | }, 181 | }); 182 | try testMessage("\xF6\xF6", &[_]midi.Message{ 183 | midi.Message{ 184 | .status = 0x76, 185 | .values = [2]u7{ 0x0, 0x0 }, 186 | }, 187 | midi.Message{ 188 | .status = 0x76, 189 | .values = [2]u7{ 0x0, 0x0 }, 190 | }, 191 | }); 192 | try testMessage("\xF7\xF7", &[_]midi.Message{ 193 | midi.Message{ 194 | .status = 0x77, 195 | .values = [2]u7{ 0x0, 0x0 }, 196 | }, 197 | midi.Message{ 198 | .status = 0x77, 199 | .values = [2]u7{ 0x0, 0x0 }, 200 | }, 201 | }); 202 | try testMessage("\xF8\xF8", &[_]midi.Message{ 203 | midi.Message{ 204 | .status = 0x78, 205 | .values = [2]u7{ 0x0, 0x0 }, 206 | }, 207 | midi.Message{ 208 | .status = 0x78, 209 | .values = [2]u7{ 0x0, 0x0 }, 210 | }, 211 | }); 212 | try testMessage("\xFA\xFA", &[_]midi.Message{ 213 | midi.Message{ 214 | .status = 0x7A, 215 | .values = [2]u7{ 0x0, 0x0 }, 216 | }, 217 | midi.Message{ 218 | .status = 0x7A, 219 | .values = [2]u7{ 0x0, 0x0 }, 220 | }, 221 | }); 222 | try testMessage("\xFB\xFB", &[_]midi.Message{ 223 | midi.Message{ 224 | .status = 0x7B, 225 | .values = [2]u7{ 0x0, 0x0 }, 226 | }, 227 | midi.Message{ 228 | .status = 0x7B, 229 | .values = [2]u7{ 0x0, 0x0 }, 230 | }, 231 | }); 232 | try testMessage("\xFC\xFC", &[_]midi.Message{ 233 | midi.Message{ 234 | .status = 0x7C, 235 | .values = [2]u7{ 0x0, 0x0 }, 236 | }, 237 | midi.Message{ 238 | .status = 0x7C, 239 | .values = [2]u7{ 0x0, 0x0 }, 240 | }, 241 | }); 242 | try testMessage("\xFE\xFE", &[_]midi.Message{ 243 | midi.Message{ 244 | .status = 0x7E, 245 | .values = [2]u7{ 0x0, 0x0 }, 246 | }, 247 | midi.Message{ 248 | .status = 0x7E, 249 | .values = [2]u7{ 0x0, 0x0 }, 250 | }, 251 | }); 252 | try testMessage("\xFF\xFF", &[_]midi.Message{ 253 | midi.Message{ 254 | .status = 0x7F, 255 | .values = [2]u7{ 0x0, 0x0 }, 256 | }, 257 | midi.Message{ 258 | .status = 0x7F, 259 | .values = [2]u7{ 0x0, 0x0 }, 260 | }, 261 | }); 262 | } 263 | 264 | test "midi.decode/encode.chunk" { 265 | try testChunk("abcd\x00\x00\x00\x04".*, midi.file.Chunk{ .kind = "abcd".*, .len = 0x04 }); 266 | try testChunk("efgh\x00\x00\x04\x00".*, midi.file.Chunk{ .kind = "efgh".*, .len = 0x0400 }); 267 | try testChunk("ijkl\x00\x04\x00\x00".*, midi.file.Chunk{ .kind = "ijkl".*, .len = 0x040000 }); 268 | try testChunk("mnop\x04\x00\x00\x00".*, midi.file.Chunk{ .kind = "mnop".*, .len = 0x04000000 }); 269 | } 270 | 271 | test "midi.decode/encode.fileHeader" { 272 | try testFileHeader("MThd\x00\x00\x00\x06\x00\x00\x00\x01\x01\x10".*, midi.file.Header{ 273 | .chunk = midi.file.Chunk{ 274 | .kind = "MThd".*, 275 | .len = 6, 276 | }, 277 | .format = 0, 278 | .tracks = 0x0001, 279 | .division = 0x0110, 280 | }); 281 | try testFileHeader("MThd\x00\x00\x00\x06\x00\x01\x01\x01\x01\x10".*, midi.file.Header{ 282 | .chunk = midi.file.Chunk{ 283 | .kind = "MThd".*, 284 | .len = 6, 285 | }, 286 | .format = 1, 287 | .tracks = 0x0101, 288 | .division = 0x0110, 289 | }); 290 | try testFileHeader("MThd\x00\x00\x00\x06\x00\x02\x01\x01\x01\x10".*, midi.file.Header{ 291 | .chunk = midi.file.Chunk{ 292 | .kind = "MThd".*, 293 | .len = 6, 294 | }, 295 | .format = 2, 296 | .tracks = 0x0101, 297 | .division = 0x0110, 298 | }); 299 | try testFileHeader("MThd\x00\x00\x00\x06\x00\x00\x00\x01\xFF\x10".*, midi.file.Header{ 300 | .chunk = midi.file.Chunk{ 301 | .kind = "MThd".*, 302 | .len = 6, 303 | }, 304 | .format = 0, 305 | .tracks = 0x0001, 306 | .division = 0xFF10, 307 | }); 308 | try testFileHeader("MThd\x00\x00\x00\x06\x00\x01\x01\x01\xFF\x10".*, midi.file.Header{ 309 | .chunk = midi.file.Chunk{ 310 | .kind = "MThd".*, 311 | .len = 6, 312 | }, 313 | .format = 1, 314 | .tracks = 0x0101, 315 | .division = 0xFF10, 316 | }); 317 | try testFileHeader("MThd\x00\x00\x00\x06\x00\x02\x01\x01\xFF\x10".*, midi.file.Header{ 318 | .chunk = midi.file.Chunk{ 319 | .kind = "MThd".*, 320 | .len = 6, 321 | }, 322 | .format = 2, 323 | .tracks = 0x0101, 324 | .division = 0xFF10, 325 | }); 326 | 327 | try testing.expectError(error.InvalidFileHeader, decode.fileHeaderFromBytes("MThd\x00\x00\x00\x05\x00\x00\x00\x01\x01\x10".*)); 328 | } 329 | 330 | test "midi.decode/encode.int" { 331 | try testInt("\x00" ++ 332 | "\x40" ++ 333 | "\x7F" ++ 334 | "\x81\x00" ++ 335 | "\xC0\x00" ++ 336 | "\xFF\x7F" ++ 337 | "\x81\x80\x00" ++ 338 | "\xC0\x80\x00" ++ 339 | "\xFF\xFF\x7F" ++ 340 | "\x81\x80\x80\x00" ++ 341 | "\xC0\x80\x80\x00" ++ 342 | "\xFF\xFF\xFF\x7F", &[_]u28{ 343 | 0x00000000, 344 | 0x00000040, 345 | 0x0000007F, 346 | 0x00000080, 347 | 0x00002000, 348 | 0x00003FFF, 349 | 0x00004000, 350 | 0x00100000, 351 | 0x001FFFFF, 352 | 0x00200000, 353 | 0x08000000, 354 | 0x0FFFFFFF, 355 | }); 356 | } 357 | 358 | test "midi.decode/encode.metaEvent" { 359 | try testMetaEvent("\x00\x00" ++ 360 | "\x00\x02", &[_]midi.file.MetaEvent{ 361 | midi.file.MetaEvent{ 362 | .kind_byte = 0, 363 | .len = 0, 364 | }, 365 | midi.file.MetaEvent{ 366 | .kind_byte = 0, 367 | .len = 2, 368 | }, 369 | }); 370 | try testMetaEvent("\x01\x00" ++ 371 | "\x01\x02", &[_]midi.file.MetaEvent{ 372 | midi.file.MetaEvent{ 373 | .kind_byte = 1, 374 | .len = 0, 375 | }, 376 | midi.file.MetaEvent{ 377 | .kind_byte = 1, 378 | .len = 2, 379 | }, 380 | }); 381 | try testMetaEvent("\x02\x00" ++ 382 | "\x02\x02", &[_]midi.file.MetaEvent{ 383 | midi.file.MetaEvent{ 384 | .kind_byte = 2, 385 | .len = 0, 386 | }, 387 | midi.file.MetaEvent{ 388 | .kind_byte = 2, 389 | .len = 2, 390 | }, 391 | }); 392 | try testMetaEvent("\x03\x00" ++ 393 | "\x03\x02", &[_]midi.file.MetaEvent{ 394 | midi.file.MetaEvent{ 395 | .kind_byte = 3, 396 | .len = 0, 397 | }, 398 | midi.file.MetaEvent{ 399 | .kind_byte = 3, 400 | .len = 2, 401 | }, 402 | }); 403 | try testMetaEvent("\x04\x00" ++ 404 | "\x04\x02", &[_]midi.file.MetaEvent{ 405 | midi.file.MetaEvent{ 406 | .kind_byte = 4, 407 | .len = 0, 408 | }, 409 | midi.file.MetaEvent{ 410 | .kind_byte = 4, 411 | .len = 2, 412 | }, 413 | }); 414 | try testMetaEvent("\x05\x00" ++ 415 | "\x05\x02", &[_]midi.file.MetaEvent{ 416 | midi.file.MetaEvent{ 417 | .kind_byte = 5, 418 | .len = 0, 419 | }, 420 | midi.file.MetaEvent{ 421 | .kind_byte = 5, 422 | .len = 2, 423 | }, 424 | }); 425 | try testMetaEvent("\x06\x00" ++ 426 | "\x06\x02", &[_]midi.file.MetaEvent{ 427 | midi.file.MetaEvent{ 428 | .kind_byte = 6, 429 | .len = 0, 430 | }, 431 | midi.file.MetaEvent{ 432 | .kind_byte = 6, 433 | .len = 2, 434 | }, 435 | }); 436 | try testMetaEvent("\x20\x00" ++ 437 | "\x20\x02", &[_]midi.file.MetaEvent{ 438 | midi.file.MetaEvent{ 439 | .kind_byte = 0x20, 440 | .len = 0, 441 | }, 442 | midi.file.MetaEvent{ 443 | .kind_byte = 0x20, 444 | .len = 2, 445 | }, 446 | }); 447 | try testMetaEvent("\x2F\x00" ++ 448 | "\x2F\x02", &[_]midi.file.MetaEvent{ 449 | midi.file.MetaEvent{ 450 | .kind_byte = 0x2F, 451 | .len = 0, 452 | }, 453 | midi.file.MetaEvent{ 454 | .kind_byte = 0x2F, 455 | .len = 2, 456 | }, 457 | }); 458 | try testMetaEvent("\x51\x00" ++ 459 | "\x51\x02", &[_]midi.file.MetaEvent{ 460 | midi.file.MetaEvent{ 461 | .kind_byte = 0x51, 462 | .len = 0, 463 | }, 464 | midi.file.MetaEvent{ 465 | .kind_byte = 0x51, 466 | .len = 2, 467 | }, 468 | }); 469 | try testMetaEvent("\x54\x00" ++ 470 | "\x54\x02", &[_]midi.file.MetaEvent{ 471 | midi.file.MetaEvent{ 472 | .kind_byte = 0x54, 473 | .len = 0, 474 | }, 475 | midi.file.MetaEvent{ 476 | .kind_byte = 0x54, 477 | .len = 2, 478 | }, 479 | }); 480 | try testMetaEvent("\x58\x00" ++ 481 | "\x58\x02", &[_]midi.file.MetaEvent{ 482 | midi.file.MetaEvent{ 483 | .kind_byte = 0x58, 484 | .len = 0, 485 | }, 486 | midi.file.MetaEvent{ 487 | .kind_byte = 0x58, 488 | .len = 2, 489 | }, 490 | }); 491 | try testMetaEvent("\x59\x00" ++ 492 | "\x59\x02", &[_]midi.file.MetaEvent{ 493 | midi.file.MetaEvent{ 494 | .kind_byte = 0x59, 495 | .len = 0, 496 | }, 497 | midi.file.MetaEvent{ 498 | .kind_byte = 0x59, 499 | .len = 2, 500 | }, 501 | }); 502 | try testMetaEvent("\x7F\x00" ++ 503 | "\x7F\x02", &[_]midi.file.MetaEvent{ 504 | midi.file.MetaEvent{ 505 | .kind_byte = 0x7F, 506 | .len = 0, 507 | }, 508 | midi.file.MetaEvent{ 509 | .kind_byte = 0x7F, 510 | .len = 2, 511 | }, 512 | }); 513 | } 514 | 515 | test "midi.decode/encode.trackEvent" { 516 | try testTrackEvent("\x00\xFF\x00\x00" ++ 517 | "\x00\xFF\x00\x02", &[_]midi.file.TrackEvent{ 518 | midi.file.TrackEvent{ 519 | .delta_time = 0, 520 | .kind = midi.file.TrackEvent.Kind{ 521 | .MetaEvent = midi.file.MetaEvent{ 522 | .kind_byte = 0, 523 | .len = 0, 524 | }, 525 | }, 526 | }, 527 | midi.file.TrackEvent{ 528 | .delta_time = 0, 529 | .kind = midi.file.TrackEvent.Kind{ 530 | .MetaEvent = midi.file.MetaEvent{ 531 | .kind_byte = 0, 532 | .len = 2, 533 | }, 534 | }, 535 | }, 536 | }); 537 | try testTrackEvent("\x00\x80\x00\x00" ++ 538 | "\x00\x7F\x7F" ++ 539 | "\x00\xFF\x00\x02", &[_]midi.file.TrackEvent{ 540 | midi.file.TrackEvent{ 541 | .delta_time = 0, 542 | .kind = midi.file.TrackEvent.Kind{ 543 | .MidiEvent = midi.Message{ 544 | .status = 0x00, 545 | .values = [2]u7{ 0x0, 0x0 }, 546 | }, 547 | }, 548 | }, 549 | midi.file.TrackEvent{ 550 | .delta_time = 0, 551 | .kind = midi.file.TrackEvent.Kind{ 552 | .MidiEvent = midi.Message{ 553 | .status = 0x00, 554 | .values = [2]u7{ 0x7F, 0x7F }, 555 | }, 556 | }, 557 | }, 558 | midi.file.TrackEvent{ 559 | .delta_time = 0, 560 | .kind = midi.file.TrackEvent.Kind{ 561 | .MetaEvent = midi.file.MetaEvent{ 562 | .kind_byte = 0, 563 | .len = 2, 564 | }, 565 | }, 566 | }, 567 | }); 568 | } 569 | 570 | test "midi.decode/encode.file" { 571 | try testFile( 572 | // File header 573 | "MThd\x00\x00\x00\x08\x00\x02\x00\x02\xFF\x10\xFF\xFF" ++ 574 | // Random chunk 575 | "abcd\x00\x00\x00\x04\xFF\xFF\xFF\xFF" ++ 576 | // Track 577 | "MTrk\x00\x00\x00\x17" ++ 578 | "\x00\xFF\x00\x00" ++ 579 | "\x00\xFF\x00\x02\xaa\xbb" ++ 580 | "\x00\x80\x00\x00" ++ 581 | "\x00\x7F\x7F" ++ 582 | "\x00\xFF\x00\x02\xaa\xbb", 583 | ); 584 | } 585 | 586 | fn testFile(bytes: []const u8) !void { 587 | var out_buf: [1024]u8 = undefined; 588 | var fb_writer = io.fixedBufferStream(&out_buf); 589 | var fb_reader = io.fixedBufferStream(bytes); 590 | const writer = fb_writer.writer(); 591 | const reader = fb_reader.reader(); 592 | const allocator = testing.allocator; 593 | 594 | const actual = try decode.file(reader, allocator); 595 | defer actual.deinit(allocator); 596 | try encode.file(writer, actual); 597 | 598 | try testing.expectError(error.EndOfStream, reader.readByte()); 599 | try testing.expectEqualSlices(u8, bytes, fb_writer.getWritten()); 600 | } 601 | 602 | fn testMessage(bytes: []const u8, results: []const midi.Message) !void { 603 | var last: ?midi.Message = null; 604 | var out_buf: [1024]u8 = undefined; 605 | var fb_writer = io.fixedBufferStream(&out_buf); 606 | var fb_reader = io.fixedBufferStream(bytes); 607 | const writer = fb_writer.writer(); 608 | const reader = fb_reader.reader(); 609 | for (results) |expected| { 610 | const actual = try decode.message(reader, last); 611 | try encode.message(writer, last, actual); 612 | try testing.expectEqual(expected, actual); 613 | last = actual; 614 | } 615 | 616 | try testing.expectError(error.EndOfStream, reader.readByte()); 617 | try testing.expectEqualSlices(u8, bytes, fb_writer.getWritten()); 618 | } 619 | 620 | fn testInt(bytes: []const u8, results: []const u28) !void { 621 | var out_buf: [1024]u8 = undefined; 622 | var fb_reader = io.fixedBufferStream(bytes); 623 | const reader = fb_reader.reader(); 624 | for (results) |expected| { 625 | var fb_writer = io.fixedBufferStream(&out_buf); 626 | const writer = fb_writer.writer(); 627 | 628 | const before = fb_reader.pos; 629 | const actual = try decode.int(reader); 630 | const after = fb_reader.pos; 631 | 632 | try encode.int(writer, actual); 633 | 634 | try testing.expectEqual(expected, actual); 635 | try testing.expectEqualSlices(u8, bytes[before..after], fb_writer.getWritten()); 636 | } 637 | 638 | try testing.expectError(error.EndOfStream, reader.readByte()); 639 | } 640 | 641 | fn testMetaEvent(bytes: []const u8, results: []const midi.file.MetaEvent) !void { 642 | var out_buf: [1024]u8 = undefined; 643 | var fb_writer = io.fixedBufferStream(&out_buf); 644 | var fb_reader = io.fixedBufferStream(bytes); 645 | const writer = fb_writer.writer(); 646 | const reader = fb_reader.reader(); 647 | for (results) |expected| { 648 | const actual = try decode.metaEvent(reader); 649 | try encode.metaEvent(writer, actual); 650 | try testing.expectEqual(expected, actual); 651 | } 652 | 653 | try testing.expectError(error.EndOfStream, reader.readByte()); 654 | try testing.expectEqualSlices(u8, bytes, fb_writer.getWritten()); 655 | } 656 | 657 | fn testTrackEvent(bytes: []const u8, results: []const midi.file.TrackEvent) !void { 658 | var last: ?midi.file.TrackEvent = null; 659 | var out_buf: [1024]u8 = undefined; 660 | var fb_writer = io.fixedBufferStream(&out_buf); 661 | var fb_reader = io.fixedBufferStream(bytes); 662 | const writer = fb_writer.writer(); 663 | const reader = fb_reader.reader(); 664 | for (results) |expected| { 665 | const actual = try decode.trackEvent(reader, last); 666 | try encode.trackEvent(writer, last, actual); 667 | try testing.expectEqual(expected.delta_time, actual.delta_time); 668 | switch (expected.kind) { 669 | .MetaEvent => try testing.expectEqual(expected.kind.MetaEvent, actual.kind.MetaEvent), 670 | .MidiEvent => try testing.expectEqual(expected.kind.MidiEvent, actual.kind.MidiEvent), 671 | } 672 | last = actual; 673 | } 674 | 675 | try testing.expectError(error.EndOfStream, reader.readByte()); 676 | try testing.expectEqualSlices(u8, bytes, fb_writer.getWritten()); 677 | } 678 | 679 | fn testChunk(bytes: [8]u8, chunk: midi.file.Chunk) !void { 680 | const decoded = decode.chunkFromBytes(bytes); 681 | const encoded = encode.chunkToBytes(chunk); 682 | try testing.expectEqual(bytes, encoded); 683 | try testing.expectEqual(chunk, decoded); 684 | } 685 | 686 | fn testFileHeader(bytes: [14]u8, header: midi.file.Header) !void { 687 | const decoded = try decode.fileHeaderFromBytes(bytes); 688 | const encoded = encode.fileHeaderToBytes(header); 689 | try testing.expectEqual(bytes, encoded); 690 | try testing.expectEqual(header, decoded); 691 | } 692 | --------------------------------------------------------------------------------