Partager via


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.

Comments

  • Anonymous
    January 17, 2007
    This is my first blog post, so please feel free to leave feedback with questions or comments, especially

  • Anonymous
    January 18, 2007
    The comment has been removed

  • Anonymous
    January 19, 2007
    interesting, I did some research into this a few years back, here is what I came up with: http://homepages.inspire.net.nz/~gambit/Midi

  • Anonymous
    January 19, 2007
    I'll look at the dynamic allocation issue next time I get a chance. It might just be a red herring in the tonetest sample; I was trying to demonstrate that one could play 20 cycles or 30 seconds, whichever comes first. The original sample code used a busy tone, which cycles once a second, so 20 cycles would take 20 seconds and one would always get WOM_DONE. Later on I changed it to a ringback tone and forgot that a ringback cycle takes 6 seconds, so 20 cycles would take 120 seconds. In that case you'll always timeout in the wait before the event gets signaled.

  • Anonymous
    January 24, 2007
    The comment has been removed

  • Anonymous
    February 21, 2007
    Hello, I was using this method under Pocket PC 2003. I would like to use it under Windows Mobile 5, but the "wfmtmidi.h" file is not present in my Windows Mobile 5 SDK. Is there something wrong with my SDK? Regards, Nicolas.

  • Anonymous
    March 22, 2007
    The wfmtmidi probably got dropped from the SDK at some point (the MIDI synth wasn't really meant to be an API that ISVs should depend on too much). You should be able to grab the old header and use that; it won't have changed for WM5 devices.

  • Anonymous
    April 13, 2007
    I am trying for the reverrse implementation... any ideas... Shaktisingh2001us@yahoo.com

  • Anonymous
    April 10, 2008
    Dear Andy:      Thanks for all your articles.      I have a question about midi driver.      If I use special hardware for playing midi file,then,Can I add the driver under the wavedev2 model?which things shoud i pay attention to? May be you can give me some suggession,thanks! (pxa270+wm9713+ymu765)

  • Anonymous
    August 30, 2008
    Po del&#353;&#237; době jsem se vr&#225;til k programov&#225;n&#237; her, tentokr&#225;t pro platformu Pocket PC 2003 / Windows Mobile. Nejsem ž&#225;dn&#253; profesion&#225;ln&#237; hern&#237; v&#253;voj&#225;ř, je to sp&#237;&#353; hobby. Každop&#2