Zig 音頻之 MIDI —— 源碼解讀

MIDI 是 “樂器數字接口” 的縮寫,是一種用於音樂設備之間通信的協議。而 zig-midi[1] 主要是在對 MIDI 的元數據、音頻頭等元數據進行一些處理的方法上進行了集成。

.
├── LICENSE
├── ReadMe.md
├── build.zig
├── example
│   └── midi_file_to_text_stream.zig
├── midi
│   ├── decode.zig
│   ├── encode.zig
│   ├── file.zig
│   └── test.zig
├── midi.zig

基礎

在 MIDI 協議中,0xFF 是一個特定的狀態字節,用來表示元事件(Meta Event)的開始。元事件是 MIDI 文件結構中的一種特定消息,通常不用於實時音頻播放,但它們包含有關 MIDI 序列的元數據,例如序列名稱、版權信息、歌詞、時間標記、速度(BPM)更改等。

以下是一些常見的元事件類型及其關聯的 0xFF 後的字節:

例如,當解析 MIDI 文件時,如果遇到字節 0xFF 0x03,那麼接下來的字節將表示序列或曲目名稱。

在實際的 MIDI 文件中,元事件的具體結構是這樣的:

  1. 0xFF: 元事件狀態字節。

  2. 元事件類型字節,例如上面列出的 0x000x01 等。

  3. 長度字節(或一系列字節),表示該事件數據的長度。

  4. 事件數據本身。

元事件主要存在於 MIDI 文件中,特別是在標準 MIDI 文件 (SMF) 的上下文中。在實時 MIDI 通信中,元事件通常不會被髮送,因爲它們通常不會影響音樂的實際播放。

Midi.zig

本文件主要是處理 MIDI 消息的模塊,爲處理 MIDI 消息提供了基礎結構和函數。

const std = @import("std");

const mem = std.mem;

const midi = @This();

pub const decode = @import("midi/decode.zig");
pub const encode = @import("midi/encode.zig");
pub const file = @import("midi/file.zig");

pub const File = file.File;

test "midi" {
    _ = @import("midi/test.zig");
    _ = decode;
    _ = file;
}

pub const Message = struct {
    status: u7,
    values: [2]u7,

    pub fn kind(message: Message) Kind {
        const _kind = @as(u3, @truncate(message.status >> 4));
        const _channel = @as(u4, @truncate(message.status));
        return switch (_kind) {
            0x0 => Kind.NoteOff,
            0x1 => Kind.NoteOn,
            0x2 => Kind.PolyphonicKeyPressure,
            0x3 => Kind.ControlChange,
            0x4 => Kind.ProgramChange,
            0x5 => Kind.ChannelPressure,
            0x6 => Kind.PitchBendChange,
            0x7 => switch (_channel) {
                0x0 => Kind.ExclusiveStart,
                0x1 => Kind.MidiTimeCodeQuarterFrame,
                0x2 => Kind.SongPositionPointer,
                0x3 => Kind.SongSelect,
                0x6 => Kind.TuneRequest,
                0x7 => Kind.ExclusiveEnd,
                0x8 => Kind.TimingClock,
                0xA => Kind.Start,
                0xB => Kind.Continue,
                0xC => Kind.Stop,
                0xE => Kind.ActiveSensing,
                0xF => Kind.Reset,

                0x4, 0x5, 0x9, 0xD => Kind.Undefined,
            },
        };
    }

    pub fn channel(message: Message) ?u4 {
        const _kind = message.kind();
        const _channel = @as(u4, @truncate(message.status));
        switch (_kind) {
            // Channel events
            .NoteOff,
            .NoteOn,
            .PolyphonicKeyPressure,
            .ControlChange,
            .ProgramChange,
            .ChannelPressure,
            .PitchBendChange,
            => return _channel,

            // System events
            .ExclusiveStart,
            .MidiTimeCodeQuarterFrame,
            .SongPositionPointer,
            .SongSelect,
            .TuneRequest,
            .ExclusiveEnd,
            .TimingClock,
            .Start,
            .Continue,
            .Stop,
            .ActiveSensing,
            .Reset,
            => return null,

            .Undefined => return null,
        }
    }

    pub fn value(message: Message) u14 {
        // TODO: Is this the right order according to the midi spec?
        return @as(u14, message.values[0]) << 7 | message.values[1];
    }

    pub fn setValue(message: *Message, v: u14) void {
        message.values = .{
            @as(u7, @truncate(v >> 7)),
            @as(u7, @truncate(v)),
        };
    }

    pub const Kind = enum {
        // Channel events
        NoteOff,
        NoteOn,
        PolyphonicKeyPressure,
        ControlChange,
        ProgramChange,
        ChannelPressure,
        PitchBendChange,

        // System events
        ExclusiveStart,
        MidiTimeCodeQuarterFrame,
        SongPositionPointer,
        SongSelect,
        TuneRequest,
        ExclusiveEnd,
        TimingClock,
        Start,
        Continue,
        Stop,
        ActiveSensing,
        Reset,

        Undefined,
    };
};

這定義了一個名爲 Message 的公共結構,表示 MIDI 消息,爲處理 MIDI 消息提供了基礎結構和函數。它包含三個字段:狀態、值和幾個公共方法。

midi 消息結構

我們需要先了解 MIDI 消息的一些背景。

在 MIDI 協議中,某些消息的值可以跨越兩個 7 位的字節,這是因爲 MIDI 協議不使用每個字節的最高位(這通常被稱爲狀態位)。這意味着每個字節只使用它的低 7 位來攜帶數據。因此,當需要發送一個大於 7 位的值時(比如 14 位),它會被拆分成兩個 7 位的字節。

setValue 這個函數做的事情是將一個 14 位的值(u14)拆分爲兩個 7 位的值,並將它們設置到 message.values 中。

以下是具體步驟的解釋:

  1. 獲取高 7 位v >> 7 把 14 位的值右移 7 位,這樣我們就得到了高 7 位的值。

  2. 截斷並轉換@truncate(v >> 7) 截斷高 7 位的值,確保它是 7 位的。@as(u7, @truncate(v >> 7)) 確保這個值是 u7 類型,即一個 7 位的無符號整數。

  3. 獲取低 7 位@truncate(v) 直接截斷原始值,保留低 7 位。

  4. 設置值message.values = .{ ... } 將這兩個 7 位的值設置到 message.values 中。

事件

針對事件,我們看 enum。

    pub const Kind = enum {
        // Channel events
        NoteOff,
        NoteOn,
        PolyphonicKeyPressure,
        ControlChange,
        ProgramChange,
        ChannelPressure,
        PitchBendChange,

        // System events
        ExclusiveStart,
        MidiTimeCodeQuarterFrame,
        SongPositionPointer,
        SongSelect,
        TuneRequest,
        ExclusiveEnd,
        TimingClock,
        Start,
        Continue,
        Stop,
        ActiveSensing,
        Reset,

        Undefined,
    };

這段代碼定義了一個名爲 Kind 的公共枚舉類型(enum),它描述了 MIDI 中可能的事件種類。每個枚舉成員都代表 MIDI 協議中的一個特定事件。這些事件分爲兩大類:頻道事件(Channel events)和系統事件(System events)。

這個 Kind 枚舉爲處理 MIDI 消息提供了一個結構化的方法,使得在編程時可以清晰地引用特定的 MIDI 事件,而不是依賴於原始的數字或其他編碼。

以下是對每個枚舉成員的簡要說明:

頻道事件 (Channel events)

  1. NoteOff:這是一個音符結束事件,表示某個音符不再播放。

  2. NoteOn:這是一個音符開始事件,表示開始播放某個音符。

  3. PolyphonicKeyPressure:多聲道鍵盤壓力事件,表示對特定音符的壓力或觸摸敏感度的變化。

  4. ControlChange:控制變更事件,用於發送如音量、平衡等控制信號。

  5. ProgramChange:程序(音色)變更事件,用於改變樂器的音色。

  6. ChannelPressure:頻道壓力事件,與多聲道鍵盤壓力相似,但它適用於整個頻道,而不是特定音符。

  7. PitchBendChange:音高彎曲變更事件,表示音符音高的上升或下降。

系統事件 (System events)

  1. ExclusiveStart:獨佔開始事件,標誌着一個獨佔消息序列的開始。

  2. MidiTimeCodeQuarterFrame:MIDI 時間碼四分之一幀,用於同步與其他設備。

  3. SongPositionPointer:歌曲位置指針,指示序列器的當前播放位置。

  4. SongSelect:歌曲選擇事件,用於選擇特定的歌曲或序列。

  5. TuneRequest:調音請求事件,指示設備應進行自我調音。

  6. ExclusiveEnd:獨佔結束事件,標誌着一個獨佔消息序列的結束。

  7. TimingClock:計時時鐘事件,用於節奏的同步。

  8. Start:開始事件,用於啓動序列播放。

  9. Continue:繼續事件,用於繼續暫停的序列播放。

  10. Stop:停止事件,用於停止序列播放。

  11. ActiveSensing:活動感知事件,是一種心跳信號,表示設備仍然在線並工作。

  12. Reset:重置事件,用於將設備重置爲其初始狀態。

其他

  1. Undefined:未定義事件,可能表示一個未在此枚舉中定義的或無效的 MIDI 事件。

decode.zig

本文件是對 MIDI 文件的解碼器, 提供了一組工具,可以從不同的輸入源解析 MIDI 文件的各個部分。這樣可以方便地讀取和處理 MIDI 文件。

const midi = @import("../midi.zig");
const std = @import("std");

const debug = std.debug;
const io = std.io;
const math = std.math;
const mem = std.mem;

const decode = @This();

fn statusByte(b: u8) ?u7 {
    if (@as(u1, @truncate(b >> 7)) != 0)
        return @as(u7, @truncate(b));

    return null;
}

fn readDataByte(reader: anytype) !u7 {
    return math.cast(u7, try reader.readByte()) catch return error.InvalidDataByte;
}

pub fn message(reader: anytype, last_message: ?midi.Message) !midi.Message {
    var first_byte: ?u8 = try reader.readByte();
    const status_byte = if (statusByte(first_byte.?)) |status_byte| blk: {
        first_byte = null;
        break :blk status_byte;
    } else if (last_message) |m| blk: {
        if (m.channel() == null)
            return error.InvalidMessage;

        break :blk m.status;
    } else return error.InvalidMessage;

    const kind = @as(u3, @truncate(status_byte >> 4));
    const channel = @as(u4, @truncate(status_byte));
    switch (kind) {
        0x0, 0x1, 0x2, 0x3, 0x6 => return midi.Message{
            .status = status_byte,
            .values = [2]u7{
                math.cast(u7, first_byte orelse try reader.readByte()) catch return error.InvalidDataByte,
                try readDataByte(reader),
            },
        },
        0x4, 0x5 => return midi.Message{
            .status = status_byte,
            .values = [2]u7{
                math.cast(u7, first_byte orelse try reader.readByte()) catch return error.InvalidDataByte,
                0,
            },
        },
        0x7 => {
            debug.assert(first_byte == null);
            switch (channel) {
                0x0, 0x6, 0x07, 0x8, 0xA, 0xB, 0xC, 0xE, 0xF => return midi.Message{
                    .status = status_byte,
                    .values = [2]u7{ 0, 0 },
                },
                0x1, 0x3 => return midi.Message{
                    .status = status_byte,
                    .values = [2]u7{
                        try readDataByte(reader),
                        0,
                    },
                },
                0x2 => return midi.Message{
                    .status = status_byte,
                    .values = [2]u7{
                        try readDataByte(reader),
                        try readDataByte(reader),
                    },
                },

                // Undefined
                0x4, 0x5, 0x9, 0xD => return midi.Message{
                    .status = status_byte,
                    .values = [2]u7{ 0, 0 },
                },
            }
        },
    }
}

pub fn chunk(reader: anytype) !midi.file.Chunk {
    var buf: [8]u8 = undefined;
    try reader.readNoEof(&buf);
    return decode.chunkFromBytes(buf);
}

pub fn chunkFromBytes(bytes: [8]u8) midi.file.Chunk {
    return midi.file.Chunk{
        .kind = bytes[0..4].*,
        .len = mem.readIntBig(u32, bytes[4..8]),
    };
}

pub fn fileHeader(reader: anytype) !midi.file.Header {
    var buf: [14]u8 = undefined;
    try reader.readNoEof(&buf);
    return decode.fileHeaderFromBytes(buf);
}

pub fn fileHeaderFromBytes(bytes: [14]u8) !midi.file.Header {
    const _chunk = decode.chunkFromBytes(bytes[0..8].*);
    if (!mem.eql(u8, &_chunk.kind, midi.file.Chunk.file_header))
        return error.InvalidFileHeader;
    if (_chunk.len < midi.file.Header.size)
        return error.InvalidFileHeader;

    return midi.file.Header{
        .chunk = _chunk,
        .format = mem.readIntBig(u16, bytes[8..10]),
        .tracks = mem.readIntBig(u16, bytes[10..12]),
        .division = mem.readIntBig(u16, bytes[12..14]),
    };
}

pub fn int(reader: anytype) !u28 {
    var res: u28 = 0;
    while (true) {
        const b = try reader.readByte();
        const is_last = @as(u1, @truncate(b >> 7)) == 0;
        const value = @as(u7, @truncate(b));
        res = try math.mul(u28, res, math.maxInt(u7) + 1);
        res = try math.add(u28, res, value);

        if (is_last)
            return res;
    }
}

pub fn metaEvent(reader: anytype) !midi.file.MetaEvent {
    return midi.file.MetaEvent{
        .kind_byte = try reader.readByte(),
        .len = try decode.int(reader),
    };
}

pub fn trackEvent(reader: anytype, last_event: ?midi.file.TrackEvent) !midi.file.TrackEvent {
    var peek_reader = io.peekStream(1, reader);
    var in_reader = peek_reader.reader();

    const delta_time = try decode.int(&in_reader);
    const first_byte = try in_reader.readByte();
    if (first_byte == 0xFF) {
        return midi.file.TrackEvent{
            .delta_time = delta_time,
            .kind = midi.file.TrackEvent.Kind{ .MetaEvent = try decode.metaEvent(&in_reader) },
        };
    }

    const last_midi_event = if (last_event) |e| switch (e.kind) {
        .MidiEvent => |m| m,
        .MetaEvent => null,
    } else null;

    peek_reader.putBackByte(first_byte) catch unreachable;
    return midi.file.TrackEvent{
        .delta_time = delta_time,
        .kind = midi.file.TrackEvent.Kind{ .MidiEvent = try decode.message(&in_reader, last_midi_event) },
    };
}

/// Decodes a midi file from a reader. Caller owns the returned value
///  (see: `midi.File.deinit`).
pub fn file(reader: anytype, allocator: *mem.Allocator) !midi.File {
    var chunks = std.ArrayList(midi.File.FileChunk).init(allocator);
    errdefer {
        (midi.File{
            .format = 0,
            .division = 0,
            .chunks = chunks.toOwnedSlice(),
        }).deinit(allocator);
    }

    const header = try decode.fileHeader(reader);
    const header_data = try allocator.alloc(u8, header.chunk.len - midi.file.Header.size);
    errdefer allocator.free(header_data);

    try reader.readNoEof(header_data);
    while (true) {
        const c = decode.chunk(reader) catch |err| switch (err) {
            error.EndOfStream => break,
            else => |e| return e,
        };

        const chunk_bytes = try allocator.alloc(u8, c.len);
        errdefer allocator.free(chunk_bytes);
        try reader.readNoEof(chunk_bytes);
        try chunks.append(.{
            .kind = c.kind,
            .bytes = chunk_bytes,
        });
    }

    return midi.File{
        .format = header.format,
        .division = header.division,
        .header_data = header_data,
        .chunks = chunks.toOwnedSlice(),
    };
}
  1. statusByte: 解析 MIDI 消息的首個字節,來確定是否這是一個狀態字節,還是一個數據字節。將一個字節 b 解碼爲一個 u7 類型的 MIDI 狀態字節,如果字節 b 不是一個狀態字節,則返回 null。換句話說,midi 的消息是 14 位,如果高 7 位不爲空,則是 midi 消息的狀態字節。在 MIDI 協議中,消息的首個字節通常是狀態字節,但也可能用之前的狀態字節(這稱爲 “運行狀態”)來解釋接下來的字節。因此,這段代碼需要確定它是否讀取了一個新的狀態字節,或者它是否應該使用前一個消息的狀態字節。

  2. readDataByte: 從 reader 中讀取並返回一個數據字節。如果讀取的字節不符合數據字節的規定,則拋出 InvalidDataByte 錯誤。

  3. message: 從 reader 讀取並解碼一個 MIDI 消息。如果讀取的字節不能形成一個有效的 MIDI 消息,則拋出 InvalidMessage 錯誤。這是一個複雜的函數,涉及到解析 MIDI 消息的不同種類。

  4. chunk,chunkFromBytes: 這兩個函數從 reader 或直接從字節數組 bytes 中解析一個 MIDI 文件塊頭。

  5. fileHeader, fileHeaderFromBytes: 這兩個函數從 reader 或直接從字節數組 bytes 中解析一個 MIDI 文件頭。

  6. int: 從 reader 中解碼一個可變長度的整數。

  7. metaEvent: 從 reader 中解析一個 MIDI 元事件。

  8. trackEvent: 從 reader 中解析一個 MIDI 軌道事件。它可以是 MIDI 消息或元事件。

  9. file: 用於從 reader 解碼一個完整的 MIDI 文件。它首先解碼文件頭,然後解碼所有的文件塊。這個函數會返回一個表示 MIDI 文件的結構體。

message 解析

const status_byte = if (statusByte(first_byte.?)) |status_byte| blk: {
    first_byte = null;
    break :blk status_byte;
} else if (last_message) |m| blk: {
    if (m.channel() == null)
        return error.InvalidMessage;

    break :blk m.status;
} else return error.InvalidMessage;

這段代碼的目的是確定 MIDI 消息的狀態字節。它可以是從 reader 讀取的當前字節,或者是從前一個 MIDI 消息中獲取的。這樣做是爲了支持 MIDI 協議中的 “運行狀態”,在該協議中,連續的 MIDI 消息可能不會重複狀態字節。

  1. const status_byte = ...;: 這是一個常量聲明。status_byte 將保存 MIDI 消息的狀態字節。

  2. if (statusByte(first_byte.?)) |status_byte| blk: { ... }:

  1. else if (last_message) |m| blk: { ... }:
  1. else return error.InvalidMessage;: 如果 first_byte 不是狀態字節,並且不存在前一個消息,那麼返回一個 InvalidMessage 錯誤。

encode.zig

本文件用於將 MIDI 數據結構編碼爲其對應的二進制形式。具體來說,它是將內存中的 MIDI 數據結構轉換爲 MIDI 文件格式的二進制數據。

const midi = @import("../midi.zig");
const std = @import("std");

const debug = std.debug;
const io = std.io;
const math = std.math;
const mem = std.mem;

const encode = @This();

pub fn message(writer: anytype, last_message: ?midi.Message, msg: midi.Message) !void {
    if (msg.channel() == null or last_message == null or msg.status != last_message.?.status) {
        try writer.writeByte((1 << 7) | @as(u8, msg.status));
    }

    switch (msg.kind()) {
        .ExclusiveStart,
        .TuneRequest,
        .ExclusiveEnd,
        .TimingClock,
        .Start,
        .Continue,
        .Stop,
        .ActiveSensing,
        .Reset,
        .Undefined,
        => {},
        .ProgramChange,
        .ChannelPressure,
        .MidiTimeCodeQuarterFrame,
        .SongSelect,
        => {
            try writer.writeByte(msg.values[0]);
        },
        .NoteOff,
        .NoteOn,
        .PolyphonicKeyPressure,
        .ControlChange,
        .PitchBendChange,
        .SongPositionPointer,
        => {
            try writer.writeByte(msg.values[0]);
            try writer.writeByte(msg.values[1]);
        },
    }
}

pub fn chunkToBytes(_chunk: midi.file.Chunk) [8]u8 {
    var res: [8]u8 = undefined;
    mem.copy(u8, res[0..4], &_chunk.kind);
    mem.writeIntBig(u32, res[4..8], _chunk.len);
    return res;
}

pub fn fileHeaderToBytes(header: midi.file.Header) [14]u8 {
    var res: [14]u8 = undefined;
    mem.copy(u8, res[0..8], &chunkToBytes(header.chunk));
    mem.writeIntBig(u16, res[8..10], header.format);
    mem.writeIntBig(u16, res[10..12], header.tracks);
    mem.writeIntBig(u16, res[12..14], header.division);
    return res;
}

pub fn int(writer: anytype, i: u28) !void {
    var tmp = i;
    var is_first = true;
    var buf: [4]u8 = undefined;
    var fbs = io.fixedBufferStream(&buf).writer();

    // TODO: Can we find a way to not encode this in reverse order and then flipping the bytes?
    while (tmp != 0 or is_first) : (is_first = false) {
        fbs.writeByte(@as(u7, @truncate(tmp)) | (@as(u8, 1 << 7) * @intFromBool(!is_first))) catch
            unreachable;
        tmp >>= 7;
    }
    mem.reverse(u8, fbs.context.getWritten());
    try writer.writeAll(fbs.context.getWritten());
}

pub fn metaEvent(writer: anytype, event: midi.file.MetaEvent) !void {
    try writer.writeByte(event.kind_byte);
    try int(writer, event.len);
}

pub fn trackEvent(writer: anytype, last_event: ?midi.file.TrackEvent, event: midi.file.TrackEvent) !void {
    const last_midi_event = if (last_event) |e| switch (e.kind) {
        .MidiEvent => |m| m,
        .MetaEvent => null,
    } else null;

    try int(writer, event.delta_time);
    switch (event.kind) {
        .MetaEvent => |meta| {
            try writer.writeByte(0xFF);
            try metaEvent(writer, meta);
        },
        .MidiEvent => |msg| try message(writer, last_midi_event, msg),
    }
}

pub fn file(writer: anytype, f: midi.File) !void {
    try writer.writeAll(&encode.fileHeaderToBytes(.{
        .chunk = .{
            .kind = midi.file.Chunk.file_header.*,
            .len = @as(u32, @intCast(midi.file.Header.size + f.header_data.len)),
        },
        .format = f.format,
        .tracks = @as(u16, @intCast(f.chunks.len)),
        .division = f.division,
    }));
    try writer.writeAll(f.header_data);

    for (f.chunks) |c| {
        try writer.writeAll(&encode.chunkToBytes(.{
            .kind = c.kind,
            .len = @as(u32, @intCast(c.bytes.len)),
        }));
        try writer.writeAll(c.bytes);
    }
}

int 函數

pub fn int(writer: anytype, i: u28) !void {
    var tmp = i;
    var is_first = true;
    var buf: [4]u8 = undefined;
    var fbs = io.fixedBufferStream(&buf).writer();

    // TODO: Can we find a way to not encode this in reverse order and then flipping the bytes?
    while (tmp != 0 or is_first) : (is_first = false) {
        fbs.writeByte(@as(u7, @truncate(tmp)) | (@as(u8, 1 << 7) * @intFromBool(!is_first))) catch
            unreachable;
        tmp >>= 7;
    }
    mem.reverse(u8, fbs.context.getWritten());
    try writer.writeAll(fbs.context.getWritten());
}

這個函數int用於編碼一個整數爲 MIDI 文件中的可變長度整數格式。在 MIDI 文件中,許多值(如 delta 時間)使用這種可變長度編碼。

詳細地解析這個函數的每一步:

  1. 參數定義:
  1. 局部變量初始化:
  1. 循環進行可變長度編碼:
  1. 翻轉字節:
  1. 寫入結果:

file.zig

主要目的是爲了表示和處理 MIDI 文件的不同部分,以及提供了一個迭代器來遍歷 MIDI 軌道的事件。

const midi = @import("../midi.zig");
const std = @import("std");
const decode = @import("./decode.zig");

const io = std.io;
const mem = std.mem;

pub const Header = struct {
    chunk: Chunk,
    format: u16,
    tracks: u16,
    division: u16,

    pub const size = 6;
};

pub const Chunk = struct {
    kind: [4]u8,
    len: u32,

    pub const file_header = "MThd";
    pub const track_header = "MTrk";
};

pub const MetaEvent = struct {
    kind_byte: u8,
    len: u28,

    pub fn kind(event: MetaEvent) Kind {
        return switch (event.kind_byte) {
            0x00 => .SequenceNumber,
            0x01 => .TextEvent,
            0x02 => .CopyrightNotice,
            0x03 => .TrackName,
            0x04 => .InstrumentName,
            0x05 => .Luric,
            0x06 => .Marker,
            0x20 => .MidiChannelPrefix,
            0x2F => .EndOfTrack,
            0x51 => .SetTempo,
            0x54 => .SmpteOffset,
            0x58 => .TimeSignature,
            0x59 => .KeySignature,
            0x7F => .SequencerSpecificMetaEvent,
            else => .Undefined,
        };
    }

    pub const Kind = enum {
        Undefined,
        SequenceNumber,
        TextEvent,
        CopyrightNotice,
        TrackName,
        InstrumentName,
        Luric,
        Marker,
        CuePoint,
        MidiChannelPrefix,
        EndOfTrack,
        SetTempo,
        SmpteOffset,
        TimeSignature,
        KeySignature,
        SequencerSpecificMetaEvent,
    };
};

pub const TrackEvent = struct {
    delta_time: u28,
    kind: Kind,

    pub const Kind = union(enum) {
        MidiEvent: midi.Message,
        MetaEvent: MetaEvent,
    };
};

pub const File = struct {
    format: u16,
    division: u16,
    header_data: []const u8 = &[_]u8{},
    chunks: []const FileChunk = &[_]FileChunk{},

    pub const FileChunk = struct {
        kind: [4]u8,
        bytes: []const u8,
    };

    pub fn deinit(file: File, allocator: *mem.Allocator) void {
        for (file.chunks) |chunk|
            allocator.free(chunk.bytes);
        allocator.free(file.chunks);
        allocator.free(file.header_data);
    }
};

pub const TrackIterator = struct {
    stream: io.FixedBufferStream([]const u8),
    last_event: ?TrackEvent = null,

    pub fn init(bytes: []const u8) TrackIterator {
        return .{ .stream = io.fixedBufferStream(bytes) };
    }

    pub const Result = struct {
        event: TrackEvent,
        data: []const u8,
    };

    pub fn next(it: *TrackIterator) ?Result {
        const s = it.stream.inStream();
        var event = decode.trackEvent(s, it.last_event) catch return null;
        it.last_event = event;

        const start = it.stream.pos;

        var end: usize = switch (event.kind) {
            .MetaEvent => |meta_event| blk: {
                it.stream.pos += meta_event.len;
                break :blk it.stream.pos;
            },
            .MidiEvent => |midi_event| blk: {
                if (midi_event.kind() == .ExclusiveStart) {
                    while ((try s.readByte()) != 0xF7) {}
                    break :blk it.stream.pos - 1;
                }
                break :blk it.stream.pos;
            },
        };

        return Result{
            .event = event,
            .data = s.buffer[start..end],
        };
    }
};
  1. Header 結構:
  1. Chunk 結構:
  1. MetaEvent 結構:
  1. TrackEvent 結構:
  1. File 結構:
  1. TrackIterator 結構:

Build.zig

buid.zig 是一個 Zig 構建腳本(build.zig),用於配置和驅動 Zig 的構建過程。

const builtin = @import("builtin");
const std = @import("std");

const Builder = std.build.Builder;
const Mode = builtin.Mode;

pub fn build(b: *Builder) void {
    const test_all_step = b.step("test", "Run all tests in all modes.");
    inline for (@typeInfo(std.builtin.Mode).Enum.fields) |field| {
        const test_mode = @field(std.builtin.Mode, field.name);
        const mode_str = @tagName(test_mode);

        const tests = b.addTest("midi.zig");
        tests.setBuildMode(test_mode);
        tests.setNamePrefix(mode_str ++ " ");

        const test_step = b.step("test-" ++ mode_str, "Run all tests in " ++ mode_str ++ ".");
        test_step.dependOn(&tests.step);
        test_all_step.dependOn(test_step);
    }

    const example_step = b.step("examples", "Build examples");
    inline for ([_][]const u8{
        "midi_file_to_text_stream",
    }) |example_name| {
        const example = b.addExecutable(example_name, "example/" ++ example_name ++ ".zig");
        example.addPackagePath("midi", "midi.zig");
        example.install();
        example_step.dependOn(&example.step);
    }

    const all_step = b.step("all", "Build everything and runs all tests");
    all_step.dependOn(test_all_step);
    all_step.dependOn(example_step);

    b.default_step.dependOn(all_step);
}

這個 build 比較複雜,我們逐行來解析:

const test_all_step = b.step("test", "Run all tests in all modes.");
inline for (@typeInfo(std.builtin.Mode).Enum.fields) |field| {}
const example_step = b.step("examples", "Build examples");
const all_step = b.step("all", "Build everything and runs all tests");
all_step.dependOn(test_all_step);
all_step.dependOn(example_step);

b.default_step.dependOn(all_step);

參考資料

[1] 

zig-midi: https://github.com/Hejsil/zig-midi

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/xpYZJANSg9wsWaQm8FKBZg