损坏的MIDI文件输出
本文关键字:输出 文件 MIDI 损坏 | 更新日期: 2023-09-27 18:28:36
我目前正在尝试实现我自己的音轨MIDI文件输出。它将存储在多个帧中的8x8颜色网格转换为MIDI文件,该文件可以导入数字音频接口并通过Novation Launchpad播放。这里有更多的上下文。
我已经设法输出了一个程序识别为MIDI的文件,但生成的MIDI不会播放,而且它与通过相同帧数据生成的文件不匹配。我一直在做比较,通过一个专用的MIDI程序录制我的程序实时MIDI消息,然后通过它吐出一个MIDI文件。然后,我通过十六进制编辑器将生成的文件与正确生成的文件进行比较。就标题而言,情况是正确的,但似乎就是这样
我一直在为MIDI规范的多个版本和现有的堆栈溢出问题而苦恼,但没有100%的解决方案。
这是我的代码,基于我的研究。我忍不住觉得我错过了一些简单的东西。我避免使用现有的MIDI库,因为我只需要这一个MIDI函数就可以工作(并且希望获得从头开始的学习体验)。任何指导都将非常有帮助。
/// <summary>
/// Outputs an MIDI file based on frames for the Novation Launchpad.
/// </summary>
/// <param name="filename"></param>
/// <param name="frameData"></param>
/// <param name="bpm"></param>
/// <param name="ppq"></param>
public static void WriteMidi(string filename, List<FrameData> frameData, int bpm, int ppq) {
decimal totalLength = 0;
using (FileStream stream = new FileStream(filename, FileMode.Create, FileAccess.Write)) {
// Output midi file header
stream.WriteByte(77);
stream.WriteByte(84);
stream.WriteByte(104);
stream.WriteByte(100);
for (int i = 0; i < 3; i++) {
stream.WriteByte(0);
}
stream.WriteByte(6);
// Set the track mode
byte[] trackMode = BitConverter.GetBytes(Convert.ToInt16(0));
stream.Write(trackMode, 0, trackMode.Length);
// Set the track amount
byte[] trackAmount = BitConverter.GetBytes(Convert.ToInt16(1));
stream.Write(trackAmount, 0, trackAmount.Length);
// Set the delta time
byte[] deltaTime = BitConverter.GetBytes(Convert.ToInt16(60000 / (bpm * ppq)));
stream.Write(deltaTime, 0, deltaTime.Length);
// Output track header
stream.WriteByte(77);
stream.WriteByte(84);
stream.WriteByte(114);
stream.WriteByte(107);
for (int i = 0; i < 3; i++) {
stream.WriteByte(0);
}
stream.WriteByte(12);
// Get our total byte length for this track. All colour arrays are the same length in the FrameData class.
byte[] bytes = BitConverter.GetBytes(frameData.Count * frameData[0].Colours.Count * 6);
// Write our byte length to the midi file.
stream.Write(bytes, 0, bytes.Length);
// Cycle through frames and output the necessary MIDI.
foreach (FrameData frame in frameData) {
// Calculate our relative delta for this frame. Frames are originally stored in milliseconds.
byte[] delta = BitConverter.GetBytes((double) frame.TimeStamp / 60000 / (bpm * ppq));
for (int i = 0; i < frame.Colours.Count; i++) {
// Output the delta length to MIDI file.
stream.Write(delta, 0, delta.Length);
// Get the respective MIDI note based on the colours array index.
byte note = (byte) NoteIdentifier.GetIntFromNote(NoteIdentifier.GetNoteFromPosition(i));
// Check if the current color signals a MIDI off event.
if (!CheckEqualColor(frame.Colours[i], Color.Black) && !CheckEqualColor(frame.Colours[i], Color.Gray) && !CheckEqualColor(frame.Colours[i], Color.Purple)) {
// Signal a MIDI on event.
stream.WriteByte(144);
// Write the current note.
stream.WriteByte(note);
// Check colour and write the respective velocity.
if (CheckEqualColor(frame.Colours[i], Color.Red)) {
stream.WriteByte(7);
} else if (CheckEqualColor(frame.Colours[i], Color.Orange)) {
stream.WriteByte(83);
} else if (CheckEqualColor(frame.Colours[i], Color.Green) || CheckEqualColor(frame.Colours[i], Color.Aqua) || CheckEqualColor(frame.Colours[i], Color.Blue)) {
stream.WriteByte(124);
} else if (CheckEqualColor(frame.Colours[i], Color.Yellow)) {
stream.WriteByte(127);
}
} else {
// Calculate the delta that the frame had.
byte[] offDelta = BitConverter.GetBytes((double) (frameData[frame.Index - 1].TimeStamp / 60000 / (bpm * ppq)));
// Write the delta to MIDI.
stream.Write(offDelta, 0, offDelta.Length);
// Signal a MIDI off event.
stream.WriteByte(128);
// Write the current note.
stream.WriteByte(note);
// No need to set our velocity to anything.
stream.WriteByte(0);
}
}
}
}
}
BitConverter.GetBytes
按本机字节顺序返回字节,但MIDI文件使用big-endian值。如果您在x86或ARM上运行,则必须反转字节- 文件头中的第三个值不称为"增量时间";它是每个季度音符的节拍数,您已经将其作为
ppq
- 轨道的长度不是
12
;您必须写入实际长度。由于delta时间的可变长度编码(见下文),在收集轨道的所有字节之前,这通常是不可能的 - 您需要编写一个节奏元事件,指定每个季度音符的微秒数
- 增量时间不是绝对时间;它指定从上一个事件开始的时间间隔
- 增量时间指定刻度数;你的计算是错误的。使用
TimeStamp * bpm * ppq / 60000
- 增量时间不是作为
double
浮点数存储的,而是作为可变长度量存储的;规范中有用于对其进行编码的示例代码 - 曲目的最后一个事件必须是曲目结束元事件
另一种方法是使用一个.NET MIDI库来编写MIDI文件。您只需要将帧转换为Midi对象,然后将它们传递到库中进行保存。库将处理所有MIDI细节。
你可以试试MIDI.NET和C#MIDI Toolkit。不确定NAudio是否编写MIDI文件,以及在什么抽象级别…
以下是有关MIDI文件格式规范的更多信息:http://www.blitter.com/~russtopia/MIDI/~jglatt/tech/medifile.htm
希望能有所帮助,Marc