Generating MIDI files


#1

I’m trying to work out how to write a MIDI file but getting problems:

FileChooser chooser( "Select file to save",
    File::getCurrentWorkingDirectory(), "*.mid" );

if ( chooser.browseForFileToSave( true ) )
{
    MidiMessageSequence sequence;

    sequence.addEvent( MidiMessage( 0x90, 48, 96,  0.0 ) );
    sequence.addEvent( MidiMessage( 0x80, 48,  0, 24.0 ) );

    sequence.updateMatchedPairs();

    MidiFile file;

    file.setTicksPerQuarterNote( 96 );

    FileOutputStream stream( chooser.getResult() );

    file.addTrack( sequence );

    file.writeTo( stream );
}

It doesn’t seem to work - what it outputs doesn’t seem to be a valid MIDI file.

:?:


#2

I did try this as well:


sequence.addEvent( MidiMessage::noteOn( 1, 48, 96 ), 0.0 );
sequence.addEvent( MidiMessage::noteOff( 1, 48 ), 24.0 );

#3

I assume you flushed and/or deleted the stream, right? Otherwise you’d have a half-written file…


#4

Didn’t make any difference.


#5

Ok, well it’s been a while since I used that class, so my memory’s a bit flakey, but tracktion uses it to create midi files, so it must work.

Did you try setting the ticks and time format stuff?


#6

Sorry, yes, I see that you did set the ticks… I’m a bit puzzled then. What’s the output like? Can the MidiFile class load its own output back in ok?


#7

It can read it back - however it doesn’t match what was written. I did the following:

FileChooser chooser( T("Select file to open"),
    File::getCurrentWorkingDirectory(), "*.mid" );

if ( chooser.browseForFileToOpen( ) )
{
    MidiFile file;
    FileInputStream stream( chooser.getResult() );

    file.readFrom( stream );

    for ( int i = 0; i < file.getNumTracks(); i++ )
    {
        const MidiMessageSequence *sequence = file.getTrack( i );

        for ( int j = 0; j < sequence->getNumEvents(); j++ )
       {
            MidiMessageSequence::MidiEventHolder *holder = 
                sequence->getEventPointer( j );

            Logger::outputDebugPrintf( T("Message: %d %d %d %f"),
                holder->message.getChannel(),
                holder->message.getNoteNumber(),
                holder->message.getVelocity(),
                holder->message.getTimeStamp() );
       }
    }
}

The results are:

Message: 1 48 0 0.000000
Message: 0 47 0 0.000000
Message: 1 48 127 0.000000

They appear to be in the wrong order and with the wrong velocity - I wrote out 96, it comes back as 127? Also, the timestamp is zero.


#8

The velocity parameter is floating point, not a byte.

I’m too snowed under to debug this at the moment - I’ll take a look when I eventually get time, but it can’t be too hard to trace what’s happening if you want to take a look.


#9

I’ll take a look. I’m a bit baffled by the timestamp at the moment. I’m used to writing out delta times (yuk!) so I can’t quite figure out how timestamp works in Juce.

Loading the MIDI file into FLStudio, it can see one note with zero length and velocity of 127, which seems consistent with what I read back.


#10

I’m using GNMIDI tools to examine the output I’m getting:

mid2txt shows the following:

MThd 00000006
  0001 0001 E728  // format, trackcount, resolution

MTrk 0000000C
0016: 00        90  30 7F // note on
001A: 00        80  30 00 // note off
001E: 00        FF  2F 00  // end of track

If I use another MIDI editor I get the following:

MTrk 00000017
0044: 00        FF  03 04  74 65 73 74 // trackname "test"
004C: 00        C0  00 // program
004F: 00        90  60 64 // note on
0053: 08        80  60 00 // note off
0057: 00        FF  2F 00  // end of track

The important bits are interesting - juce is writing

90 30 7F

as the first note, which is wrong - I didn’t select 7F as the velocity.

If I dump the MidiMessage as a trace statement, I get:

90 30 58

which is more like it.

The second note looks correct:

80 30 00

What’s wrong is the timestamp of 00, which means the note is of zero duration.

I’ll keep looking into this.


#11

Ok, it’s Visual Studio Express

This is the result if I move the code onto my MacBook:

MThd 00000006
  0001 0001 0060  // format, trackcount, resolution

MTrk 0000000C
0016: 00        90  30 58 // note on
001A: 18        80  30 00 // note off
001E: 00        FF  2F 00  // end of track

It would appear that somehow Visual Studio C++ Express is mangling the data?


#12

Don’t think VCExpress could be to blame… Like I said above, did you check that the velocity param is a float, not an integer?


#13

I tried this with VC 2005 Pro:

MidiMessageSequence sequence;

sequence.addEvent( MidiMessage::noteOn( 1, 48, 0.7f ), 0.0 );
sequence.addEvent( MidiMessage::noteOff( 1, 48 ), 24.0 );

FileChooser chooser( T("Select MIDI file to save"),
    File::getCurrentWorkingDirectory(), T("*.mid") );

if ( chooser.browseForFileToSave( true ) )
{
    MidiFile file;

    FileOutputStream stream( chooser.getResult() );

    file.setTicksPerQuarterNote( 96 );

    file.addTrack( sequence );

    file.writeTo( stream );
}

Same result, note of zero duration and velocity 127 - I did use 0.7 in the above example.


#14

Combining the snip-its of code from above, but changing the MIDI slightly (now it’s nonsensical and non-musical):
i.e.

		{//hacky test
		MidiMessageSequence sequence; 

		sequence.addEvent( MidiMessage::noteOn( 1, 38, (juce::uint8)0x3f ), 0.0 );
		sequence.addEvent( MidiMessage::noteOn( 1, 38, (juce::uint8)0x3f ), 0.0 );
			sequence.addEvent( MidiMessage::noteOn( 2, 39, (juce::uint8)0x7f ), 96.0 );
			sequence.addEvent( MidiMessage::noteOn( 2, 39, (juce::uint8)0x7f ), 96.0 );
			sequence.addEvent( MidiMessage::noteOff( 3, 40 ), 124.0 );
			sequence.addEvent( MidiMessage::noteOff( 3, 40 ), 124.0 );

		FileChooser chooser( T("Select MIDI file to save"), File::getCurrentWorkingDirectory(), T("*.mid") ); 

			if ( chooser.browseForFileToSave( true ) ) 
			{ 
			MidiFile file; 

				chooser.getResult().File::deleteFile();
				FileOutputStream stream( chooser.getResult() );
				//stream.setPosition(0);

				file.setTicksPerQuarterNote( 96 ); 

				file.addTrack( sequence ); 

				file.writeTo( stream ); 

				//stream.flush();
			} 	
			{
		    MidiFile file; 
			FileInputStream stream( chooser.getResult() ); 

			    file.readFrom( stream ); 

				for ( int i = 0; i < file.getNumTracks(); i++ ) 
				{ 
					const MidiMessageSequence *sequence = file.getTrack( i ); 

					for ( int j = 0; j < sequence->getNumEvents(); j++ ) 
					{ 
						MidiMessageSequence::MidiEventHolder *holder = 
							sequence->getEventPointer( j ); 

						Logger::outputDebugPrintf( T("Message: chn:%d Note:%d Vel:%d T:%f"), 
														holder->message.getChannel(), 
														holder->message.getNoteNumber(), 
														holder->message.getVelocity(), 
														holder->message.getTimeStamp() ); 
					} 
				} 
			}
			
			
		}//Hacky test

Here’s the output I get: (same on both XP and OSX machines), which leaves me thinking there’s an OBOE somewhere!

[quote]Message: chn:1 Note:38 Vel:63 T:0.000000
Message: chn:1 Note:38 Vel:0 T:0.000000
Message: chn:1 Note:38 Vel:63 T:0.000000
Message: chn:2 Note:39 Vel:127 T:96.000000
Message: chn:2 Note:39 Vel:0 T:96.000000
Message: chn:2 Note:39 Vel:127 T:96.000000
Message: chn:3 Note:40 Vel:0 T:124.000000
Message: chn:0 Note:47 Vel:0 T:124.000000
Message: chn:3 Note:40 Vel:0 T:124.000000[/quote]

Those bolded items are interesting…

I haven’t had a chance to step through everything, but that’s where I’m off to next…

EDIT Thurs PM

A cruel, but valid test tonight… (only tested on Windows)
Creating an empty track, and adding that, saving and reloading it generates one “Note”

which is the endOfTrack Message. Which is OK.

but I don’t understand this line (in void MidiFile::readNextTrack)

[quote] // use a sort that puts all the note-offs before note-ons that have the same time
result.list.sort (*this);[/quote]

But that seems that the endOfTrack’s cropping up in odd places (IMHO) -

Should the comparator ignore comparisons (return 0) ie either message is an endOfTrack message, or something?

Actually, that might be wrong- I tried removing the sort, and I got this:
(after I added debugs to writeTrack/nextTrack)

[quote]MidiFile::writeTrack - Message: chn:1 Note:38 Vel:63 T:0.000000
MidiFile::writeTrack - Message: chn:1 Note:38 Vel:63 T:1.000000
MidiFile::writeTrack - Message: chn:2 Note:39 Vel:127 T:96.000000
MidiFile::writeTrack - Message: chn:2 Note:39 Vel:127 T:97.000000
MidiFile::writeTrack - Message: chn:3 Note:40 Vel:0 T:124.000000
MidiFile::writeTrack - Message: chn:3 Note:40 Vel:0 T:125.000000
MidiFile:readNextTrack - Message: chn:1 Note:38 Vel:63 T:0.000000
MidiFile:readNextTrack - Message: chn:1 Note:38 Vel:63 T:1.000000
MidiFile:readNextTrack - Message: chn:2 Note:39 Vel:127 T:96.000000
MidiFile:readNextTrack - Message: chn:2 Note:39 Vel:127 T:97.000000
MidiFile:readNextTrack - Message: chn:3 Note:40 Vel:0 T:124.000000
MidiFile:readNextTrack - Message: chn:3 Note:40 Vel:0 T:125.000000
MidiFile:readNextTrack - Message: chn:0 Note:47 Vel:0 T:125.000000
final Message: chn:1 Note:38 Vel:63 T:0.000000
final Message: chn:1 Note:38 Vel:0 T:1.000000
final Message: chn:1 Note:38 Vel:63 T:1.000000
final Message: chn:2 Note:39 Vel:127 T:96.000000
final Message: chn:2 Note:39 Vel:0 T:97.000000
final Message: chn:2 Note:39 Vel:127 T:97.000000
final Message: chn:3 Note:40 Vel:0 T:124.000000
final Message: chn:3 Note:40 Vel:0 T:125.000000
final Message: chn:0 Note:47 Vel:0 T:125.000000[/quote]

It looks like it’s happening after the MidiFile is loaded (even without the sort in place, I think)

Additional: Running the above on OSX somehow manages to pop-up 2 “File Open” windows?


#15

Why not try it with a sequence that doesn’t leave midi notes left on? It’s probably putting in the extra note-offs to stop the hanging notes that are there.


#16

Actually, just looked at it again, and the extra note-offs are perfectly legitimate, because it adds a note-off before a repeated note-on if there’s not already one there. Nothing wrong with that.

The sorting callback is correctly returning 0 for the end-of-track, but I’m not sure if I trust the sort method to leave the order unchanged. Hmm. Will have to look at that one.


#17

In the 718 version, MidiFile.writeTo is writing an extra 0xFF 0x2f 0x00 0x00 before writing the normal 0xFF 0x2F 0x00. That’s an extra MIDI end of track with a null byte separating them. MidiFile.readFrom reads the resulting file ok as does Tracktion, but Cakewalk Pro Audio 9 doesn’t.

Commenting out these lines in MidiFile::writeTrack corrects the problem:

out.writeByte (0); const MidiMessage m (MidiMessage::endOfTrack()); out.write (m.getRawData(), m.getRawDataSize());


#18

Although I can’t remember writing that, it seems strange that I’d have put it in there without a good reason, and tracktion will have been using that code to pump out midi files for years… Are you sure that without those lines the files still load in a range of hosts?


#19

Coming back to this (after a few years), I still couldn't get it to work:


    MidiFile midiFile;
    midiFile.setTicksPerQuarterNote(96);
    createEmptyTrack(midiFile, 0.7f);
    createTrack(midiFile, 1);
    createTrack(midiFile, 2);
    createTrack(midiFile, 10);
    File file("D:\\tools\\midi2txt\\test.mid");
    FileOutputStream stream(file);
    midiFile.writeTo(stream);

I got an emtpy midi file with garbage at the end of the file (according to the GNMIDI tool MIDI2TXT). So I started debugging, and realised that:

File file("D:\\tools\\midi2txt\\test.mid");
FileOutputStream stream(file);
midiFile.writeTo(stream);

has a problem. FileOutputStream keeps writing to the end of the file. What's missing is stream.setPosition(0) to ensure the file is written correctly, and new data is not just appended to the end of the file.

So corrected it looks like the following:


    MidiFile midiFile;
    midiFile.setTicksPerQuarterNote(96);
    createEmptyTrack(midiFile, 0.7f);
    createTrack(midiFile, 1);
    createTrack(midiFile, 2);
    createTrack(midiFile, 10);
    File file("D:\\tools\\midi2txt\\test.mid");
    FileOutputStream stream(file);
    stream.setPosition(0);    // <-- here lies dragons!
    midiFile.writeTo(stream);


#20

I think you probably want to delete the file first.. Just skipping back to the start won't truncate it, so you could end up leaving garbage at the end.