![]() |
This is a prototype as I couldn't get things working/sounding right. This is due to two main reasons:
A) I couldn't work out how to select instruments.
B) For some reason when the WAVEFORMAT_MIDI_MESSAGE is dynamicly allocated, waveOutUnprepareHeader never sends the WOM_DONE message, therefor locking the program up (until waveOutUnprepareHeader() is called).
To test this, copy the example from WAVEFORMAT_MIDI_MESSAGE in the SDK help, and change the line
WAVEFORMAT_MIDI_MESSAGE MidiMessage[5];
to
WAVEFORMAT_MIDI_MESSAGE *MidiMessage = new WAVEFORMAT_MIDI_MESSAGE[5];
Drop us a line if you figure A out, I would REALLY like to know, as it is I'm going to have to shelf this code as I'm out of ideas for the moment.
For B you could try creating two static buffers, and while one is playing full the other, and switch them over.
Unfortunatly FMOD doesn't support MIDI on Windows CE,
but Arcsoft does, although it will cost 'ya.
The following is based on the example in the SDK help.
The function loads the midi file into memory, calls readmidi() to return the track data, then calls parseMidiData() to get the number of MIDI commands and track speed. If this is acceptable it sets up the waveOut device, and calls parseMidiData() again, which fills out the WAVEFORMAT_MIDI_MESSAGE array with MIDI commands.
Things to note: Firstly, I have hardcoded the size of WAVEFORMAT_MIDI_MESSAGE to 48, this is just big enough for the the MIDI files I was using to test the routine.
Secondly I have hard coded the speed of play back (USecPerQuarterNote and TicksPerQuarterNote) as I was finding it didn't always sound right.
Lastly the WaitForSingleObject() timeout is 10 seconds, as dynamicly allocating the WAVEFORMAT_MIDI_MESSAGE would lock the program up here.
bool CSoundsDialog::playMidi(CString name)
{
#ifdef PLAY_MIDI
struct miditrack seq[MAXTRKS];
CFile f;
f.Open(name, CFile::modeRead | CFile::shareDenyWrite);
unsigned char * p = new unsigned char[f.GetLength()+1];
f.Read(p, f.GetLength());
int tracks = readmidi(p, f.GetLength(), seq);
WAVEFORMAT_MIDI wfm; // WAVEFORMATEX
memset(&wfm,0,sizeof(wfm));
wfm.USecPerQuarterNote=263157;
wfm.TicksPerQuarterNote=(p[12]<<8) | p[13];//480
int j = parseMidiData(0, &wfm.USecPerQuarterNote, &wfm.TicksPerQuarterNote, 0, seq);
wfm.USecPerQuarterNote=263157;
wfm.TicksPerQuarterNote=480;
if (j < 48) {
hEvent=CreateEvent( NULL,TRUE,FALSE,NULL);
wfm.wfx.wFormatTag=WAVE_FORMAT_MIDI;
wfm.wfx.nChannels=1;
wfm.wfx.nSamplesPerSec=32;
wfm.wfx.nAvgBytesPerSec=32;
wfm.wfx.nBlockAlign=sizeof(WAVEFORMAT_MIDI_MESSAGE);
wfm.wfx.wBitsPerSample=16;
wfm.wfx.cbSize=WAVEFORMAT_MIDI_EXTRASIZE;
MMRESULT Result;
HWAVEOUT hWaveOut;
HANDLE hEvent;
Result = waveOutOpen(&hWaveOut, WAVE_MAPPER, (LPWAVEFORMATEX)&wfm, (DWORD)waveOutProc, hEvent, CALLBACK_FUNCTION);
if (Result!=MMSYSERR_NOERROR) {
return false;
}
waveOutSetVolume(hWaveOut, 0x7777 );
WAVEFORMAT_MIDI_MESSAGE MidiMessage[48];
/*
WAVEFORMAT_MIDI_MESSAGE * MidiMessage;
MidiMessage = new WAVEFORMAT_MIDI_MESSAGE[48];
//*/
parseMidiData(MidiMessage, 0, 0, 0, seq);
MidiMessage[j].DeltaTicks=0;
MidiMessage[j].MidiMsg=0;
j++;
f.Close();
delete [] p;
//s.Format(L"created %d midi messages", j);
//MessageBox(s);
WAVEHDR WaveHdr;
WaveHdr.lpData = (LPSTR)&MidiMessage;
WaveHdr.dwBufferLength = sizeof(WAVEFORMAT_MIDI_MESSAGE) * j;
WaveHdr.dwFlags = 0;
WaveHdr.dwLoops = 1;
Result = waveOutPrepareHeader(hWaveOut,&WaveHdr,sizeof(WaveHdr));
if (Result != MMSYSERR_NOERROR)
MessageBox(L"waveOutPrepareHeader failed");
else {
Result = waveOutWrite(hWaveOut,&WaveHdr,sizeof(WaveHdr));
if (Result != MMSYSERR_NOERROR)
MessageBox(L"waveOutWrite failed");
else {
WaitForSingleObject(hEvent,10000);
}
Result = waveOutUnprepareHeader(hWaveOut,&WaveHdr,sizeof(WaveHdr));
}
waveOutReset(hWaveOut);
Result = waveOutClose(hWaveOut);
//delete [] MidiMessage;
return true;
} else {
f.Close();
delete [] p;
return false;
}
#else
return false;
#endif
}
The MIDI header looks like this: 'MThd' (4bytes), length=6 (4bytes), format (4bytes), tracks (2bytes), division (2bytes)
Followed by 1-to-many tracks, which look like this: 'MTrk' (4bytes), length (4bytes), data
And below you will find my function to read them. Now WAVEFORMAT_MIDI_MESSAGE only supports one track, so I have commented out the code to do multitrack MIDI files. (note: the
MIDI 'format' should be 0, which means its a single track MIDI file, but I don't check this)
int CSoundsDialog::readmidi(unsigned char*midifilebuf, int filelength, miditrack *seq)
{
int ntrks;
if (Read32(&midifilebuf) == 0x4d546864) { // MThd
midifilebuf += 4;//tracklen
midifilebuf += 2;//format
ntrks = Read16(&midifilebuf);
midifilebuf += 2;//division
} else
return 0;
if (ntrks > MAXTRKS) {
CString s;
s.Format(L"%d tracks found, only %d is supported", ntrks, MAXTRKS);
MessageBox(s, L"", MB_ICONWARNING);
ntrks = MAXTRKS;
}
//for (int track = 0; track < ntrks; track++) {
if (Read32(&midifilebuf) != 0x4d54726b) //MTrk
return 0;
seq[0].length = Read32(&midifilebuf);
seq[0].data = midifilebuf;
midifilebuf += seq[0].length;
//}
return ntrks;
}
And here is the key to getting things working, its all by the MIDI book, its just that sometimes its hard to find :-)
Each data chunk looks like this: delta time (varible size), event.
For efficiency MIDI has varible-size numbers. The lower 7 bits is the number, and the MSB signifies another byte has been used to represent the number. For example: 0x7F -> 127, and 0x81 00 -> 128, have a look at the DeltaTicks calculation for how to implement this. (Note I have only implemented 2 bytes, in practice it could be more, thats a prototype for you)
Event is always greater than or equal to 0x80
Event 0xff is a meta event, and takes the form: 0xff, event (2bytes), size (varible), data.
The one I'm most interested in is the Set Tempo event, which takes this form: 0xff 51 03 tt tt tt, where tt is the tempo.
Note: if I was doing this properly I would dynamicly interpret the Set Tempo message into a MIDI_MESSAGE_UPDATETEMPO message.
Events 0xf0 and 0xf7 are Sysex events, which can be device specific messages
Any other messages (such as 0x9x AKA Note On messages) we can send onto the WaveOut functions to handle
Lastly, and messages < 0x80 are 'running status' messages, which means keep using the last command. These are oftern used when you want to play several notes in a row.
int CSoundsDialog::parseMidiData(WAVEFORMAT_MIDI_MESSAGE*MidiMessage,
UINT32 * secs, UINT32 * tics, DWORD track, miditrack * seq)
{
int j = 0; // j indexes into MidiMessage
DWORD sz;
for (unsigned int i = 0; i < seq[track].length; ++i) { // i indexes into the midi data track
// the delta time
if (seq[track].data[i] >= 0x80) {
if (MidiMessage) MidiMessage[j].DeltaTicks = ((seq[track].data[i]&0x7f)<<7)|seq[track].data[i+1];
++i;
} else
if (MidiMessage) MidiMessage[j].DeltaTicks = seq[track].data[i];
++i;
// the event
if (seq[track].data[i] >= 0x80) {
if (seq[track].data[i] == 0xff) { // Meta
int type = seq[track].data[++i];
sz = seq[track].data[++i];
if (type == 0x51 && secs) // set tempo
*secs = (seq[track].data[i]<<16) | (seq[track].data[i+1]<<8) | (seq[track].data[i+2]);
i+= sz;
} else if (seq[track].data[i] == 0xf0 || seq[track].data[i] == 0xf7) { // systex
sz = seq[track].data[++i];
i+= sz;
} else {
++i;
if (MidiMessage)
MidiMessage[j].MidiMsg = (seq[track].data[i]<<8) | (seq[track].data[i+1]<<16) | seq[track].data[i-1];
++j;
++i;
}
} else {
// running status, check the SDK help for more info
if (MidiMessage)
MidiMessage[j].MidiMsg = seq[track].data[i] | (seq[track].data[i+1]<<8);
++j;
++i;
}
}
return j;
}
Here is an example midi file breakdown of AlarmXtra2.mid (sans the delta-time):
Header
======
SMF type 0
Number of tracks = 1
480 ticks per quarter note
Track 1 (100 bytes)
===================
Meta 20 Channel prefix 5
Meta 03 Track name Celesta*Demixed 06
Meta 04 Instrument name GM Device 6
Meta 58 Time signature 4:0 24 8
Meta 59 Key signature
Meta 54 SMPTE offset
Meta 51 Set tempo 151114
Midi 95 Note on 57 5F
Midi 95 Note on 53 50
Midi 95 Note on 57 00
Midi 95 Note on 53 00
Midi 95 Note on 57 50
Midi 95 Note on 57 00
Meta 2F End of track
As you can see I've given you a VERY quick overview of the MIDI format, so if you want to know more have a read of this.
Here is the utility functions/header that have been referenced by the above code. Nothing special here, I just include it for completeness.
#include "wfmtmidi.h"
class CSoundsDialog : public CDialogImpl<CSoundsDialog>
{
private:
struct miditrack {
unsigned char *data;
unsigned long length;
};
int parseMidiData(WAVEFORMAT_MIDI_MESSAGE*MidiMessage, UINT32 * secs, UINT32 * tics, DWORD track, miditrack * seq);
bool playMidi(CString name);
int readmidi(unsigned char*filebuf, int filelength, miditrack *seq);
};
#define MAXTRKS 1
void CALLBACK waveOutProc(HWAVEOUT hwo, UINT uMsg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2)
{
if (uMsg==WOM_DONE)
SetEvent((HANDLE)dwInstance);
return;
}
unsigned short Read16(unsigned char **midifilebuf)
{
unsigned short x = (**(midifilebuf) << 8) | (*midifilebuf)[1];
*midifilebuf += 2;
return x;
}
unsigned long Read32(unsigned char **midifilebuf)
{
unsigned long x = (**(midifilebuf) << 24) | ((*midifilebuf)[1] << 16) |
((*midifilebuf)[2] << 8) | (*midifilebuf)[3];
*midifilebuf += 4;
return x;
}