ref: aa8172e9002ff31a8a2f8daa74fbd9f49c44b3b6
dir: /src/i_winmusic.c/
// // Copyright(C) 2021 Roman Fomin // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // DESCRIPTION: // Windows native MIDI #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <mmsystem.h> #include <stdio.h> #include <stdlib.h> #include "doomtype.h" #include "m_misc.h" #include "midifile.h" static HMIDISTRM hMidiStream; static HANDLE hBufferReturnEvent; static HANDLE hExitEvent; static HANDLE hPlayerThread; // This is a reduced Windows MIDIEVENT structure for MEVT_F_SHORT // type of events. typedef struct { DWORD dwDeltaTime; DWORD dwStreamID; // always 0 DWORD dwEvent; } native_event_t; typedef struct { native_event_t *native_events; int num_events; int position; boolean looping; } win_midi_song_t; static win_midi_song_t song; typedef struct { midi_track_iter_t *iter; int absolute_time; } win_midi_track_t; static float volume_factor = 1.0; // Save the last volume for each MIDI channel. static int channel_volume[MIDI_CHANNELS_PER_TRACK]; // Macros for use with the Windows MIDIEVENT dwEvent field. #define MIDIEVENT_CHANNEL(x) (x & 0x0000000F) #define MIDIEVENT_TYPE(x) (x & 0x000000F0) #define MIDIEVENT_DATA1(x) ((x & 0x0000FF00) >> 8) #define MIDIEVENT_VOLUME(x) ((x & 0x007F0000) >> 16) // Maximum of 4 events in the buffer for faster volume updates. #define STREAM_MAX_EVENTS 4 typedef struct { native_event_t events[STREAM_MAX_EVENTS]; int num_events; MIDIHDR MidiStreamHdr; } buffer_t; static buffer_t buffer; static const int volume_correction[] = { 0, 4, 7, 11, 13, 14, 16, 18, 21, 22, 23, 24, 24, 24, 25, 25, 25, 26, 26, 27, 27, 27, 28, 28, 29, 29, 29, 30, 30, 31, 31, 32, 32, 32, 33, 33, 34, 34, 35, 35, 36, 37, 37, 38, 38, 39, 39, 40, 40, 41, 42, 42, 43, 43, 44, 45, 45, 46, 47, 47, 48, 49, 49, 50, 51, 52, 52, 53, 54, 55, 56, 56, 57, 58, 59, 60, 61, 62, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 77, 78, 79, 80, 81, 82, 84, 85, 86, 87, 89, 90, 91, 92, 94, 95, 96, 98, 99, 101, 102, 104, 105, 107, 108, 110, 112, 113, 115, 117, 118, 120, 122, 123, 125, 127 }; // Message box for midiStream errors. static void MidiErrorMessageBox(DWORD dwError) { char szErrorBuf[MAXERRORLENGTH]; MMRESULT mmr; mmr = midiOutGetErrorText(dwError, (LPSTR) szErrorBuf, MAXERRORLENGTH); if (mmr == MMSYSERR_NOERROR) { MessageBox(NULL, szErrorBuf, "midiStream Error", MB_ICONEXCLAMATION); } else { fprintf(stderr, "Unknown midiStream error.\n"); } } // Fill the buffer with MIDI events, adjusting the volume as needed. static void FillBuffer(void) { int i; for (i = 0; i < STREAM_MAX_EVENTS; ++i) { native_event_t *event = &buffer.events[i]; if (song.position >= song.num_events) { if (song.looping) { song.position = 0; } else { break; } } *event = song.native_events[song.position]; if (MIDIEVENT_TYPE(event->dwEvent) == MIDI_EVENT_CONTROLLER && MIDIEVENT_DATA1(event->dwEvent) == MIDI_CONTROLLER_MAIN_VOLUME) { int volume = MIDIEVENT_VOLUME(event->dwEvent); channel_volume[MIDIEVENT_CHANNEL(event->dwEvent)] = volume; volume *= volume_factor; volume = volume_correction[volume]; event->dwEvent = (event->dwEvent & 0xFF00FFFF) | ((volume & 0x7F) << 16); } song.position++; } buffer.num_events = i; } // Queue MIDI events. static void StreamOut(void) { MIDIHDR *hdr = &buffer.MidiStreamHdr; MMRESULT mmr; int num_events = buffer.num_events; if (num_events == 0) { return; } hdr->lpData = (LPSTR)buffer.events; hdr->dwBytesRecorded = num_events * sizeof(native_event_t); mmr = midiStreamOut(hMidiStream, hdr, sizeof(MIDIHDR)); if (mmr != MMSYSERR_NOERROR) { MidiErrorMessageBox(mmr); } } // midiStream callback. static void CALLBACK MidiStreamProc(HMIDIIN hMidi, UINT uMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2) { if (uMsg == MOM_DONE) { SetEvent(hBufferReturnEvent); } } // The Windows API documentation states: "Applications should not call any // multimedia functions from inside the callback function, as doing so can // cause a deadlock." We use thread to avoid possible deadlocks. static DWORD WINAPI PlayerProc(void) { HANDLE events[2] = { hBufferReturnEvent, hExitEvent }; while (1) { switch (WaitForMultipleObjects(2, events, FALSE, INFINITE)) { case WAIT_OBJECT_0: FillBuffer(); StreamOut(); break; case WAIT_OBJECT_0 + 1: return 0; } } return 0; } // Convert a multi-track MIDI file to an array of Windows MIDIEVENT structures. static void MIDItoStream(midi_file_t *file) { int i; int num_tracks = MIDI_NumTracks(file); win_midi_track_t *tracks = malloc(num_tracks * sizeof(win_midi_track_t)); int current_time = 0; for (i = 0; i < num_tracks; ++i) { tracks[i].iter = MIDI_IterateTrack(file, i); tracks[i].absolute_time = 0; } song.native_events = calloc(MIDI_NumEvents(file), sizeof(native_event_t)); while (1) { midi_event_t *event; DWORD data = 0; int min_time = INT_MAX; int idx = -1; // Look for an event with a minimal delta time. for (i = 0; i < num_tracks; ++i) { int time = 0; if (tracks[i].iter == NULL) { continue; } time = tracks[i].absolute_time + MIDI_GetDeltaTime(tracks[i].iter); if (time < min_time) { min_time = time; idx = i; } } // No more MIDI events left, end the loop. if (idx == -1) { break; } tracks[idx].absolute_time = min_time; if (!MIDI_GetNextEvent(tracks[idx].iter, &event)) { MIDI_FreeIterator(tracks[idx].iter); tracks[idx].iter = NULL; continue; } switch ((int)event->event_type) { case MIDI_EVENT_META: if (event->data.meta.type == MIDI_META_SET_TEMPO) { data = event->data.meta.data[2] | (event->data.meta.data[1] << 8) | (event->data.meta.data[0] << 16) | (MEVT_TEMPO << 24); } break; case MIDI_EVENT_NOTE_OFF: case MIDI_EVENT_NOTE_ON: case MIDI_EVENT_AFTERTOUCH: case MIDI_EVENT_CONTROLLER: case MIDI_EVENT_PITCH_BEND: data = event->event_type | event->data.channel.channel | (event->data.channel.param1 << 8) | (event->data.channel.param2 << 16) | (MEVT_SHORTMSG << 24); break; case MIDI_EVENT_PROGRAM_CHANGE: case MIDI_EVENT_CHAN_AFTERTOUCH: data = event->event_type | event->data.channel.channel | (event->data.channel.param1 << 8) | (0 << 16) | (MEVT_SHORTMSG << 24); break; } if (data) { native_event_t *native_event = &song.native_events[song.num_events]; native_event->dwDeltaTime = min_time - current_time; native_event->dwStreamID = 0; native_event->dwEvent = data; song.num_events++; current_time = min_time; } } if (tracks) { free(tracks); } } boolean I_WIN_InitMusic(void) { UINT MidiDevice = MIDI_MAPPER; MIDIHDR *hdr = &buffer.MidiStreamHdr; MMRESULT mmr; mmr = midiStreamOpen(&hMidiStream, &MidiDevice, (DWORD)1, (DWORD_PTR)MidiStreamProc, (DWORD_PTR)NULL, CALLBACK_FUNCTION); if (mmr != MMSYSERR_NOERROR) { MidiErrorMessageBox(mmr); return false; } hdr->lpData = (LPSTR)buffer.events; hdr->dwBytesRecorded = 0; hdr->dwBufferLength = STREAM_MAX_EVENTS * sizeof(native_event_t); hdr->dwFlags = 0; hdr->dwOffset = 0; mmr = midiOutPrepareHeader((HMIDIOUT)hMidiStream, hdr, sizeof(MIDIHDR)); if (mmr != MMSYSERR_NOERROR) { MidiErrorMessageBox(mmr); return false; } hBufferReturnEvent = CreateEvent(NULL, FALSE, FALSE, NULL); hExitEvent = CreateEvent(NULL, FALSE, FALSE, NULL); return true; } void I_WIN_SetMusicVolume(int volume) { int i; volume_factor = (float)volume / 127; // Send MIDI controller events to adjust the volume. for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i) { DWORD msg = 0; int value = channel_volume[i] * volume_factor; value = volume_correction[value]; msg = MIDI_EVENT_CONTROLLER | i | (MIDI_CONTROLLER_MAIN_VOLUME << 8) | (value << 16); midiOutShortMsg((HMIDIOUT)hMidiStream, msg); } } void I_WIN_StopSong(void) { MMRESULT mmr; if (hPlayerThread) { SetEvent(hExitEvent); WaitForSingleObject(hPlayerThread, INFINITE); CloseHandle(hPlayerThread); hPlayerThread = NULL; } mmr = midiStreamStop(hMidiStream); if (mmr != MMSYSERR_NOERROR) { MidiErrorMessageBox(mmr); } mmr = midiOutReset((HMIDIOUT)hMidiStream); if (mmr != MMSYSERR_NOERROR) { MidiErrorMessageBox(mmr); } } void I_WIN_PlaySong(boolean looping) { MMRESULT mmr; song.looping = looping; hPlayerThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)PlayerProc, 0, 0, 0); SetThreadPriority(hPlayerThread, THREAD_PRIORITY_TIME_CRITICAL); mmr = midiStreamRestart(hMidiStream); if (mmr != MMSYSERR_NOERROR) { MidiErrorMessageBox(mmr); } } boolean I_WIN_RegisterSong(char *filename) { int i; midi_file_t *file; MIDIPROPTIMEDIV timediv; MIDIPROPTEMPO tempo; MMRESULT mmr; file = MIDI_LoadFile(filename); if (file == NULL) { fprintf(stderr, "I_WIN_RegisterSong: Failed to load MID.\n"); return false; } // Initialize channels volume. for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i) { channel_volume[i] = 100; } timediv.cbStruct = sizeof(MIDIPROPTIMEDIV); timediv.dwTimeDiv = MIDI_GetFileTimeDivision(file); mmr = midiStreamProperty(hMidiStream, (LPBYTE)&timediv, MIDIPROP_SET | MIDIPROP_TIMEDIV); if (mmr != MMSYSERR_NOERROR) { MidiErrorMessageBox(mmr); return false; } // Set initial tempo. tempo.cbStruct = sizeof(MIDIPROPTIMEDIV); tempo.dwTempo = 500000; // 120 bmp mmr = midiStreamProperty(hMidiStream, (LPBYTE)&tempo, MIDIPROP_SET | MIDIPROP_TEMPO); if (mmr != MMSYSERR_NOERROR) { MidiErrorMessageBox(mmr); return false; } MIDItoStream(file); MIDI_FreeFile(file); ResetEvent(hBufferReturnEvent); ResetEvent(hExitEvent); FillBuffer(); StreamOut(); return true; } void I_WIN_UnRegisterSong(void) { if (song.native_events) { free(song.native_events); song.native_events = NULL; } song.num_events = 0; song.position = 0; } void I_WIN_ShutdownMusic(void) { MIDIHDR *hdr = &buffer.MidiStreamHdr; MMRESULT mmr; I_WIN_StopSong(); mmr = midiOutUnprepareHeader((HMIDIOUT)hMidiStream, hdr, sizeof(MIDIHDR)); if (mmr != MMSYSERR_NOERROR) { MidiErrorMessageBox(mmr); } mmr = midiStreamClose(hMidiStream); if (mmr != MMSYSERR_NOERROR) { MidiErrorMessageBox(mmr); } hMidiStream = NULL; CloseHandle(hBufferReturnEvent); CloseHandle(hExitEvent); } #endif