ref: 09e77d155e28fb036cb2eca375ffbb2c51a726f8
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;
// 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);
}
}
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)
{
volume_factor = (float)volume / 127;
UpdateVolume();
}
void I_WIN_StopSong(void)
{
int i;
MMRESULT mmr;
if (hPlayerThread)
{
SetEvent(hExitEvent);
WaitForSingleObject(hPlayerThread, INFINITE);
CloseHandle(hPlayerThread);
hPlayerThread = NULL;
}
for (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);
}
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