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 COM_NO_WINDOWS_H
49 #define NTOS_MODE_USER
50 #include <ndk/iofuncs.h>
51 #include <ndk/obfuncs.h>
52 #include <ndk/rtlfuncs.h>
57 /*#define DPRINT printf*/
58 #define DPRINT FakePrintf
60 /* A few MIDI command categories */
61 #define MIDI_NOTE_OFF 0x80
62 #define MIDI_NOTE_ON 0x90
63 #define MIDI_CONTROL_CHANGE 0xB0
64 #define MIDI_PROGRAM 0xC0
65 #define MIDI_PITCH_BEND 0xE0
66 #define MIDI_SYSTEM 0xFF
68 /* Specific commands */
69 #define MIDI_RESET 0xFF
72 typedef struct _NoteNode
74 struct _NoteNode
* next
;
75 struct _NoteNode
* previous
;
78 UCHAR velocity
; /* 0 is note-off */
81 typedef struct _DeviceInfo
92 DWORD playing_notes_count
;
97 BOOL terminate_thread
;
98 HANDLE thread_termination_complete
;
101 DeviceInfo
* the_device
;
102 CRITICAL_SECTION device_lock
;
105 FakePrintf(char* str
, ...)
107 /* Just to shut the compiler up */
112 This is designed to be treated as a thread, however it behaves as a
113 normal function if CONTINUOUS_NOTES is not defined.
120 DeviceInfo
* device_info
= (DeviceInfo
*) parameter
;
122 IO_STATUS_BLOCK io_status_block
;
125 DPRINT("Note processing started\n");
127 /* We lock the note list only while accessing it */
129 #ifdef CONTINUOUS_NOTES
130 while ( ! device_info
->terminate_thread
)
135 /* Number of notes being arpeggiated */
138 EnterCriticalSection(&device_lock
);
140 /* Calculate how much time to allocate to each playing note */
142 DPRINT("%d notes active\n", (int) device_info
->playing_notes_count
);
144 node
= device_info
->note_list
;
146 while ( ( node
!= NULL
) && ( arp_notes
<= POLYPHONY
) )
148 BEEP_SET_PARAMETERS beep_data
;
149 DWORD actually_playing
= 0;
151 double frequency
= node
->note
;
153 DPRINT("playing..\n");
155 frequency
= frequency
/ 12;
156 frequency
= pow(2, frequency
);
157 frequency
= 8.1758 * frequency
;
159 if (device_info
->playing_notes_count
> POLYPHONY
)
160 actually_playing
= POLYPHONY
;
162 actually_playing
= device_info
->playing_notes_count
;
164 DPRINT("Frequency %f\n", frequency
);
167 beep_data
.Frequency
= (DWORD
) frequency
;
168 beep_data
.Duration
= TIMESLICE_SIZE
/ actually_playing
; /* device_info->playing_notes_count; */
170 status
= NtDeviceIoControlFile(device_info
->kernel_device
,
177 sizeof(BEEP_SET_PARAMETERS
),
181 if ( ! NT_SUCCESS(status
) )
183 DPRINT("ERROR %d\n", (int) GetLastError());
186 SleepEx(beep_data
.Duration
, TRUE
);
188 if ( device_info
->refresh_notes
)
190 device_info
->refresh_notes
= FALSE
;
198 LeaveCriticalSection(&device_lock
);
201 #ifdef CONTINUOUS_NOTES
202 SetEvent(device_info
->thread_termination_complete
);
210 Fills a MIDIOUTCAPS structure with information about our device.
214 GetDeviceCapabilities(
217 /* These are ignored for now */
221 caps
->vDriverVersion
= 0x0100;
223 memset(caps
->szPname
, 0, sizeof(caps
->szPname
));
224 memcpy(caps
->szPname
, L
"PC speaker\0", strlen("PC speaker\0") * 2);
226 caps
->wTechnology
= MOD_SQSYNTH
;
228 caps
->wVoices
= 1; /* We only have one voice */
229 caps
->wNotes
= POLYPHONY
;
230 caps
->wChannelMask
= 0xFFBF; /* Ignore channel 10 */
234 return MMSYSERR_NOERROR
;
239 Helper function that just simplifies calling the application making use
245 DeviceInfo
* device_info
,
247 DWORD_PTR parameter1
,
248 DWORD_PTR parameter2
)
250 DPRINT("Calling client - callback 0x%x mmhandle 0x%x\n", device_info
->callback
, device_info
->mme_handle
);
251 return DriverCallback(device_info
->callback
,
252 HIWORD(device_info
->flags
),
253 device_info
->mme_handle
,
255 device_info
->instance
,
263 Open the kernel-mode device and allocate resources. This opens the
264 BEEP.SYS kernel device.
269 DeviceInfo
** private_data
,
270 MIDIOPENDESC
* open_desc
,
275 HANDLE kernel_device
;
276 UNICODE_STRING beep_device_name
;
277 OBJECT_ATTRIBUTES attribs
;
278 IO_STATUS_BLOCK status_block
;
280 /* One at a time.. */
283 DPRINT("Already allocated\n");
284 return MMSYSERR_ALLOCATED
;
287 /* Make the device name into a unicode string and open it */
289 RtlInitUnicodeString(&beep_device_name
,
292 InitializeObjectAttributes(&attribs
,
298 status
= NtCreateFile(&kernel_device
,
299 FILE_READ_DATA
| FILE_WRITE_DATA
,
304 FILE_SHARE_READ
| FILE_SHARE_WRITE
,
310 if ( ! NT_SUCCESS(status
) )
312 DPRINT("Could not connect to BEEP device - %d\n", (int) GetLastError());
313 return MMSYSERR_ERROR
;
318 /* Allocate and initialize the device info */
320 heap
= GetProcessHeap();
322 the_device
= HeapAlloc(heap
, HEAP_ZERO_MEMORY
, sizeof(DeviceInfo
));
326 DPRINT("Out of memory\n");
327 return MMSYSERR_NOMEM
;
331 the_device
->kernel_device
= kernel_device
;
332 the_device
->playing_notes_count
= 0;
333 the_device
->note_list
= NULL
;
334 the_device
->thread_handle
= 0;
335 the_device
->terminate_thread
= FALSE
;
336 the_device
->running_status
= 0;
339 the_device
->mme_handle
= (HDRVR
) open_desc
->hMidi
;
340 the_device
->callback
= open_desc
->dwCallback
;
341 the_device
->instance
= open_desc
->dwInstance
;
342 the_device
->flags
= flags
;
344 /* Store the pointer in the user data */
345 *private_data
= the_device
;
347 /* This is threading-related code */
348 #ifdef CONTINUOUS_NOTES
349 the_device
->thread_termination_complete
= CreateEvent(NULL
, FALSE
, FALSE
, NULL
);
351 if ( ! the_device
->thread_termination_complete
)
353 DPRINT("CreateEvent failed\n");
354 HeapFree(heap
, 0, the_device
);
355 return MMSYSERR_NOMEM
;
358 the_device
->thread_handle
= CreateThread(NULL
,
365 if ( ! the_device
->thread_handle
)
367 DPRINT("CreateThread failed\n");
368 CloseHandle(the_device
->thread_termination_complete
);
369 HeapFree(heap
, 0, the_device
);
370 return MMSYSERR_NOMEM
;
374 /* Now we call the client application to say the device is open */
375 DPRINT("Sending MOM_OPEN\n");
376 DPRINT("Success? %d\n", (int) CallClient(the_device
, MOM_OPEN
, 0, 0));
378 return MMSYSERR_NOERROR
;
383 Close the kernel-mode device.
387 CloseDevice(DeviceInfo
* device_info
)
389 HANDLE heap
= GetProcessHeap();
391 /* If we're working in threaded mode we need to wait for thread to die */
392 #ifdef CONTINUOUS_NOTES
393 the_device
->terminate_thread
= TRUE
;
395 WaitForSingleObject(the_device
->thread_termination_complete
, INFINITE
);
397 CloseHandle(the_device
->thread_termination_complete
);
400 /* Let the client application know the device is closing */
401 DPRINT("Sending MOM_CLOSE\n");
402 CallClient(device_info
, MOM_CLOSE
, 0, 0);
404 NtClose(device_info
->kernel_device
);
407 HeapFree(heap
, 0, device_info
);
411 return MMSYSERR_NOERROR
;
416 Removes a note from the playing notes list. If the note is not playing,
417 we just pretend nothing happened.
422 DeviceInfo
* device_info
,
425 HANDLE heap
= GetProcessHeap();
427 NoteNode
* prev_node
= NULL
;
429 DPRINT("StopNote\n");
431 EnterCriticalSection(&device_lock
);
433 node
= device_info
->note_list
;
435 while ( node
!= NULL
)
437 if ( node
->note
== note
)
439 /* Found the note - just remove the node from the list */
441 DPRINT("Stopping note %d\n", (int) node
->note
);
443 if ( prev_node
!= NULL
)
444 prev_node
->next
= node
->next
;
446 device_info
->note_list
= node
->next
;
448 HeapFree(heap
, 0, node
);
450 device_info
->playing_notes_count
--;
452 DPRINT("Note stopped - now playing %d notes\n", (int) device_info
->playing_notes_count
);
454 LeaveCriticalSection(&device_lock
);
455 device_info
->refresh_notes
= TRUE
;
457 return MMSYSERR_NOERROR
;
464 LeaveCriticalSection(&device_lock
);
466 /* Hmm, a good idea? */
467 #ifndef CONTINUOUS_NOTES
468 ProcessPlayingNotes((PVOID
) device_info
);
471 return MMSYSERR_NOERROR
;
476 Adds a note to the playing notes list. If the note is already playing,
477 the definition of ALLOW_DUPLICATE_NOTES determines if an existing note
478 may be duplicated. Otherwise, duplicate notes are ignored.
483 DeviceInfo
* device_info
,
487 HANDLE heap
= GetProcessHeap();
491 DPRINT("PlayNote\n");
495 DPRINT("Zero velocity\n");
497 /* Velocity zero is effectively a "note off" */
498 StopNote(device_info
, note
);
502 /* Start playing the note */
505 EnterCriticalSection(&device_lock
);
507 node
= device_info
->note_list
;
509 while ( node
!= NULL
)
511 #ifndef ALLOW_DUPLICATE_NOTES
512 if ( ( node
->note
== note
) && ( velocity
> 0 ) )
514 /* The note is already playing - do nothing */
515 DPRINT("Duplicate note playback request ignored\n");
516 LeaveCriticalSection(&device_lock
);
517 return MMSYSERR_NOERROR
;
524 new_node
= HeapAlloc(heap
, HEAP_ZERO_MEMORY
, sizeof(NoteNode
));
528 LeaveCriticalSection(&device_lock
);
529 return MMSYSERR_NOMEM
;
532 new_node
->note
= note
;
533 new_node
->velocity
= velocity
;
536 Prepend to the playing notes list. If exceeding polyphony,
537 remove the oldest note (which will be at the tail.)
540 if ( device_info
->note_list
)
541 device_info
->note_list
->previous
= new_node
;
543 new_node
->next
= device_info
->note_list
;
544 new_node
->previous
= NULL
;
546 device_info
->note_list
= new_node
;
547 device_info
->playing_notes_count
++;
550 if ( device_info->playing_notes_count > POLYPHONY )
554 DPRINT("Polyphony exceeded\n");
556 tail_node->previous->next = NULL;
558 HeapFree(heap, 0, tail_node);
560 device_info->playing_notes_count --;
564 LeaveCriticalSection(&device_lock
);
566 DPRINT("Note started - now playing %d notes\n", (int) device_info
->playing_notes_count
);
567 device_info
->refresh_notes
= TRUE
;
570 #ifndef CONTINUOUS_NOTES
571 ProcessPlayingNotes((PVOID
) device_info
);
574 return MMSYSERR_NOERROR
;
578 Decipher a short MIDI message (which is a MIDI message packed into a DWORD.)
579 This will set "running status", but does not take this into account when
580 processing messages (is this necessary?)
584 ProcessShortMidiMessage(
585 DeviceInfo
* device_info
,
594 status
= message
& 0x000000FF;
596 /* Deal with running status */
598 if ( status
< MIDI_NOTE_OFF
)
600 status
= device_info
->running_status
;
603 /* Ensure the status is sane! */
605 if ( status
< MIDI_NOTE_OFF
)
607 /* It's garbage, ignore it */
608 return MMSYSERR_NOERROR
;
611 /* Figure out the message category and channel */
613 category
= status
& 0xF0;
614 channel
= status
& 0x0F; /* we don't use this */
616 data1
= (message
& 0x0000FF00) >> 8;
617 data2
= (message
& 0x00FF0000) >> 16;
619 DPRINT("0x%x, %d, %d\n", (int) status
, (int) data1
, (int) data2
);
621 /* Filter drums (which are *usually* on channel 10) */
624 return MMSYSERR_NOERROR
;
627 /* Pass to the appropriate message handler */
633 PlayNote(device_info
, data1
, data2
);
639 StopNote(device_info
, data1
);
644 return MMSYSERR_NOERROR
;
648 #define PACK_MIDI(b1, b2, b3) \
649 ((b3 * 65536) + (b2 * 256) + b1);
653 Processes a "long" MIDI message (ie, a MIDI message contained within a
654 buffer.) This is intended for supporting SysEx data, or blocks of MIDI
655 events. However in our case we're only interested in short MIDI messages,
656 so we scan the buffer, and each time we encounter a valid status byte
657 we start recording it as a new event. Once 3 bytes or a new status is
658 received, the event is passed to the short message handler.
662 ProcessLongMidiMessage(
663 DeviceInfo
* device_info
,
666 unsigned int index
= 0;
667 UCHAR
* midi_bytes
= (UCHAR
*) header
->lpData
;
669 unsigned int msg_index
= 0;
672 /* Initialize the buffer */
673 msg
[0] = msg
[1] = msg
[2] = 0;
675 if ( ! ( header
->dwFlags
& MHDR_PREPARED
) )
677 DPRINT("Not prepared!\n");
678 return MIDIERR_UNPREPARED
;
681 DPRINT("Processing %d bytes of MIDI\n", (int) header
->dwBufferLength
);
683 while ( index
< header
->dwBufferLength
)
685 /* New status byte? ( = new event) */
686 if ( midi_bytes
[index
] & 0x80 )
690 /* Deal with the existing event */
694 short_msg
= PACK_MIDI(msg
[0], msg
[1], msg
[2]);
696 DPRINT("Complete msg is 0x%x %d %d\n", (int) msg
[0], (int) msg
[1], (int) msg
[2]);
697 ProcessShortMidiMessage(device_info
, short_msg
);
700 /* Set new running status and start recording the event */
701 DPRINT("Set new running status\n");
702 device_info
->running_status
= midi_bytes
[index
];
703 msg
[0] = midi_bytes
[index
];
707 /* Unexpected data byte? ( = re-use previous status) */
708 else if ( msg_index
== 0 )
710 if ( device_info
->running_status
& 0x80 )
712 DPRINT("Retrieving running status\n");
713 msg
[0] = device_info
->running_status
;
714 msg
[1] = midi_bytes
[index
];
721 /* Expected data ( = append to message until buffer full) */
724 DPRINT("Next byte...\n");
725 msg
[msg_index
] = midi_bytes
[index
];
732 short_msg
= PACK_MIDI(msg
[0], msg
[1], msg
[2]);
734 DPRINT("Complete msg is 0x%x %d %d\n", (int) msg
[0], (int) msg
[1], (int) msg
[2]);
735 ProcessShortMidiMessage(device_info
, short_msg
);
739 msg
[0] = msg
[1] = msg
[2] = 0;
747 We're meant to clear MHDR_DONE and set MHDR_INQUEUE but since we
748 deal with everything here and now we might as well just say so.
750 header
->dwFlags
|= MHDR_DONE
;
751 header
->dwFlags
&= ~ MHDR_INQUEUE
;
753 DPRINT("Success? %d\n", CallClient(the_device
, MOM_DONE
, (DWORD_PTR
) header
, 0));
755 return MMSYSERR_NOERROR
;
760 Exported function that receives messages from WINMM (the MME API.)
768 DWORD_PTR private_data
,
769 DWORD_PTR parameter1
,
770 DWORD_PTR parameter2
)
774 case MODM_GETNUMDEVS
:
776 /* Only one internal PC speaker device (and even that's too much) */
777 DPRINT("MODM_GETNUMDEVS\n");
781 case MODM_GETDEVCAPS
:
783 DPRINT("MODM_GETDEVCAPS\n");
784 return GetDeviceCapabilities((MIDIOUTCAPS
*) parameter1
);
789 DPRINT("MODM_OPEN\n");
791 return OpenDevice((DeviceInfo
**) private_data
,
792 (MIDIOPENDESC
*) parameter1
,
798 DPRINT("MODM_CLOSE\n");
799 return CloseDevice((DeviceInfo
*) private_data
);
804 return ProcessShortMidiMessage((DeviceInfo
*) private_data
, parameter1
);
809 /* We don't bother with this */
810 MIDIHDR
* hdr
= (MIDIHDR
*) parameter1
;
811 hdr
->dwFlags
|= MHDR_PREPARED
;
812 return MMSYSERR_NOERROR
;
815 case MODM_UNPREPARE
:
817 MIDIHDR
* hdr
= (MIDIHDR
*) parameter1
;
818 hdr
->dwFlags
&= ~MHDR_PREPARED
;
819 return MMSYSERR_NOERROR
;
824 DPRINT("LONGDATA\n");
825 return ProcessLongMidiMessage((DeviceInfo
*) private_data
, (MIDIHDR
*) parameter1
);
835 DPRINT("Not supported %d\n", message
);
837 return MMSYSERR_NOTSUPPORTED
;
857 DPRINT("DRV_LOAD\n");
862 DPRINT("DRV_FREE\n");
866 DPRINT("DRV_OPEN\n");
867 InitializeCriticalSection(&device_lock
);
871 DPRINT("DRV_CLOSE\n");
875 DPRINT("DRV_ENABLE\n");
879 DPRINT("DRV_DISABLE\n");
883 We don't provide configuration capabilities. This used to be
884 for things like I/O port, IRQ, DMA settings, etc.
887 case DRV_QUERYCONFIGURE
:
888 DPRINT("DRV_QUERYCONFIGURE\n");
892 DPRINT("DRV_CONFIGURE\n");
896 DPRINT("DRV_INSTALL\n");
897 return DRVCNF_RESTART
;
902 return DefDriverProc(driver_id
,