The Wavedev2 MIDI Implementation

The wavedev2 wave driver sample code includes a fairly primitive MIDI synthesizer as part of the source code. The reason for its relative simplicity dates back to the question: "What can you implement in two weeks with no additional ROM hit to run on a 100MHz ARM processor"?

Seriously, the original goal was just to provide a sample implementation, with the assumption that OEMs would replace it by licensing or developing their own synthesizer (which, in fact, many OEMs do). We also (correctly, I believe) anticipated that in the future ringtones would more likely be implemented as compressed audio files (e.g. WMA). It made little sense to pour money/development resources into developing our own high-quality MIDI synthesizer, and there are third parties who handle that quite well anyway (such as Beatnik).

Having said that, the wavedev2 sample still includes the primitive MIDI synthesizer, and it probably ships unmodified on some platforms, so it might be interesting to know how to use it.

The wavedev2 sample MIDI synth has the following attributes:

- Instruments: Only sine wave generation, no other instruments.

- Polyphony: No limit on the number of midi streams. Number of concurrent notes per stream is limited to 32 (controlled by a #define in the driver). Realistically, the total number of notes will be limited by the amount of CPU MIPS. I don’t think we’ll have any problem with 8-10 notes.

- Sample accurate timing

- Extensions to support arbitrary frequency tone generation (e.g. for things like DTMF, ringback, busy, etc.) and tempo changes.

The OS doesn't support the standard Win32 MIDI apis, so we had to invent our own somewhat proprietary method. To do this without creating new API entry points, we implemented MIDI as a proprietary wave format. To play MIDI notes, you open the wave device using waveOutOpen with a WAVEFORMAT_MIDI format structure, which is defined in wfmtmidi.h as:

 

typedef struct _WAVEFORMAT_MIDI

{

    WAVEFORMATEX wfx;

    UINT32 USecPerQuarterNote;

    UINT32 TicksPerQuarterNote;

} WAVEFORMAT_MIDI, *LPWAVEFORMAT_MIDI;

The wfx.wFormatTag field should be filled in with WAVE_FORMAT_MIDI, which is defined in the header as:

 

#define WAVE_FORMAT_MIDI 0x3000

(In retrospect we should have used a WaveFormatExtensible structure, which uses a GUID, rather than arbitrarily allocating another format tag, since there's a chance we'll collide with some other OEM format tag).

 

You then start passing buffers to the driver using waveOutMessage, just as you would for wave data. The data in the buffers consists of an array of WAVEFORMAT_MIDI_MESSAGE structures, which are defined as:

 

typedef struct _WAVEFORMAT_MIDI_MESSAGE

{

    UINT32 DeltaTicks;

    DWORD MidiMsg;

} WAVEFORMAT_MIDI_MESSAGE;

 

The wave driver will automatically take care of the timing of when to sequence each midi message, based on the relationship between the DeltaTicks field of the next midi event and the USecPerQuarterNote and TicksPerQuarterNote fields of the WAVEFORMAT_MIDI structure.

 

You can send just about any MIDI message to the driver, but the sample drivers only process the midi messages for “note on”, “note off”, and the control change message for “all notes off”; any other MIDI message will be ignored by the driver.

The sample driver also supports proprietary messages for playing an arbitrary frequency and for changing the tempo during playback:

MIDI_MESSAGE_FREQGENON and MIDI_MESSAGE_FREQGENOFF are roughly analogous to NoteOn/NoteOff, but they take a 16-bit frequency value rather than a 7-bit note value, and they always play a sine wave. This can be useful for things like DTMF, ringback, busy, and other call progress tones which require exact frequencies and which don’t map exactly to the frequencies supported by the musical scale. For these messages, the upper 8 bits of MidiMsg (which are normally 0) are set to either MIDI_MESSAGE_FREQGENON or MIDI_MESSAGE_FREQGENOFF. The next 8 bits are the 7-bit velocity (e.g. volume) (the top bit must be 0), and the lowest 16 bits are the desired frequency.

MIDI_MESSAGE_UPDATETEMPO can be used to update the USecPerQuarterNote parameter in the middle of a stream. For these messages, the upper 8 bits of MidiMsg (which are normally 0) are set to either MIDI_MESSAGE_ UPDATETEMPO. The low 24 bits are the updated tempo value.

Other notes:

  • While the MIDI synth handles sequencing and tone generation, it doesn't include any provision for MIDI file parsing. If you want to play MIDI files, you'll need to implement a MIDI file parser, extract the data and timestamp information, and feed it down the the wave driver.
  • The sample MIDI synth only supports sine waves, and has no concept of channels, instruments, or patch changes: all instruments are going to be remapped to sine wave tones. In general this yields a recognizable melody, with one major exception: in MIDI, percussion is considered considered a single instrument, with different types of drums, symbols, etc. mapped to different note values. If you try to play an arbitrary MIDI stream which includes percussion, the various percussive sounds are going to be played as apparently random sine waves. It's going to sound awful.

I've appended two samples below. The first, miditest.cpp, plays a midi scale. The second plays a ringback tone for 30 seconds.

miditest.cpp (plays an 8-note midi scale):

#include "windows.h"
#include "wfmtmidi.h"

int _tmain(int argc, TCHAR *argv[])
{
// Code to play a simple 8-note scale.
unsigned char Scale[8] =
{
63,65,67,68,70,72,74,75
};

    // Build a MIDI waveformat header
WAVEFORMAT_MIDI wfm;
memset(&wfm,0,sizeof(wfm));
wfm.wfx.wFormatTag=WAVE_FORMAT_MIDI;
wfm.wfx.nChannels=1;
wfm.wfx.nBlockAlign=sizeof(WAVEFORMAT_MIDI_MESSAGE);
wfm.wfx.cbSize=WAVEFORMAT_MIDI_EXTRASIZE;

    // These fields adjust the interpretation of DeltaTicks, and thus the rate of playback
wfm.USecPerQuarterNote=1000000; // Set to 1 second. Note driver will default to 500000 if we set this to 0
wfm.TicksPerQuarterNote=100; // Set to 100. Note driver will default to 96 if we set this to 0

    HANDLE hEvent;
hEvent = CreateEvent( NULL,TRUE,FALSE,NULL);

    MMRESULT Result;
HWAVEOUT hWaveOut;

    // Open the waveout device
Result = waveOutOpen(&hWaveOut, 0, (LPWAVEFORMATEX)&wfm, (DWORD)hEvent, 0, CALLBACK_EVENT);

    if (Result!=MMSYSERR_NOERROR)
{
return -1;
}

    // Build a MIDI buffer with 16 MIDI messages.
int i,j;
WAVEFORMAT_MIDI_MESSAGE MidiMessage[16];
for (i=0,j=0;i<8;i++,j+=2)
{
MidiMessage[j].DeltaTicks=100; // Wait 1 second : (DeltaTicks * (UsecPerQuarterNote/TicksPerQuarterNote))
MidiMessage[j].MidiMsg=0x7F0090 | ((Scale[i])<<8); // Note on
MidiMessage[j+1].DeltaTicks=100; // Wait 1 second
MidiMessage[j+1].MidiMsg=0x7F0080 | ((Scale[i])<<8); // Note off
}

    WAVEHDR WaveHdr;
WaveHdr.lpData = (LPSTR)MidiMessage;
WaveHdr.dwBufferLength = sizeof(MidiMessage);
WaveHdr.dwFlags = 0;
Result = waveOutPrepareHeader(hWaveOut,&WaveHdr,sizeof(WaveHdr));

    // Play the data
Result = waveOutWrite(hWaveOut,&WaveHdr,sizeof(WaveHdr));

    // Wait for playback to complete
WaitForSingleObject(hEvent,INFINITE);

    // Cleanup
Result = waveOutUnprepareHeader(hWaveOut,&WaveHdr,sizeof(WaveHdr));
Result = waveOutClose(hWaveOut);
return 0;
}

tonetest.cpp (Plays a 30 second ringback tone):

#include "windows.h"
#include "wfmtmidi.h"

/*
DTMF frequencies:

DTMF stands for Dual Tone Multi Frequency. These are the tones you get when
you press a key on your telephone touchpad. The tone of the button is the
sum of the column and row tones. The ABCD keys do not exist on standard
telephones.

                        Frequency 1

                    1209 1336 1477 1633

                697 1 2 3 A

                770 4 5 6 B
Frequency 2
852 7 8 9 C

                941 * 0 # D

Frequencies of other telephone tones

Type Hz On Off
---------------------------------------------------------------------
Dial Tone 350 & 400 --- ---
Busy Signal 480 & 620 0.5 0.5
Toll Congestion 480 & 620 0.2 0.3
Ringback (Normal) 440 & 480 2.0 4.0
Ringback (PBX) 440 & 480 1.5 4.5
Reorder (Local) 480 & 620 3.0 2.0
Invalid Number 200 & 400
Hang Up Warning 1400 & 2060 0.1 0.1
Hang Up 2450 & 2600 --- ---
*/

int _tmain(int argc, TCHAR *argv[])
{
WAVEFORMAT_MIDI wfm = {0};
wfm.wfx.wFormatTag=WAVE_FORMAT_MIDI;
wfm.wfx.nBlockAlign=sizeof(WAVEFORMAT_MIDI_MESSAGE);
wfm.wfx.cbSize=WAVEFORMAT_MIDI_EXTRASIZE;

    // Force each tick to be 1/10 sec
wfm.USecPerQuarterNote=100000;
wfm.TicksPerQuarterNote=1;

    MMRESULT Result;
HWAVEOUT hWaveOut;
HANDLE hEvent=CreateEvent( NULL,TRUE,FALSE,NULL);

    Result = waveOutOpen(&hWaveOut, 0, (LPWAVEFORMATEX)&wfm, (DWORD)hEvent, 0, CALLBACK_EVENT);
if (Result!=MMSYSERR_NOERROR)
{
return -1;
}

    // Create a buffer for 5 midi messages
WAVEFORMAT_MIDI_MESSAGE MidiMessage[5];

    MidiMessage[0].DeltaTicks=0;
MidiMessage[0].MidiMsg=0x207F0000 | 440; // Note on 440Hz

    MidiMessage[1].DeltaTicks=0;
MidiMessage[1].MidiMsg=0x207F0000 | 480; // Note on 480Hz

    MidiMessage[2].DeltaTicks=20; // Wait 2 sec
MidiMessage[2].MidiMsg=0x307F0000 | 440; // Note off 440Hz

    MidiMessage[3].DeltaTicks=0;
MidiMessage[3].MidiMsg=0x307F0000 | 480; // Note off 480Hz

    MidiMessage[4].DeltaTicks=40; // Wait 4 sec
MidiMessage[4].MidiMsg=0; // Dummy msg, does nothing

    WAVEHDR WaveHdr;

    // Point wave header to MIDI data
WaveHdr.lpData = (LPSTR)MidiMessage;
WaveHdr.dwBufferLength = sizeof(MidiMessage);

    // Loop on this buffer 20 times
WaveHdr.dwFlags = WHDR_BEGINLOOP|WHDR_ENDLOOP;
WaveHdr.dwLoops = 20;

    // Play it!
Result = waveOutPrepareHeader(hWaveOut,&WaveHdr,sizeof(WaveHdr));
Result = waveOutWrite(hWaveOut,&WaveHdr,sizeof(WaveHdr));

    // Wait for it to be done or 30 seconds, whichever comes first
WaitForSingleObject(hEvent,30000);
Result = waveOutReset(hWaveOut);
Result = waveOutUnprepareHeader(hWaveOut,&WaveHdr,sizeof(WaveHdr));
Result = waveOutClose(hWaveOut);

    return 0;
}

Q&A (I'll start moving my responses to comments here):

Q. I changed the definition of MidiMessage in the above sample to make it a dynamically allocated pointer, and now the sample doesn't work.

A. You need to also change the line that says "WaveHdr.dwBufferLength = sizeof(MidiMessage);" or else dwBufferLength will end up being 4 (the size of your pointer) rather than the size of the buffer. If you don't, the call to waveOutPrepareHeader will fail, as will everything else past that point. Blame me for not doing error checking in the sample code above.

Q. wmftmidi.h is no longer present in the Windows Mobile 5 SDK.

A. You should be able to grab it from an older SDK. Keep in mind that this is really a fairly "unofficial" API which was really designed solely for OEM use to play ringtones. One shouldn't expect it to be present on every device.

Q. How come the ringtones on my phone sound like high-quality MIDI, but when I use the interface described above I only get low-quality sine waves?

A. There are two possibilities. The first is that the ringtone you hear is actuallly a compressed WMA file, which are supported as ring tones. The second is that the OEM may have implemented their own proprietary MIDI synthesizer elsewhere in the system. To elaborate on the latter situation: There's a higher-level API, known as EventSound, where OEMs can plug in their own MIDI synthesizer (or arbitrary audio codec) to play ringtones or other system sounds. This API isn't open to ISVs (it's very subject to change from release to release). The actual implementation of this will vary greatly from device to device.

Q. Didn't Windows CE support a higher quality MIDI synth at some point in the past as part of DirectShow?

A. At one point DirectMusic was ported to Windows CE and shipped to support playback of MIDI files. However, for a variety of reasons (performance, code size, RAM usage, stability) it was unacceptable. As far as I know no one ever shipped a product using it, and it was dropped from the product in successive releases.