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:
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):
/*DTMF frequencies:
DTMF stands for Dual Tone Multi Frequency. These are the tones you get whenyou press a key on your telephone touchpad. The tone of the button is thesum of the column and row tones. The ABCD keys do not exist on standardtelephones.
Frequency 1
1209 1336 1477 1633
697 1 2 3 A
770 4 5 6 BFrequency 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.5Toll Congestion 480 & 620 0.2 0.3Ringback (Normal) 440 & 480 2.0 4.0Ringback (PBX) 440 & 480 1.5 4.5Reorder (Local) 480 & 620 3.0 2.0Invalid Number 200 & 400Hang Up Warning 1400 & 2060 0.1 0.1Hang 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.