ref: 7bb12317044d04b539649041eb3ea69350a1320b
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 <math.h> #include <stdio.h> #include <stdlib.h> #include "doomtype.h" #include "m_misc.h" #include "midifile.h" #include "i_sound.h" #include "i_winmusic.h" #define BETWEEN(l,u,x) (((l)>(x))?(l):((x)>(u))?(u):(x)) #define REVERB_MIN 0 #define REVERB_MAX 127 #define CHORUS_MIN 0 #define CHORUS_MAX 127 char *winmm_midi_device = NULL; int winmm_reverb_level = 40; int winmm_chorus_level = 0; 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; // 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; 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); } } static void UpdateVolume(void) { int i; // 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; msg = MIDI_EVENT_CONTROLLER | i | (MIDI_CONTROLLER_MAIN_VOLUME << 8) | (value << 16); midiOutShortMsg((HMIDIOUT)hMidiStream, msg); } } void ResetDevice(void) { for (int i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i) { DWORD msg = 0; // RPN sequence to adjust pitch bend range (RPN value 0x0000) msg = MIDI_EVENT_CONTROLLER | i | 0x65 << 8 | 0x00 << 16; midiOutShortMsg((HMIDIOUT)hMidiStream, msg); msg = MIDI_EVENT_CONTROLLER | i | 0x64 << 8 | 0x00 << 16; midiOutShortMsg((HMIDIOUT)hMidiStream, msg); // reset pitch bend range to central tuning +/- 2 semitones and 0 cents msg = MIDI_EVENT_CONTROLLER | i | 0x06 << 8 | 0x02 << 16; midiOutShortMsg((HMIDIOUT)hMidiStream, msg); msg = MIDI_EVENT_CONTROLLER | i | 0x26 << 8 | 0x00 << 16; midiOutShortMsg((HMIDIOUT)hMidiStream, msg); // end of RPN sequence msg = MIDI_EVENT_CONTROLLER | i | 0x64 << 8 | 0x7F << 16; midiOutShortMsg((HMIDIOUT)hMidiStream, msg); msg = MIDI_EVENT_CONTROLLER | i | 0x65 << 8 | 0x7F << 16; midiOutShortMsg((HMIDIOUT)hMidiStream, msg); // reset all controllers msg = MIDI_EVENT_CONTROLLER | i | 0x79 << 8 | 0x00 << 16; midiOutShortMsg((HMIDIOUT)hMidiStream, msg); // reset pan to 64 (center) msg = MIDI_EVENT_CONTROLLER | i | 0x0A << 8 | 0x40 << 16; midiOutShortMsg((HMIDIOUT)hMidiStream, msg); // reset reverb and other effect controllers msg = MIDI_EVENT_CONTROLLER | i | 0x5B << 8 | winmm_reverb_level << 16; midiOutShortMsg((HMIDIOUT)hMidiStream, msg); msg = MIDI_EVENT_CONTROLLER | i | 0x5C << 8 | 0x00 << 16; // tremolo midiOutShortMsg((HMIDIOUT)hMidiStream, msg); msg = MIDI_EVENT_CONTROLLER | i | 0x5D << 8 | winmm_chorus_level << 16; midiOutShortMsg((HMIDIOUT)hMidiStream, msg); msg = MIDI_EVENT_CONTROLLER | i | 0x5E << 8 | 0x00 << 16; // detune midiOutShortMsg((HMIDIOUT)hMidiStream, msg); msg = MIDI_EVENT_CONTROLLER | i | 0x5F << 8 | 0x00 << 16; // phaser midiOutShortMsg((HMIDIOUT)hMidiStream, msg); } } boolean I_WIN_InitMusic(void) { UINT MidiDevice; int all_devices; int i; MIDIHDR *hdr = &buffer.MidiStreamHdr; MIDIOUTCAPS mcaps; MMRESULT mmr; // find the midi device that matches the saved one if (winmm_midi_device != NULL) { all_devices = midiOutGetNumDevs() + 1; // include MIDI_MAPPER for (i = 0; i < all_devices; ++i) { // start from device id -1 (MIDI_MAPPER) mmr = midiOutGetDevCaps(i - 1, &mcaps, sizeof(mcaps)); if (mmr == MMSYSERR_NOERROR) { if (strstr(winmm_midi_device, mcaps.szPname)) { MidiDevice = i - 1; break; } } if (i == all_devices - 1) { // give up and use MIDI_MAPPER free(winmm_midi_device); winmm_midi_device = NULL; } } } if (winmm_midi_device == NULL) { MidiDevice = MIDI_MAPPER; mmr = midiOutGetDevCaps(MIDI_MAPPER, &mcaps, sizeof(mcaps)); if (mmr == MMSYSERR_NOERROR) { winmm_midi_device = M_StringDuplicate(mcaps.szPname); } } 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); winmm_reverb_level = BETWEEN(REVERB_MIN, REVERB_MAX, winmm_reverb_level); winmm_chorus_level = BETWEEN(CHORUS_MIN, CHORUS_MAX, winmm_chorus_level); ResetDevice(); return true; } void I_WIN_SetMusicVolume(int volume) { volume_factor = sqrt((float)volume / 120); UpdateVolume(); } void I_WIN_StopSong(void) { MMRESULT mmr; if (hPlayerThread) { SetEvent(hExitEvent); WaitForSingleObject(hPlayerThread, INFINITE); CloseHandle(hPlayerThread); hPlayerThread = NULL; } ResetDevice(); 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); } UpdateVolume(); } void I_WIN_PauseSong(void) { MMRESULT mmr; mmr = midiStreamPause(hMidiStream); if (mmr != MMSYSERR_NOERROR) { MidiErrorMessageBox(mmr); } } void I_WIN_ResumeSong(void) { MMRESULT mmr; 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(); I_WIN_UnRegisterSong(); 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