Broken MIDI File Output

I'm currently trying to implement my own single track MIDI file output. It turns an 8x8 grid of colours stored in multiple frames, into a MIDI file that can be imported into a digital audio interface and played through a Novation Launchpad. Some more context here.

I've managed to output a file that programs recognize as MIDI, but the resultant MIDI does not play, and its not matching files generated via the same frame data. I've been doing comparisions by recording my programs live MIDI messages through a dedicated MIDI program, and then spitting out a MIDI file via that. I then compare my generated file to that properly generated file via a hex editor. Things are correct as far as the headers, but that seems to be it.

I've been slaving over multiple renditions of the MIDI specification and existing Stack Overflow questions with no 100% solution.

Here is my code, based on what I have researched. I can't help but feel I'm missing something simple. I'm avoiding the use of existing MIDI libraries, as I only need this one MIDI function to work (and want the learning experience of doing this from scratch). Any guidance would be very helpful.

/// <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);
                }
            }
        }
    }
}

Upvotes: 2

Views: 510

Answers (2)

CL.
CL.

Reputation: 180080

  • BitConverter.GetBytes returns the bytes in the native byte order, but MIDI files use big-endian values. If you're running on x86 or ARM, you must reverse the bytes.
  • The third value in the file header is not called "delta time"; it is the number of ticks per quarter note, which you already have as ppq.
  • The length of the track is not 12; you must write the actual length. Due to the variable-length encoding of delta times (see below), this is usually not possible before collecting all bytes of the track.
  • You need to write a tempo meta event that specifies the number of microseconds per quarter note.
  • A delta time is not an absolute time; it specifies the interval starting from the time of the previous event.
  • A delta time specifies the number ticks; your calculation is wrong. Use TimeStamp * bpm * ppq / 60000.
  • Delta times are not stored as a double floating-point number but as a variable-length quantity; the specification has example code for encoding it.
  • The last event of the track must be an end-of-track meta event.

Upvotes: 2

obiwanjacobi
obiwanjacobi

Reputation: 2463

Another approach would be to use one of the .NET MIDI libraries to write the MIDI file. You just have to convert your frames into Midi objects and pass them to the library to save. The library will take care of all the MIDI details.

You could try MIDI.NET and C# Midi Toolkit. Not sure if NAudio does writing MIDI files and at what abstraction level that is...

Here is more info on the MIDI File Format specification: http://www.blitter.com/~russtopia/MIDI/~jglatt/tech/midifile.htm

Hope it helps, Marc

Upvotes: 0

Related Questions