【问题标题】:Broken MIDI File Output损坏的 MIDI 文件输出
【发布时间】:2019-02-02 16:45:36
【问题描述】:

我目前正在尝试实现我自己的单轨 MIDI 文件输出。它将存储在多个帧中的 8x8 颜色网格转换为 MIDI 文件,该文件可以导入数字音频接口并通过 Novation Launchpad 播放。更多上下文here

我设法输出了一个程序识别为 MIDI 的文件,但生成的 MIDI 无法播放,并且它与通过相同帧数据生成的文件不匹配。我一直在通过专门的 MIDI 程序记录我的程序实时 MIDI 消息,然后通过它吐出一个 MIDI 文件来进行比较。然后,我通过十六进制编辑器将生成的文件与正确生成的文件进行比较。就标题而言,事情是正确的,但似乎就是这样。

我一直在为 MIDI 规范的多个版本和现有的 Stack Overflow 问题苦恼,但没有 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);
                }
            }
        }
    }
}

【问题讨论】:

  • 你能发布你的“FrameData”课程吗?
  • FrameData 只是一个时间戳和一个颜色数组。无论如何,我在一年前就放弃了。
  • 是的,我知道对不起。仅仅因为我正在尝试编写保存 MIDI 文件的代码(尽管与您的不同,我使用的是实际的音符:p)

标签: c# midi


【解决方案1】:
  • BitConverter.GetBytes 以本机字节顺序返回字节,但 MIDI 文件使用大端值。如果您在 x86 或 ARM 上运行,则必须反转字节。
  • 文件头中的第三个值不叫“增量时间”;它是每个四分音符的节拍数,您已经拥有 ppq
  • 轨道长度不是12;你必须写出实际的长度。 由于增量时间的可变长度编码(见下文),这通常在收集轨道的所有字节之前是不可能的。
  • 您需要编写一个速度元事件来指定每个四分音符的微秒数。
  • 增量时间不是绝对时间;它指定从上一个事件的时间开始的间隔。
  • 增量时间指定滴答数;你的计算是错误的。 使用TimeStamp * bpm * ppq / 60000
  • 增量时间不存储为double 浮点数,而是作为可变长度量;该规范有对其进行编码的示例代码。
  • 轨道的最后一个事件必须是轨道结束元事件。

【讨论】:

  • 因此,对于每个时间戳,我必须计算当前帧和前一帧之间的差异,因为时间戳是绝对毫秒位置。然后,将结果值乘以“60000 * bpm * ppq”得到间隔。对吗?
【解决方案2】:

另一种方法是使用其中一个 .NET MIDI 库来编写 MIDI 文件。您只需将帧转换为 Midi 对象并将它们传递到库进行保存。该库将处理所有 MIDI 细节。

你可以试试MIDI.NETC# Midi Toolkit。不确定NAudio 是否确实在编写 MIDI 文件以及在什么抽象级别......

以下是有关 MIDI 文件格式规范的更多信息: http://www.blitter.com/~russtopia/MIDI/~jglatt/tech/midifile.htm

希望对你有帮助, 马克

【讨论】:

    猜你喜欢
    • 2021-11-06
    • 2020-05-19
    • 1970-01-01
    • 2020-01-17
    • 2016-03-01
    • 2022-06-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多