MidiFile format accessor needed (if possible)


#1

Hi Jules,

Your class MidiFile is clean, robust and easy to use. Perfect or quasi-perfect. Quasi because you don't allow to access to a particular value that is the MidiFile format. The value just in between the data length and the number of tracks in the header. 

short : 00 00 = Format 0 (Monotrack)
short : 00 01 = Format 1 (Multitrack)
short : 00 02 = Format 2 (Monotrack multi-pattern)

Although the format 2 is not really used for standard midi song, and even if the number of tracks and playback of content to determine if we are dealing with a MIDI file format 0 or 1, you should allow to read and write this value. Would be only for browsing the header file for a library, see where you can specify the format 0 if necessary. It seems to me that you record format 1 regardless of the number of tracks.

What do you think?

Friendly,

Max

 


#2

I don't have time to do the necessary research on the different formats to implement that, but am happy to consider any changes you want to suggest!


#3

Jules,

You don't need any investigation at this level. The Midi File format is just a short value (16 bits in big endian) that you put at 1 by default for all the case. Your class don't need change this is just a header value used to identify the format implemented (1 by default is multitrack format).

By convention, in format 1, the first track you put in the midi file is a free-of-note track that include the tempo, time signature, key signature, section, and other master information of the song. All other tracks are dedicated to the song tracks, always by convention, one instrument by track, one channel by track (but you can multiply the program change and channel change inside a track, this is just a convention)

The Midi File format 0, is a single track midi file that include all the events of all "song tracks" mixed together and with all the tempo, time, key, ... informations. So, your class is already able to generate Midi File in format 0 and 1.

The Midi File format 2, is a single track where you put multiple pattern. This is a specific format used by drum machine for example to generate multiple partial sequences that can be played with a playlist. This is not really a used format, all the midi files you can find are in Format 1 and 0.

Your class don't need to manage the difference between 0, 1 or 2, but you need to allow the access to the value you write here :

bool MidiFile::writeTo (OutputStream& out)
{
    out.writeIntBigEndian ((int) ByteOrder::bigEndianInt ("MThd"));
    out.writeIntBigEndian (6);
    out.writeShortBigEndian (1); // type
    out.writeShortBigEndian ((short) tracks.size());
    out.writeShortBigEndian (timeFormat);
    for (int i = 0; i < tracks.size(); ++i)
        writeTrack (out, i);
    out.flush();
    return true;
}

The class need just a new variable "fileType" that have "1" as default value, and two accessors (set/get) :

private:
    //==============================================================================
    OwnedArray<MidiMessageSequence> tracks;
    short timeFormat;
    short fileType;

    void readNextTrack (const uint8*, int size);
    void writeTrack (OutputStream&, int trackNum);
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiFile)
  /** Returns the midi file format.

         Values can be 0, 1 or 2 (default 1).

         General Midi specification :

             Midifile Format 0 : monotrack
             Midifile Format 1 : multitrack
             Midifile Format 2 : patterns (monotrack)
    */

    short getFileType () const noexcept;

    void setFileType ( short fileType ) noexcept;

You already read the fileType (fileFormat) :

static bool parseMidiHeader (const uint8* &data, short& timeFormat, short& fileType, short& numberOfTracks) noexcept
    {
        unsigned int ch = ByteOrder::bigEndianInt (data);
        data += 4;
        if (ch != ByteOrder::bigEndianInt ("MThd"))
        {
            bool ok = false;
            if (ch == ByteOrder::bigEndianInt ("RIFF"))
            {
                for (int i = 0; i < 8; ++i)
                {
                    ch = ByteOrder::bigEndianInt (data);
                    data += 4;
                    if (ch == ByteOrder::bigEndianInt ("MThd"))
                    {
                        ok = true;
                        break;
                    }
                }
            }
            if (! ok)
                return false;
        }
        unsigned int bytesRemaining = ByteOrder::bigEndianInt (data);
        data += 4;
        fileType = (short) ByteOrder::bigEndianShort (data);
        data += 2;
        numberOfTracks = (short) ByteOrder::bigEndianShort (data);
        data += 2;
        timeFormat = (short) ByteOrder::bigEndianShort (data);
        data += 2;
        bytesRemaining -= 6;
        data += bytesRemaining;
        return true;
    }

You just need to allow access to this value (read), and modify the previous method writeTo :

bool MidiFile::writeTo (OutputStream& out)
{
    out.writeIntBigEndian ((int) ByteOrder::bigEndianInt ("MThd"));
    out.writeIntBigEndian (6);
    out.writeShortBigEndian (fileType);
    out.writeShortBigEndian ((short) tracks.size());
    out.writeShortBigEndian (timeFormat);
    for (int i = 0; i < tracks.size(); ++i)
        writeTrack (out, i);
    out.flush();
    return true;
}

That's all, this is the user of the this class that is responsable to the integrity of is midi file. In all case, with a 1 as default value, the use of the class stay unchanged and the final user will be not constrain by this change.

All my informations come's froms the legendary book "Maximum MIDI" by Paul Messick (subtitle Music Applications in C++) (ISBN : 1-884777-44-1) - http://www.manning.com/messick/

Do you think you'll we able to do that ? If not, and for information for other users that want to generate midi file format 0. You can just write a file in midi file format 0 style with the Juce class and hack the file like that :

    FileOutputStream stream (midiFile);
    stream->setPosition (9);
    stream->writeByte ( 0, 1 or 2 ); 

This is a non-elegant way to modify the format by putting a byte in the half part of the short :

position 8 : always 0
position 9 : 0, 1 or 2

Thank's for all Jules !

Max

 


#4

I personally just modified the JUCE code to:

    /** Writes the midi tracks as a standard midi file.

        @returns true if the operation succeeded.
    */
    bool writeTo (OutputStream& destStream, int iType = 1);
//==============================================================================
bool MidiFile::writeTo (OutputStream& out, int iType)
{
    out.writeIntBigEndian ((int) ByteOrder::bigEndianInt ("MThd"));
    out.writeIntBigEndian (6);
    out.writeShortBigEndian (iType); // type
    out.writeShortBigEndian ((short) tracks.size());
    out.writeShortBigEndian (timeFormat);

    for (int i = 0; i < tracks.size(); ++i)
        writeTrack (out, i);

    out.flush();
    return true;
}

Rail


#5

I see that I am not alone in this case. Ideally and as Format 0 is fairly widespread, it would be interesting to have an "official" access to this variable.

Thank's for sharing Rail Jon Rogut !!!


#6

But surely if the format number is different, the data itself would have to be written differently?


#7

The data is formatted/written the same… but there would be limitations to the number of tracks allowed. I’ve had no problem using the change I made above and ensuring the track count is correct myself.

From the spec:

Format Type

The first word describes the MIDI format type. It can be a value of 0, 1 or 2 and describes what how the following track information is to be interpreted. A type 0 MIDI file has one track that contains all of the MIDI events for the entire song, including the song title, time signature, tempo and music events. A type 1 MIDI file should have two or more tracks. The first, by convention, contains song information such as the title, time signature, tempo, etc. (more detail in Track Chunk section). The second and following tracks contain a title, musical event data, etc. specific to that track. This closely matches the organization of modern multi-track MIDI sequencers. A type 2 MIDI file is sort of a combination of the other two types. It contains multiple tracks, but each track represents a different sequence which may not necessarily be played simultaneously. This is meant to be used to save drum patterns, or other multi-pattern music sequences.

Cheers,

Rail


#8

No, this is just a convention. If you specify a value format of 0 (midi file format 0) the software or hardware that will read the file will think that's a conventional one track midi file, multitrack if the value is 1.


Technically, there's no difference. This is the same way to write the track in the stream.


The main interest to have access to this value is to directly know if the file is a mono track or a multi-track. Because you can have a Midi File format 1 that have one track (a solo guitar for example) that is not a proof that is a single instrument track (1) or a multi-instrument track (0). 


The only way to know if this is a midi file 0 or 1 is to read the content of the single track and determine if there's more than one instrument inside. So, for a librarian tool that read directory of midi file, the best way is to use the Midi File format to know quickly the true content.


I don't know if my english is understandable. Let me know if you've doubt about that.


#9

Ok, gotcha, will add that.


#10

Thank's jules !!!