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
後的字節:
-
0x00
: 序列號 (Sequence Number) -
0x01
: 文本事件 (Text Event) -
0x02
: 版權通知 (Copyright Notice) -
0x03
: 序列 / 曲目名稱 (Sequence/Track Name) -
0x04
: 樂器名稱 (Instrument Name) -
0x05
: 歌詞 (Lyric) -
0x06
: 標記 (Marker) -
0x07
: 註釋 (Cue Point) -
0x20
: MIDI Channel Prefix -
0x21
: End of Track (通常跟隨值0x00
,表示軌道的結束) -
0x2F
: Set Tempo (設定速度,即每分鐘的四分音符數) -
0x51
: SMPTE Offset -
0x54
: 拍號 (Time Signature) -
0x58
: 調號 (Key Signature) -
0x59
: Sequencer-Specific Meta-event
例如,當解析 MIDI 文件時,如果遇到字節 0xFF 0x03
,那麼接下來的字節將表示序列或曲目名稱。
在實際的 MIDI 文件中,元事件的具體結構是這樣的:
-
0xFF
: 元事件狀態字節。 -
元事件類型字節,例如上面列出的
0x00
,0x01
等。 -
長度字節(或一系列字節),表示該事件數據的長度。
-
事件數據本身。
元事件主要存在於 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 消息提供了基礎結構和函數。它包含三個字段:狀態、值和幾個公共方法。
-
kind 函數:根據 MIDI 消息的狀態碼確定消息的種類。
-
channel 函數:根據消息的種類返回 MIDI 通道,如果消息不包含通道信息則返回 null。
-
value 和 setValue 函數:用於獲取和設置 MIDI 消息的值字段。
-
Kind 枚舉:定義了 MIDI 消息的所有可能種類,包括通道事件和系統事件。
midi 消息結構
我們需要先了解 MIDI 消息的一些背景。
在 MIDI 協議中,某些消息的值可以跨越兩個 7 位的字節,這是因爲 MIDI 協議不使用每個字節的最高位(這通常被稱爲狀態位)。這意味着每個字節只使用它的低 7 位來攜帶數據。因此,當需要發送一個大於 7 位的值時(比如 14 位),它會被拆分成兩個 7 位的字節。
setValue
這個函數做的事情是將一個 14 位的值(u14
)拆分爲兩個 7 位的值,並將它們設置到 message.values
中。
以下是具體步驟的解釋:
-
獲取高 7 位:
v >> 7
把 14 位的值右移 7 位,這樣我們就得到了高 7 位的值。 -
截斷並轉換:
@truncate(v >> 7)
截斷高 7 位的值,確保它是 7 位的。@as(u7, @truncate(v >> 7))
確保這個值是u7
類型,即一個 7 位的無符號整數。 -
獲取低 7 位:
@truncate(v)
直接截斷原始值,保留低 7 位。 -
設置值:
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)
-
NoteOff:這是一個音符結束事件,表示某個音符不再播放。
-
NoteOn:這是一個音符開始事件,表示開始播放某個音符。
-
PolyphonicKeyPressure:多聲道鍵盤壓力事件,表示對特定音符的壓力或觸摸敏感度的變化。
-
ControlChange:控制變更事件,用於發送如音量、平衡等控制信號。
-
ProgramChange:程序(音色)變更事件,用於改變樂器的音色。
-
ChannelPressure:頻道壓力事件,與多聲道鍵盤壓力相似,但它適用於整個頻道,而不是特定音符。
-
PitchBendChange:音高彎曲變更事件,表示音符音高的上升或下降。
系統事件 (System events)
-
ExclusiveStart:獨佔開始事件,標誌着一個獨佔消息序列的開始。
-
MidiTimeCodeQuarterFrame:MIDI 時間碼四分之一幀,用於同步與其他設備。
-
SongPositionPointer:歌曲位置指針,指示序列器的當前播放位置。
-
SongSelect:歌曲選擇事件,用於選擇特定的歌曲或序列。
-
TuneRequest:調音請求事件,指示設備應進行自我調音。
-
ExclusiveEnd:獨佔結束事件,標誌着一個獨佔消息序列的結束。
-
TimingClock:計時時鐘事件,用於節奏的同步。
-
Start:開始事件,用於啓動序列播放。
-
Continue:繼續事件,用於繼續暫停的序列播放。
-
Stop:停止事件,用於停止序列播放。
-
ActiveSensing:活動感知事件,是一種心跳信號,表示設備仍然在線並工作。
-
Reset:重置事件,用於將設備重置爲其初始狀態。
其他
- 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(),
};
}
-
statusByte: 解析 MIDI 消息的首個字節,來確定是否這是一個狀態字節,還是一個數據字節。將一個字節 b 解碼爲一個 u7 類型的 MIDI 狀態字節,如果字節 b 不是一個狀態字節,則返回 null。換句話說,midi 的消息是 14 位,如果高 7 位不爲空,則是 midi 消息的狀態字節。在 MIDI 協議中,消息的首個字節通常是狀態字節,但也可能用之前的狀態字節(這稱爲 “運行狀態”)來解釋接下來的字節。因此,這段代碼需要確定它是否讀取了一個新的狀態字節,或者它是否應該使用前一個消息的狀態字節。
-
readDataByte: 從 reader 中讀取並返回一個數據字節。如果讀取的字節不符合數據字節的規定,則拋出 InvalidDataByte 錯誤。
-
message: 從 reader 讀取並解碼一個 MIDI 消息。如果讀取的字節不能形成一個有效的 MIDI 消息,則拋出 InvalidMessage 錯誤。這是一個複雜的函數,涉及到解析 MIDI 消息的不同種類。
-
chunk,chunkFromBytes: 這兩個函數從 reader 或直接從字節數組 bytes 中解析一個 MIDI 文件塊頭。
-
fileHeader, fileHeaderFromBytes: 這兩個函數從 reader 或直接從字節數組 bytes 中解析一個 MIDI 文件頭。
-
int: 從 reader 中解碼一個可變長度的整數。
-
metaEvent: 從 reader 中解析一個 MIDI 元事件。
-
trackEvent: 從 reader 中解析一個 MIDI 軌道事件。它可以是 MIDI 消息或元事件。
-
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 消息可能不會重複狀態字節。
-
const status_byte = ...;
: 這是一個常量聲明。status_byte
將保存 MIDI 消息的狀態字節。 -
if (statusByte(first_byte.?)) |status_byte| blk: { ... }
:
-
statusByte(first_byte.?)
: 這是一個函數調用,它檢查first_byte
是否是一個有效的狀態字節。.?
是可選值的語法,它用於解包first_byte
的值(它是一個可選的u8
,可以是u8
或null
)。 -
|status_byte|
: 如果statusByte
函數返回一個有效的狀態字節,則這個值會被捕獲並賦給這裏的status_byte
變量。 -
blk:
: 這是一個匿名代碼塊的標籤。Zig 允許你給代碼塊命名,這樣你可以從該代碼塊中跳出。 -
{ ... }
: 這是一個代碼塊。在這裏,first_byte
被設置爲null
,然後使用break :blk status_byte;
來結束此代碼塊,並將status_byte
的值賦給外部的status_byte
常量。
else if (last_message) |m| blk: { ... }
:
-
如果
first_byte
不是一個狀態字節,代碼會檢查是否存在一個名爲last_message
的前一個 MIDI 消息。 -
|m|
: 如果last_message
存在(即它不是null
),它的值將被捕獲並賦給m
。 -
{ ... }
: 這是另一個代碼塊。在這裏,它檢查m
是否有一個通道。如果沒有,則返回一個InvalidMessage
錯誤。否則,使用break :blk m.status;
結束此代碼塊,並將m.status
的值賦給外部的status_byte
常量。
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);
}
}
-
message 函數:這是將 MIDI 消息編碼爲字節序列的函數, 將單個 MIDI 消息編碼爲其二進制形式。根據消息類型,這會向提供的 writer 寫入一個或多個字節。若消息需要狀態字節,並且不同於前一個消息的狀態,函數會寫入狀態字節。接着,根據消息的種類,函數會寫入所需的數據字節。
-
chunkToBytes 函數:將 MIDI 文件的塊(Chunk)信息轉換爲 8 字節的二進制數據。這 8 字節中包括 4 字節的塊類型和 4 字節的塊長度。它複製塊類型到前 4 個字節,然後寫入塊的長度到後 4 個字節,並返回結果。
-
fileHeaderToBytes 函數:編碼 MIDI 文件的頭部爲 14 字節的二進制數據。這 14 字節包括塊信息、文件格式、軌道數量和時間劃分信息。
-
int 函數:將一個整數編碼爲 MIDI 文件中的可變長度整數格式。在 MIDI 文件中,某些整數值使用一種特殊的編碼格式,可以根據整數的大小變化長度。
-
metaEvent 函數:將 MIDI 元事件(Meta Event)編碼爲二進制數據, 這包括事件的類型和長度。具體則是編碼一個元事件,首先寫入其種類字節,然後是其長度。
-
trackEvent 函數:編碼軌道事件。軌道事件可以是元事件或 MIDI 事件,函數首先寫入事件之間的時間差(delta 時間),然後根據事件類型(MetaEvent 或 MidiEvent)編碼事件內容。
-
file 函數:這是主函數,用於將整個 MIDI 文件數據結構編碼爲其二進制形式。它首先編碼文件頭,然後循環編碼每個塊和塊中的事件。
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 時間)使用這種可變長度編碼。
詳細地解析這個函數的每一步:
- 參數定義:
-
writer
: 任意類型的寫入對象,通常是一種流或緩衝區,可以向其寫入數據。 -
i
: 一個最多 28 位的無符號整數(u28
),即要編碼的值。
- 局部變量初始化:
-
tmp
:作爲輸入整數的臨時副本。 -
is_first
:一個布爾值,用於指示當前處理的是否是整數的第一個字節。 -
buf
: 定義一個 4 字節的緩衝區。因爲最大的u28
值需要 4 個字節的可變長度編碼。 -
fbs
:使用io.fixedBufferStream
創建一個固定緩衝區的流,並獲取它的寫入器。
- 循環進行可變長度編碼:
-
循環條件是:直到
tmp
爲 0 並且不是第一個字節。 -
: (is_first = false)
是一個後置條件,每次循環結束後都會執行。 -
(@as(u8, 1 << 7) * @intFromBool(!is_first))
-
1 << 7
: 這個操作是左移操作。數字 1 在二進制中表示爲0000 0001
。當你將它左移 7 位時,你得到1000 0000
,這在十進制中等於128
。 -
@intFromBool(!is_first)
: 這是將上一步得到的布爾值轉換爲整數。在許多編程語言中,true 通常被視爲 1,false 被視爲 0。在 Zig 中,這種轉換不是隱式的,所以需要用@intFromBool()
函數來進行轉換 -
@as(u8, 1 << 7)
: 這裏是將數字 128(從 1 << 7 得到)顯式地轉換爲一個 8 位無符號整數。 -
(@as(u8, 1 << 7) * @intFromBool(!is_first))
: 將轉換後的數字 128 與從布爾轉換得到的整數(0 或 1)相乘。如果is_first
爲true
(即這是第一個字節),那麼整個表達式的值爲 0。如果is_first爲false
(即這不是第一個字節),那麼整個表達式的值爲 128(1000 0000
in 二進制)。 -
這種結構在 MIDI 變長值的編碼中很常見。MIDI 變長值的每個字節的最高位被用作 “繼續” 位,指示是否有更多的字節跟隨。如果最高位是 1,那麼表示還有更多的字節;如果是 0,表示這是最後一個字節。
-
在每次迭代中,它提取
tmp
的最後 7 位並將其編碼爲一個字節,最高位根據是否是第一個字節來設置(如果是第一個字節,則爲 0,否則爲 1)。 -
然後,整數右移 7 位,以處理下一個字節。
-
請注意,這種編碼方式實際上是從低字節到高字節的反向方式,所以接下來需要翻轉這些字節。
- 翻轉字節:
- 使用
mem.reverse
翻轉在固定緩衝區流中編碼的字節。這是因爲我們是以反序編碼它們的,現在我們要將它們放在正確的順序。
- 寫入結果:
- 使用提供的
writer
將翻轉後的字節寫入到目標位置。
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],
};
}
};
- Header 結構:
-
表示 MIDI 文件的頭部。
-
包含一個塊、格式、軌道數以及除法。
- Chunk 結構:
-
表示 MIDI 文件中的塊,每個塊有一個種類和長度。
-
定義了文件頭和軌道頭的常量。
- MetaEvent 結構:
-
表示 MIDI 的元事件。
-
它有一個種類字節和長度。
-
有一個函數,根據種類字節返回事件的種類。
-
定義了所有可能的元事件種類。
- TrackEvent 結構:
-
表示 MIDI 軌道中的事件。
-
它有一個 delta 時間和種類。
-
事件種類可以是 MIDI 事件或元事件。
- File 結構:
-
表示整個 MIDI 文件。
-
它有格式、除法、頭部數據和一系列塊。
-
定義了一個子結構 FileChunk,用於表示文件塊的種類和字節數據。
-
提供了一個清除方法來釋放文件的資源。
- TrackIterator 結構:
-
是一個迭代器,用於遍歷 MIDI 軌道的事件。
-
它使用一個 FixedBufferStream 來讀取事件。
-
定義了一個 Result 結構來返回事件和關聯的數據。
-
提供了一個
next
方法來讀取下一個事件。
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.");
- 使用 b.step() 方法定義了一個名爲 test 的步驟。描述是 “在所有模式下運行所有測試”。
inline for (@typeInfo(std.builtin.Mode).Enum.fields) |field| {}
-
Zig 有幾種構建模式,例如 Debug、ReleaseSafe 等, 上面則是爲每種構建模式生成測試.
-
這裏,@typeInfo() 函數獲取了一個類型的元信息。std.builtin.Mode 是 Zig 中定義的構建模式的枚舉。Enum.fields 獲取了這個枚舉的所有字段。
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);
- all_step 是一個彙總步驟,它依賴於之前定義的所有其他步驟。最後,b.default_step.dependOn(all_step); 確保當你僅僅執行 zig build(沒有指定步驟)時,all_step 會被執行。
參考資料
[1]
zig-midi: https://github.com/Hejsil/zig-midi
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/xpYZJANSg9wsWaQm8FKBZg