2 BeepMidi :: beep.sys MIDI player
4 (c) Andrew Greenwood, 2007.
6 Released as open-source software. You may copy, re-distribute and modify
7 this software, provided this copyright notice remains intact.
9 Please see the included README.TXT for more information
12 16th January 2007 Started
13 17th January 2007 Polyphony support and threading added
14 18th January 2007 Made threading optional, added comments
17 /* The timeslice to allocate for all playing notes (in milliseconds) */
18 #define TIMESLICE_SIZE 60
21 If this is defined, notes are added to the playing list, even if
22 they already exist. As a result, the note will sound twice during
23 each timeslice. Also each note on will require a corresponding note
26 #define ALLOW_DUPLICATE_NOTES
29 The maximum number of notes that may be playing at any one time.
30 Higher values result in a messier sound as all the frequencies get
31 mashed together. Do not set this below 2. Recommended = 4
36 Define CONTINUOUS_NOTES to perform note playback in a separate thread.
37 This was originally the intended behaviour, but after experimentation
38 doesn't sound as good for MIDI files which have a lot going on. If not
39 defined, all playing notes are output in sequence as a new note starts.
41 #define CONTINUOUS_NOTES
43 #define WIN32_NO_STATUS
45 #define NTOS_MODE_USER
46 #include <ndk/iofuncs.h>
47 #include <ndk/obfuncs.h>
48 #include <ndk/rtlfuncs.h>
56 /*#define DPRINT printf*/
57 #define DPRINT FakePrintf
59 /* A few MIDI command categories */
60 #define MIDI_NOTE_OFF 0x80
61 #define MIDI_NOTE_ON 0x90
62 #define MIDI_CONTROL_CHANGE 0xB0
63 #define MIDI_PROGRAM 0xC0
64 #define MIDI_PITCH_BEND 0xE0
65 #define MIDI_SYSTEM 0xFF
67 /* Specific commands */
68 #define MIDI_RESET 0xFF
71 typedef struct _NoteNode
73 struct _NoteNode
* next
;
74 struct _NoteNode
* previous
;
77 UCHAR velocity
; /* 0 is note-off */
80 typedef struct _DeviceInfo
91 DWORD playing_notes_count
;
96 BOOL terminate_thread
;
97 HANDLE thread_termination_complete
;
100 DeviceInfo
* the_device
;
101 CRITICAL_SECTION device_lock
;
104 FakePrintf(char* str
, ...)
106 /* Just to shut the compiler up */
111 This is designed to be treated as a thread, however it behaves as a
112 normal function if CONTINUOUS_NOTES is not defined.
119 DeviceInfo
* device_info
= (DeviceInfo
*) parameter
;
121 IO_STATUS_BLOCK io_status_block
;
124 DPRINT("Note processing started\n");
126 /* We lock the note list only while accessing it */
128 #ifdef CONTINUOUS_NOTES
129 while ( ! device_info
->terminate_thread
)
134 /* Number of notes being arpeggiated */
137 EnterCriticalSection(&device_lock
);
139 /* Calculate how much time to allocate to each playing note */
141 DPRINT("%d notes active\n", (int) device_info
->playing_notes_count
);
143 node
= device_info
->note_list
;
145 while ( ( node
!= NULL
) && ( arp_notes
<= POLYPHONY
) )
147 BEEP_SET_PARAMETERS beep_data
;
148 DWORD actually_playing
= 0;
150 double frequency
= node
->note
;
152 DPRINT("playing..\n");
154 frequency
= frequency
/ 12;
155 frequency
= pow(2, frequency
);
156 frequency
= 8.1758 * frequency
;
158 if (device_info
->playing_notes_count
> POLYPHONY
)
159 actually_playing
= POLYPHONY
;
161 actually_playing
= device_info
->playing_notes_count
;
163 DPRINT("Frequency %f\n", frequency
);
166 beep_data
.Frequency
= (DWORD
) frequency
;
167 beep_data
.Duration
= TIMESLICE_SIZE
/ actually_playing
; /* device_info->playing_notes_count; */
169 status
= NtDeviceIoControlFile(device_info
->kernel_device
,
176 sizeof(BEEP_SET_PARAMETERS
),
180 if ( ! NT_SUCCESS(status
) )
182 DPRINT("ERROR %d\n", (int) GetLastError());
185 SleepEx(beep_data
.Duration
, TRUE
);
187 if ( device_info
->refresh_notes
)
189 device_info
->refresh_notes
= FALSE
;
197 LeaveCriticalSection(&device_lock
);
200 #ifdef CONTINUOUS_NOTES
201 SetEvent(device_info
->thread_termination_complete
);
209 Fills a MIDIOUTCAPS structure with information about our device.
213 GetDeviceCapabilities(
216 /* These are ignored for now */
220 caps
->vDriverVersion
= 0x0100;
222 memset(caps
->szPname
, 0, sizeof(caps
->szPname
));
223 memcpy(caps
->szPname
, L
"PC speaker\0", strlen("PC speaker\0") * 2);
225 caps
->wTechnology
= MOD_SQSYNTH
;
227 caps
->wVoices
= 1; /* We only have one voice */
228 caps
->wNotes
= POLYPHONY
;
229 caps
->wChannelMask
= 0xFFBF; /* Ignore channel 10 */
233 return MMSYSERR_NOERROR
;
238 Helper function that just simplifies calling the application making use
244 DeviceInfo
* device_info
,
246 DWORD_PTR parameter1
,
247 DWORD_PTR parameter2
)
249 DPRINT("Calling client - callback 0x%x mmhandle 0x%x\n", device_info
->callback
, device_info
->mme_handle
);
250 return DriverCallback(device_info
->callback
,
251 HIWORD(device_info
->flags
),
252 device_info
->mme_handle
,
254 device_info
->instance
,
262 Open the kernel-mode device and allocate resources. This opens the
263 BEEP.SYS kernel device.
268 DeviceInfo
** private_data
,
269 MIDIOPENDESC
* open_desc
,
274 HANDLE kernel_device
;
275 UNICODE_STRING beep_device_name
;
276 OBJECT_ATTRIBUTES attribs
;
277 IO_STATUS_BLOCK status_block
;
279 /* One at a time.. */
282 DPRINT("Already allocated\n");
283 return MMSYSERR_ALLOCATED
;
286 /* Make the device name into a unicode string and open it */
288 RtlInitUnicodeString(&beep_device_name
,
291 InitializeObjectAttributes(&attribs
,
297 status
= NtCreateFile(&kernel_device
,
298 FILE_READ_DATA
| FILE_WRITE_DATA
,
303 FILE_SHARE_READ
| FILE_SHARE_WRITE
,
309 if ( ! NT_SUCCESS(status
) )
311 DPRINT("Could not connect to BEEP device - %d\n", (int) GetLastError());
312 return MMSYSERR_ERROR
;
317 /* Allocate and initialize the device info */
319 heap
= GetProcessHeap();
321 the_device
= HeapAlloc(heap
, HEAP_ZERO_MEMORY
, sizeof(DeviceInfo
));
325 DPRINT("Out of memory\n");
326 return MMSYSERR_NOMEM
;
330 the_device
->kernel_device
= kernel_device
;
331 the_device
->playing_notes_count
= 0;
332 the_device
->note_list
= NULL
;
333 the_device
->thread_handle
= 0;
334 the_device
->terminate_thread
= FALSE
;
335 the_device
->running_status
= 0;
338 the_device
->mme_handle
= (HDRVR
) open_desc
->hMidi
;
339 the_device
->callback
= open_desc
->dwCallback
;
340 the_device
->instance
= open_desc
->dwInstance
;
341 the_device
->flags
= flags
;
343 /* Store the pointer in the user data */
344 *private_data
= the_device
;
346 /* This is threading-related code */
347 #ifdef CONTINUOUS_NOTES
348 the_device
->thread_termination_complete
= CreateEvent(NULL
, FALSE
, FALSE
, NULL
);
350 if ( ! the_device
->thread_termination_complete
)
352 DPRINT("CreateEvent failed\n");
353 HeapFree(heap
, 0, the_device
);
354 return MMSYSERR_NOMEM
;
357 the_device
->thread_handle
= CreateThread(NULL
,
364 if ( ! the_device
->thread_handle
)
366 DPRINT("CreateThread failed\n");
367 CloseHandle(the_device
->thread_termination_complete
);
368 HeapFree(heap
, 0, the_device
);
369 return MMSYSERR_NOMEM
;
373 /* Now we call the client application to say the device is open */
374 DPRINT("Sending MOM_OPEN\n");
375 DPRINT("Success? %d\n", (int) CallClient(the_device
, MOM_OPEN
, 0, 0));
377 return MMSYSERR_NOERROR
;
382 Close the kernel-mode device.
386 CloseDevice(DeviceInfo
* device_info
)
388 HANDLE heap
= GetProcessHeap();
390 /* If we're working in threaded mode we need to wait for thread to die */
391 #ifdef CONTINUOUS_NOTES
392 the_device
->terminate_thread
= TRUE
;
394 WaitForSingleObject(the_device
->thread_termination_complete
, INFINITE
);
396 CloseHandle(the_device
->thread_termination_complete
);
399 /* Let the client application know the device is closing */
400 DPRINT("Sending MOM_CLOSE\n");
401 CallClient(device_info
, MOM_CLOSE
, 0, 0);
403 NtClose(device_info
->kernel_device
);
406 HeapFree(heap
, 0, device_info
);
410 return MMSYSERR_NOERROR
;
415 Removes a note from the playing notes list. If the note is not playing,
416 we just pretend nothing happened.
421 DeviceInfo
* device_info
,
424 HANDLE heap
= GetProcessHeap();
426 NoteNode
* prev_node
= NULL
;
428 DPRINT("StopNote\n");
430 EnterCriticalSection(&device_lock
);
432 node
= device_info
->note_list
;
434 while ( node
!= NULL
)
436 if ( node
->note
== note
)
438 /* Found the note - just remove the node from the list */
440 DPRINT("Stopping note %d\n", (int) node
->note
);
442 if ( prev_node
!= NULL
)
443 prev_node
->next
= node
->next
;
445 device_info
->note_list
= node
->next
;
447 HeapFree(heap
, 0, node
);
449 device_info
->playing_notes_count
--;
451 DPRINT("Note stopped - now playing %d notes\n", (int) device_info
->playing_notes_count
);
453 LeaveCriticalSection(&device_lock
);
454 device_info
->refresh_notes
= TRUE
;
456 return MMSYSERR_NOERROR
;
463 LeaveCriticalSection(&device_lock
);
465 /* Hmm, a good idea? */
466 #ifndef CONTINUOUS_NOTES
467 ProcessPlayingNotes((PVOID
) device_info
);
470 return MMSYSERR_NOERROR
;
475 Adds a note to the playing notes list. If the note is already playing,
476 the definition of ALLOW_DUPLICATE_NOTES determines if an existing note
477 may be duplicated. Otherwise, duplicate notes are ignored.
482 DeviceInfo
* device_info
,
486 HANDLE heap
= GetProcessHeap();
490 DPRINT("PlayNote\n");
494 DPRINT("Zero velocity\n");
496 /* Velocity zero is effectively a "note off" */
497 StopNote(device_info
, note
);
501 /* Start playing the note */
504 EnterCriticalSection(&device_lock
);
506 node
= device_info
->note_list
;
508 while ( node
!= NULL
)
510 #ifndef ALLOW_DUPLICATE_NOTES
511 if ( ( node
->note
== note
) && ( velocity
> 0 ) )
513 /* The note is already playing - do nothing */
514 DPRINT("Duplicate note playback request ignored\n");
515 LeaveCriticalSection(&device_lock
);
516 return MMSYSERR_NOERROR
;
523 new_node
= HeapAlloc(heap
, HEAP_ZERO_MEMORY
, sizeof(NoteNode
));
527 LeaveCriticalSection(&device_lock
);
528 return MMSYSERR_NOMEM
;
531 new_node
->note
= note
;
532 new_node
->velocity
= velocity
;
535 Prepend to the playing notes list. If exceeding polyphony,
536 remove the oldest note (which will be at the tail.)
539 if ( device_info
->note_list
)
540 device_info
->note_list
->previous
= new_node
;
542 new_node
->next
= device_info
->note_list
;
543 new_node
->previous
= NULL
;
545 device_info
->note_list
= new_node
;
546 device_info
->playing_notes_count
++;
549 if ( device_info->playing_notes_count > POLYPHONY )
553 DPRINT("Polyphony exceeded\n");
555 tail_node->previous->next = NULL;
557 HeapFree(heap, 0, tail_node);
559 device_info->playing_notes_count --;
563 LeaveCriticalSection(&device_lock
);
565 DPRINT("Note started - now playing %d notes\n", (int) device_info
->playing_notes_count
);
566 device_info
->refresh_notes
= TRUE
;
569 #ifndef CONTINUOUS_NOTES
570 ProcessPlayingNotes((PVOID
) device_info
);
573 return MMSYSERR_NOERROR
;
577 Decipher a short MIDI message (which is a MIDI message packed into a DWORD.)
578 This will set "running status", but does not take this into account when
579 processing messages (is this necessary?)
583 ProcessShortMidiMessage(
584 DeviceInfo
* device_info
,
593 status
= message
& 0x000000FF;
595 /* Deal with running status */
597 if ( status
< MIDI_NOTE_OFF
)
599 status
= device_info
->running_status
;
602 /* Ensure the status is sane! */
604 if ( status
< MIDI_NOTE_OFF
)
606 /* It's garbage, ignore it */
607 return MMSYSERR_NOERROR
;
610 /* Figure out the message category and channel */
612 category
= status
& 0xF0;
613 channel
= status
& 0x0F; /* we don't use this */
615 data1
= (message
& 0x0000FF00) >> 8;
616 data2
= (message
& 0x00FF0000) >> 16;
618 DPRINT("0x%x, %d, %d\n", (int) status
, (int) data1
, (int) data2
);
620 /* Filter drums (which are *usually* on channel 10) */
623 return MMSYSERR_NOERROR
;
626 /* Pass to the appropriate message handler */
632 PlayNote(device_info
, data1
, data2
);
638 StopNote(device_info
, data1
);
643 return MMSYSERR_NOERROR
;
647 #define PACK_MIDI(b1, b2, b3) \
648 ((b3 * 65536) + (b2 * 256) + b1);
652 Processes a "long" MIDI message (ie, a MIDI message contained within a
653 buffer.) This is intended for supporting SysEx data, or blocks of MIDI
654 events. However in our case we're only interested in short MIDI messages,
655 so we scan the buffer, and each time we encounter a valid status byte
656 we start recording it as a new event. Once 3 bytes or a new status is
657 received, the event is passed to the short message handler.
661 ProcessLongMidiMessage(
662 DeviceInfo
* device_info
,
665 unsigned int index
= 0;
666 UCHAR
* midi_bytes
= (UCHAR
*) header
->lpData
;
668 unsigned int msg_index
= 0;
671 /* Initialize the buffer */
672 msg
[0] = msg
[1] = msg
[2] = 0;
674 if ( ! ( header
->dwFlags
& MHDR_PREPARED
) )
676 DPRINT("Not prepared!\n");
677 return MIDIERR_UNPREPARED
;
680 DPRINT("Processing %d bytes of MIDI\n", (int) header
->dwBufferLength
);
682 while ( index
< header
->dwBufferLength
)
684 /* New status byte? ( = new event) */
685 if ( midi_bytes
[index
] & 0x80 )
689 /* Deal with the existing event */
693 short_msg
= PACK_MIDI(msg
[0], msg
[1], msg
[2]);
695 DPRINT("Complete msg is 0x%x %d %d\n", (int) msg
[0], (int) msg
[1], (int) msg
[2]);
696 ProcessShortMidiMessage(device_info
, short_msg
);
699 /* Set new running status and start recording the event */
700 DPRINT("Set new running status\n");
701 device_info
->running_status
= midi_bytes
[index
];
702 msg
[0] = midi_bytes
[index
];
706 /* Unexpected data byte? ( = re-use previous status) */
707 else if ( msg_index
== 0 )
709 if ( device_info
->running_status
& 0x80 )
711 DPRINT("Retrieving running status\n");
712 msg
[0] = device_info
->running_status
;
713 msg
[1] = midi_bytes
[index
];
720 /* Expected data ( = append to message until buffer full) */
723 DPRINT("Next byte...\n");
724 msg
[msg_index
] = midi_bytes
[index
];
731 short_msg
= PACK_MIDI(msg
[0], msg
[1], msg
[2]);
733 DPRINT("Complete msg is 0x%x %d %d\n", (int) msg
[0], (int) msg
[1], (int) msg
[2]);
734 ProcessShortMidiMessage(device_info
, short_msg
);
738 msg
[0] = msg
[1] = msg
[2] = 0;
746 We're meant to clear MHDR_DONE and set MHDR_INQUEUE but since we
747 deal with everything here and now we might as well just say so.
749 header
->dwFlags
|= MHDR_DONE
;
750 header
->dwFlags
&= ~ MHDR_INQUEUE
;
752 DPRINT("Success? %d\n", CallClient(the_device
, MOM_DONE
, (DWORD_PTR
) header
, 0));
754 return MMSYSERR_NOERROR
;
759 Exported function that receives messages from WINMM (the MME API.)
767 DWORD_PTR private_data
,
768 DWORD_PTR parameter1
,
769 DWORD_PTR parameter2
)
773 case MODM_GETNUMDEVS
:
775 /* Only one internal PC speaker device (and even that's too much) */
776 DPRINT("MODM_GETNUMDEVS\n");
780 case MODM_GETDEVCAPS
:
782 DPRINT("MODM_GETDEVCAPS\n");
783 return GetDeviceCapabilities((MIDIOUTCAPS
*) parameter1
);
788 DPRINT("MODM_OPEN\n");
790 return OpenDevice((DeviceInfo
**) private_data
,
791 (MIDIOPENDESC
*) parameter1
,
797 DPRINT("MODM_CLOSE\n");
798 return CloseDevice((DeviceInfo
*) private_data
);
803 return ProcessShortMidiMessage((DeviceInfo
*) private_data
, parameter1
);
808 /* We don't bother with this */
809 MIDIHDR
* hdr
= (MIDIHDR
*) parameter1
;
810 hdr
->dwFlags
|= MHDR_PREPARED
;
811 return MMSYSERR_NOERROR
;
814 case MODM_UNPREPARE
:
816 MIDIHDR
* hdr
= (MIDIHDR
*) parameter1
;
817 hdr
->dwFlags
&= ~MHDR_PREPARED
;
818 return MMSYSERR_NOERROR
;
823 DPRINT("LONGDATA\n");
824 return ProcessLongMidiMessage((DeviceInfo
*) private_data
, (MIDIHDR
*) parameter1
);
834 DPRINT("Not supported %d\n", message
);
836 return MMSYSERR_NOTSUPPORTED
;
856 DPRINT("DRV_LOAD\n");
861 DPRINT("DRV_FREE\n");
865 DPRINT("DRV_OPEN\n");
866 InitializeCriticalSection(&device_lock
);
870 DPRINT("DRV_CLOSE\n");
874 DPRINT("DRV_ENABLE\n");
878 DPRINT("DRV_DISABLE\n");
882 We don't provide configuration capabilities. This used to be
883 for things like I/O port, IRQ, DMA settings, etc.
886 case DRV_QUERYCONFIGURE
:
887 DPRINT("DRV_QUERYCONFIGURE\n");
891 DPRINT("DRV_CONFIGURE\n");
895 DPRINT("DRV_INSTALL\n");
896 return DRVCNF_RESTART
;
901 return DefDriverProc(driver_id
,