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 */
503 NoteNode
* tail_node
= NULL
;
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
;
525 new_node
= HeapAlloc(heap
, HEAP_ZERO_MEMORY
, sizeof(NoteNode
));
529 LeaveCriticalSection(&device_lock
);
530 return MMSYSERR_NOMEM
;
533 new_node
->note
= note
;
534 new_node
->velocity
= velocity
;
537 Prepend to the playing notes list. If exceeding polyphony,
538 remove the oldest note (which will be at the tail.)
541 if ( device_info
->note_list
)
542 device_info
->note_list
->previous
= new_node
;
544 new_node
->next
= device_info
->note_list
;
545 new_node
->previous
= NULL
;
547 device_info
->note_list
= new_node
;
548 device_info
->playing_notes_count
++;
551 if ( device_info->playing_notes_count > POLYPHONY )
555 DPRINT("Polyphony exceeded\n");
557 tail_node->previous->next = NULL;
559 HeapFree(heap, 0, tail_node);
561 device_info->playing_notes_count --;
565 LeaveCriticalSection(&device_lock
);
567 DPRINT("Note started - now playing %d notes\n", (int) device_info
->playing_notes_count
);
568 device_info
->refresh_notes
= TRUE
;
571 #ifndef CONTINUOUS_NOTES
572 ProcessPlayingNotes((PVOID
) device_info
);
575 return MMSYSERR_NOERROR
;
579 Decipher a short MIDI message (which is a MIDI message packed into a DWORD.)
580 This will set "running status", but does not take this into account when
581 processing messages (is this necessary?)
585 ProcessShortMidiMessage(
586 DeviceInfo
* device_info
,
595 status
= message
& 0x000000FF;
597 /* Deal with running status */
599 if ( status
< MIDI_NOTE_OFF
)
601 status
= device_info
->running_status
;
604 /* Ensure the status is sane! */
606 if ( status
< MIDI_NOTE_OFF
)
608 /* It's garbage, ignore it */
609 return MMSYSERR_NOERROR
;
612 /* Figure out the message category and channel */
614 category
= status
& 0xF0;
615 channel
= status
& 0x0F; /* we don't use this */
617 data1
= (message
& 0x0000FF00) >> 8;
618 data2
= (message
& 0x00FF0000) >> 16;
620 DPRINT("0x%x, %d, %d\n", (int) status
, (int) data1
, (int) data2
);
622 /* Filter drums (which are *usually* on channel 10) */
625 return MMSYSERR_NOERROR
;
628 /* Pass to the appropriate message handler */
634 PlayNote(device_info
, data1
, data2
);
640 StopNote(device_info
, data1
);
645 return MMSYSERR_NOERROR
;
649 #define PACK_MIDI(b1, b2, b3) \
650 ((b3 * 65536) + (b2 * 256) + b1);
654 Processes a "long" MIDI message (ie, a MIDI message contained within a
655 buffer.) This is intended for supporting SysEx data, or blocks of MIDI
656 events. However in our case we're only interested in short MIDI messages,
657 so we scan the buffer, and each time we encounter a valid status byte
658 we start recording it as a new event. Once 3 bytes or a new status is
659 received, the event is passed to the short message handler.
663 ProcessLongMidiMessage(
664 DeviceInfo
* device_info
,
667 unsigned int index
= 0;
668 UCHAR
* midi_bytes
= (UCHAR
*) header
->lpData
;
670 unsigned int msg_index
= 0;
673 /* Initialize the buffer */
674 msg
[0] = msg
[1] = msg
[2] = 0;
676 if ( ! ( header
->dwFlags
& MHDR_PREPARED
) )
678 DPRINT("Not prepared!\n");
679 return MIDIERR_UNPREPARED
;
682 DPRINT("Processing %d bytes of MIDI\n", (int) header
->dwBufferLength
);
684 while ( index
< header
->dwBufferLength
)
686 /* New status byte? ( = new event) */
687 if ( midi_bytes
[index
] & 0x80 )
691 /* Deal with the existing event */
695 short_msg
= PACK_MIDI(msg
[0], msg
[1], msg
[2]);
697 DPRINT("Complete msg is 0x%x %d %d\n", (int) msg
[0], (int) msg
[1], (int) msg
[2]);
698 ProcessShortMidiMessage(device_info
, short_msg
);
701 /* Set new running status and start recording the event */
702 DPRINT("Set new running status\n");
703 device_info
->running_status
= midi_bytes
[index
];
704 msg
[0] = midi_bytes
[index
];
708 /* Unexpected data byte? ( = re-use previous status) */
709 else if ( msg_index
== 0 )
711 if ( device_info
->running_status
& 0x80 )
713 DPRINT("Retrieving running status\n");
714 msg
[0] = device_info
->running_status
;
715 msg
[1] = midi_bytes
[index
];
722 /* Expected data ( = append to message until buffer full) */
725 DPRINT("Next byte...\n");
726 msg
[msg_index
] = midi_bytes
[index
];
733 short_msg
= PACK_MIDI(msg
[0], msg
[1], msg
[2]);
735 DPRINT("Complete msg is 0x%x %d %d\n", (int) msg
[0], (int) msg
[1], (int) msg
[2]);
736 ProcessShortMidiMessage(device_info
, short_msg
);
740 msg
[0] = msg
[1] = msg
[2] = 0;
748 We're meant to clear MHDR_DONE and set MHDR_INQUEUE but since we
749 deal with everything here and now we might as well just say so.
751 header
->dwFlags
|= MHDR_DONE
;
752 header
->dwFlags
&= ~ MHDR_INQUEUE
;
754 DPRINT("Success? %d\n", CallClient(the_device
, MOM_DONE
, (DWORD_PTR
) header
, 0));
756 return MMSYSERR_NOERROR
;
761 Exported function that receives messages from WINMM (the MME API.)
769 DWORD_PTR private_data
,
770 DWORD_PTR parameter1
,
771 DWORD_PTR parameter2
)
775 case MODM_GETNUMDEVS
:
777 /* Only one internal PC speaker device (and even that's too much) */
778 DPRINT("MODM_GETNUMDEVS\n");
782 case MODM_GETDEVCAPS
:
784 DPRINT("MODM_GETDEVCAPS\n");
785 return GetDeviceCapabilities((MIDIOUTCAPS
*) parameter1
);
790 DPRINT("MODM_OPEN\n");
792 return OpenDevice((DeviceInfo
**) private_data
,
793 (MIDIOPENDESC
*) parameter1
,
799 DPRINT("MODM_CLOSE\n");
800 return CloseDevice((DeviceInfo
*) private_data
);
805 return ProcessShortMidiMessage((DeviceInfo
*) private_data
, parameter1
);
810 /* We don't bother with this */
811 MIDIHDR
* hdr
= (MIDIHDR
*) parameter1
;
812 hdr
->dwFlags
|= MHDR_PREPARED
;
813 return MMSYSERR_NOERROR
;
816 case MODM_UNPREPARE
:
818 MIDIHDR
* hdr
= (MIDIHDR
*) parameter1
;
819 hdr
->dwFlags
&= ~MHDR_PREPARED
;
820 return MMSYSERR_NOERROR
;
825 DPRINT("LONGDATA\n");
826 return ProcessLongMidiMessage((DeviceInfo
*) private_data
, (MIDIHDR
*) parameter1
);
836 DPRINT("Not supported %d\n", message
);
838 return MMSYSERR_NOTSUPPORTED
;
858 DPRINT("DRV_LOAD\n");
863 DPRINT("DRV_FREE\n");
867 DPRINT("DRV_OPEN\n");
868 InitializeCriticalSection(&device_lock
);
872 DPRINT("DRV_CLOSE\n");
876 DPRINT("DRV_ENABLE\n");
880 DPRINT("DRV_DISABLE\n");
884 We don't provide configuration capabilities. This used to be
885 for things like I/O port, IRQ, DMA settings, etc.
888 case DRV_QUERYCONFIGURE
:
889 DPRINT("DRV_QUERYCONFIGURE\n");
893 DPRINT("DRV_CONFIGURE\n");
897 DPRINT("DRV_INSTALL\n");
898 return DRVCNF_RESTART
;
903 return DefDriverProc(driver_id
,