use an external command to play midi music on windows

this works around the SDL_mixer known issue that on
windows, controlling the music volume for midi playback
always changes the entire process volume, so also affects
effect sounds. unfortunately, Mix_SetMusicCMD doesn't
work on windows either. so we just detect midi file
playback and launch a trivial midi player in an external
process with a fixed volume.
This commit is contained in:
Tim Felgentreff 2022-03-29 22:01:14 +02:00
parent 4bb6e4d572
commit 9e11518abb
6 changed files with 549 additions and 28 deletions

View file

@ -1265,10 +1265,19 @@ if(WIN32 AND ENABLE_NSIS AND MAKENSIS_FOUND)
add_custom_target(nsis ALL DEPENDS Stratagus-${STRATAGUS_VERSION}${MAKENSIS_SUFFIX})
endif()
if (WIN32)
add_executable(midiplayer WIN32 "src/sound/win32/midiplayer.c")
set_target_properties(midiplayer PROPERTIES LINK_FLAGS "/SUBSYSTEM:CONSOLE")
set_target_properties(midiplayer PROPERTIES OUTPUT_NAME "stratagus-midiplayer")
endif()
########### install files ###############
install(TARGETS stratagus DESTINATION ${GAMEDIR})
install(TARGETS png2stratagus DESTINATION ${BINDIR})
if (WIN32)
install(TARGETS midiplayer DESTINATION ${GAMEDIR})
endif()
if(ENABLE_DOC AND DOXYGEN_FOUND)
install(FILES doc/stratagus.6 DESTINATION ${MANDIR})

View file

@ -88,8 +88,6 @@ extern bool IsEffectsEnabled();
/// Set the music finished callback
void SetMusicFinishedCallback(void (*callback)());
/// Play a music file
extern int PlayMusic(Mix_Music *sample);
/// Play a music file
extern int PlayMusic(const std::string &file);
/// Stop music playing
extern void StopMusic();

View file

@ -60,6 +60,119 @@ static bool EffectsEnabled = true;
static double VolumeScale = 1.0;
static int MusicVolume = 0;
static void (*MusicFinishedCallback)();
#ifdef USE_WIN32
static volatile bool threadWaiting = false;
static std::string externalFile;
static HANDLE hWaitingThread;
static PROCESS_INFORMATION pi;
DWORD WINAPI MyThreadFunction(LPVOID lpParam) {
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
if (threadWaiting) {
MusicFinishedCallback();
threadWaiting = false;
}
return 0;
}
static void killPlayingProcess() {
if (threadWaiting) {
threadWaiting = false;
TerminateProcess(pi.hProcess, 0);
WaitForSingleObject(hWaitingThread, INFINITE);
threadWaiting = false;
} else {
TerminateProcess(pi.hProcess, 0);
}
}
static bool External_Play(const std::string &file) {
if (threadWaiting && file == externalFile) {
return true;
}
static std::string midi = ".mid";
auto it = midi.begin();
if (file.size() > midi.size() &&
std::all_of(std::next(file.begin(), file.size() - midi.size()), file.end(), [&it](const char & c) { return c == ::tolower(*(it++)); })) {
// midi file, use external player, since windows vista+ does not allow midi volume control independent of process volume
std::string full_filename = LibraryFileName(file.c_str());
static const char* midiplayerExe = "stratagus-midiplayer.exe";
static const int midiplayerExeSz = strlen(midiplayerExe);
// set up a job so our children die with us
static bool firstRun = true;
static HANDLE hJob;
if (firstRun) {
hJob = CreateJobObject(NULL, NULL);
JOBOBJECT_BASIC_LIMIT_INFORMATION limitInfo;
ZeroMemory(&limitInfo, sizeof(limitInfo));
limitInfo.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
SetInformationJobObject(hJob, JobObjectBasicLimitInformation, &limitInfo, sizeof(limitInfo));
AssignProcessToJobObject(hJob, GetCurrentProcess());
firstRun = false;
}
int sz = midiplayerExeSz + 2 + 3 + 2 + full_filename.size() + 1; // exe + 2 spaces + 3 volume + 2 quotes + filename + nullbyte
char *cmdline = new char[sz];
snprintf(cmdline, sz, "%s %3d \"%s\"", midiplayerExe, std::min(MusicVolume, 255), full_filename.c_str());
DebugPrint("Using external command to play midi on windows: %s\n" _C_ cmdline);
killPlayingProcess();
STARTUPINFO si;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
bool result = true;
if (CreateProcess(NULL, cmdline, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
AssignProcessToJobObject(hJob, pi.hProcess);
externalFile = file;
hWaitingThread = CreateThread(NULL, 0, MyThreadFunction, NULL, 0, NULL);
threadWaiting = true;
} else {
result = false;
DebugPrint("CreateProcess failed (%d).\n" _C_ GetLastError());
}
delete[] cmdline;
return result;
}
killPlayingProcess();
return false;
}
static bool External_IsPlaying() {
return threadWaiting;
}
static bool External_Stop() {
if (External_IsPlaying()) {
killPlayingProcess();
return true;
}
return false;
}
static bool External_Volume(int volume, int oldVolume) {
if (External_IsPlaying() && externalFile.size() > 0) {
if (oldVolume != volume) {
External_Stop();
External_Play(externalFile);
}
return true;
}
return false;
}
#else
#define External_Play(file) false
#define External_IsPlaying() false
#define External_Stop()
#define External_Volume(volume) false
#endif
extern volatile bool MusicFinished;
/// Channels for sound effects and unit speech
@ -334,30 +447,10 @@ bool IsEffectsEnabled()
*/
void SetMusicFinishedCallback(void (*callback)())
{
MusicFinishedCallback = callback;
Mix_HookMusicFinished(callback);
}
/**
** Play a music file.
**
** @param sample Music sample.
**
** @return 0 if music is playing, -1 if not.
*/
int PlayMusic(Mix_Music *sample)
{
if (sample) {
Mix_VolumeMusic(MusicVolume);
MusicFinished = false;
Mix_PlayMusic(sample, 0);
Mix_VolumeMusic(MusicVolume / 4.0);
return 0;
} else {
DebugPrint("Could not play sample\n");
return -1;
}
}
/**
** Play a music file.
**
@ -371,8 +464,13 @@ int PlayMusic(const std::string &file)
return -1;
}
DebugPrint("play music %s\n" _C_ file.c_str());
Mix_Music *music = LoadMusic(file);
if (External_Play(file)) {
MusicFinished = false;
return 0;
}
Mix_Music *music = LoadMusic(file);
if (music) {
MusicFinished = false;
Mix_FadeInMusic(music, 0, 200);
@ -388,6 +486,9 @@ int PlayMusic(const std::string &file)
*/
void StopMusic()
{
if (External_Stop()) {
return;
}
Mix_FadeOutMusic(200);
}
@ -400,7 +501,11 @@ void SetMusicVolume(int volume)
{
// due to left-right separation, sound effect volume is effectively halfed,
// so we adjust the music
int oldVolume = MusicVolume;
MusicVolume = volume;
if (External_Volume(volume, oldVolume)) {
return;
}
Mix_VolumeMusic(volume / 4.0);
}

View file

@ -0,0 +1,411 @@
/*
* mididemo.c
*
* Created on: Dec 21, 2011
* Author: David J. Rager
* Email: djrager@fourthwoods.com
*
* This code is hereby released into the public domain per the Creative Commons
* Public Domain dedication.
*
* http://http://creativecommons.org/publicdomain/zero/1.0/
*/
#pragma comment(lib, "winmm.lib")
#include <windows.h>
#include <mmsystem.h>
#include <stdio.h>
#include <stdlib.h>
#define MAX_BUFFER_SIZE (512 * 12)
HANDLE event;
#pragma pack(push, 1)
struct _mid_header {
unsigned int id; // identifier "MThd"
unsigned int size; // always 6 in big-endian format
unsigned short format; // big-endian format
unsigned short tracks; // number of tracks, big-endian
unsigned short ticks; // number of ticks per quarter note, big-endian
};
struct _mid_track {
unsigned int id; // identifier "MTrk"
unsigned int length; // track length, big-endian
};
#pragma pack(pop)
struct trk {
struct _mid_track* track;
unsigned char* buf;
unsigned char last_event;
unsigned int absolute_time;
};
struct evt {
unsigned int absolute_time;
unsigned char* data;
unsigned char event;
};
static unsigned char* load_file(const unsigned char* filename, unsigned int* len)
{
unsigned char* buf;
unsigned int ret;
FILE* f = fopen((char*)filename, "rb");
if(f == NULL)
return 0;
fseek(f, 0, SEEK_END);
*len = ftell(f);
fseek(f, 0, SEEK_SET);
buf = (unsigned char*)malloc(*len);
if(buf == 0)
{
fclose(f);
return 0;
}
ret = fread(buf, 1, *len, f);
fclose(f);
if(ret != *len)
{
free(buf);
return 0;
}
return buf;
}
static unsigned long read_var_long(unsigned char* buf, unsigned int* bytesread)
{
unsigned long var = 0;
unsigned char c;
*bytesread = 0;
do
{
c = buf[(*bytesread)++];
var = (var << 7) + (c & 0x7f);
}
while(c & 0x80);
return var;
}
static unsigned short swap_bytes_short(unsigned short in)
{
return ((in << 8) | (in >> 8));
}
static unsigned long swap_bytes_long(unsigned long in)
{
unsigned short *p;
p = (unsigned short*)&in;
return ( (((unsigned long)swap_bytes_short(p[0])) << 16) |
(unsigned long)swap_bytes_short(p[1]));
}
static struct evt get_next_event(const struct trk* track)
{
unsigned char* buf;
struct evt e;
unsigned int bytesread;
unsigned int time;
buf = track->buf;
time = read_var_long(buf, &bytesread);
buf += bytesread;
e.absolute_time = track->absolute_time + time;
e.data = buf;
e.event = *e.data;
return e;
}
static int is_track_end(const struct evt* e)
{
if(e->event == 0xff) // meta-event?
if(*(e->data + 1) == 0x2f) // track end?
return 1;
return 0;
}
static void CALLBACK example9_callback(HMIDIOUT out, UINT msg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2)
{
switch (msg)
{
case MOM_DONE:
SetEvent(event);
break;
case MOM_POSITIONCB:
case MOM_OPEN:
case MOM_CLOSE:
break;
}
}
static unsigned int get_buffer(struct trk* tracks, unsigned int ntracks, unsigned int* out, unsigned int* outlen)
{
MIDIEVENT e, *p;
unsigned int streamlen = 0;
unsigned int i;
static unsigned int current_time = 0;
if(tracks == NULL || out == NULL || outlen == NULL)
return 0;
*outlen = 0;
while(TRUE)
{
unsigned int time = (unsigned int)-1;
unsigned int idx = -1;
struct evt evt;
unsigned char c;
if(((streamlen + 3) * sizeof(unsigned int)) >= MAX_BUFFER_SIZE)
break;
// get the next event
for(i = 0; i < ntracks; i++)
{
evt = get_next_event(&tracks[i]);
if(!(is_track_end(&evt)) && (evt.absolute_time < time))
{
time = evt.absolute_time;
idx = i;
}
}
// if idx == -1 then all the tracks have been read up to the end of track mark
if(idx == -1)
break; // we're done
e.dwStreamID = 0; // always 0
evt = get_next_event(&tracks[idx]);
tracks[idx].absolute_time = evt.absolute_time;
e.dwDeltaTime = tracks[idx].absolute_time - current_time;
current_time = tracks[idx].absolute_time;
if(!(evt.event & 0x80)) // running mode
{
unsigned char last = tracks[idx].last_event;
c = *evt.data++; // get the first data byte
e.dwEvent = ((unsigned long)MEVT_SHORTMSG << 24) |
((unsigned long)last) |
((unsigned long)c << 8);
if(!((last & 0xf0) == 0xc0 || (last & 0xf0) == 0xd0))
{
c = *evt.data++; // get the second data byte
e.dwEvent |= ((unsigned long)c << 16);
}
p = (MIDIEVENT*)&out[streamlen];
*p = e;
streamlen += 3;
tracks[idx].buf = evt.data;
}
else if(evt.event == 0xff) // meta-event
{
evt.data++; // skip the event byte
unsigned char meta = *evt.data++; // read the meta-event byte
unsigned int len;
switch(meta)
{
case 0x51: // only care about tempo events
{
unsigned char a, b, c;
len = *evt.data++; // get the length byte, should be 3
a = *evt.data++;
b = *evt.data++;
c = *evt.data++;
e.dwEvent = ((unsigned long)MEVT_TEMPO << 24) |
((unsigned long)a << 16) |
((unsigned long)b << 8) |
((unsigned long)c << 0);
p = (MIDIEVENT*)&out[streamlen];
*p = e;
streamlen += 3;
}
break;
default: // skip all other meta events
len = *evt.data++; // get the length byte
evt.data += len;
break;
}
tracks[idx].buf = evt.data;
}
else if((evt.event & 0xf0) != 0xf0) // normal command
{
tracks[idx].last_event = evt.event;
evt.data++; // skip the event byte
c = *evt.data++; // get the first data byte
e.dwEvent = ((unsigned long)MEVT_SHORTMSG << 24) |
((unsigned long)evt.event << 0) |
((unsigned long)c << 8);
if(!((evt.event & 0xf0) == 0xc0 || (evt.event & 0xf0) == 0xd0))
{
c = *evt.data++; // get the second data byte
e.dwEvent |= ((unsigned long)c << 16);
}
p = (MIDIEVENT*)&out[streamlen];
*p = e;
streamlen += 3;
tracks[idx].buf = evt.data;
}
}
*outlen = streamlen * sizeof(unsigned int);
return 1;
}
unsigned int example9(char* filename, int volume)
{
unsigned char* midibuf = NULL;
unsigned int midilen = 0;
struct _mid_header* hdr = NULL;
unsigned int i;
unsigned short ntracks = 0;
struct trk* tracks = NULL;
unsigned int streambufsize = MAX_BUFFER_SIZE;
unsigned int* streambuf = NULL;
unsigned int streamlen = 0;
HMIDISTRM out;
MIDIPROPTIMEDIV prop;
MIDIHDR mhdr;
unsigned int device = 0;
midibuf = load_file((unsigned char*)filename, &midilen);
if(midibuf == NULL)
{
printf("could not open %s\n", filename);
return 0;
}
hdr = (struct _mid_header*)midibuf;
midibuf += sizeof(struct _mid_header);
ntracks = swap_bytes_short(hdr->tracks);
tracks = (struct trk*)malloc(ntracks * sizeof(struct trk));
if(tracks == NULL)
goto error1;
for(i = 0; i < ntracks; i++)
{
tracks[i].track = (struct _mid_track*)midibuf;
tracks[i].buf = midibuf + sizeof(struct _mid_track);
tracks[i].absolute_time = 0;
tracks[i].last_event = 0;
midibuf += sizeof(struct _mid_track) + swap_bytes_long(tracks[i].track->length);
}
streambuf = (unsigned int*)malloc(sizeof(unsigned int) * streambufsize);
if(streambuf == NULL)
goto error2;
memset(streambuf, 0, sizeof(unsigned int) * streambufsize);
if ((event = CreateEvent(0, FALSE, FALSE, 0)) == NULL)
goto error3;
if (midiStreamOpen(&out, &device, 1, (DWORD)example9_callback, 0, CALLBACK_FUNCTION) != MMSYSERR_NOERROR)
goto error4;
prop.cbStruct = sizeof(MIDIPROPTIMEDIV);
prop.dwTimeDiv = swap_bytes_short(hdr->ticks);
if(midiStreamProperty(out, (LPBYTE)&prop, MIDIPROP_SET|MIDIPROP_TIMEDIV) != MMSYSERR_NOERROR)
goto error5;
mhdr.lpData = (char*)streambuf;
mhdr.dwBufferLength = mhdr.dwBytesRecorded = streambufsize;
mhdr.dwFlags = 0;
if(midiOutPrepareHeader((HMIDIOUT)out, &mhdr, sizeof(MIDIHDR)) != MMSYSERR_NOERROR)
goto error5;
if(midiStreamRestart(out) != MMSYSERR_NOERROR)
goto error6;
if (midiOutSetVolume((HMIDIOUT)out, (DWORD)((volume & 0xFF) << 4)) != MMSYSERR_NOERROR)
goto error6;
printf("buffering...\n");
get_buffer(tracks, ntracks, streambuf, &streamlen);
while(streamlen > 0)
{
mhdr.dwBytesRecorded = streamlen;
if(midiStreamOut(out, &mhdr, sizeof(MIDIHDR)) != MMSYSERR_NOERROR)
goto error7;
WaitForSingleObject(event, INFINITE);
printf("buffering...\n");
get_buffer(tracks, ntracks, streambuf, &streamlen);
}
printf("done.\n");
error7:
midiOutReset((HMIDIOUT)out);
error6:
midiOutUnprepareHeader((HMIDIOUT)out, &mhdr, sizeof(MIDIHDR));
error5:
midiStreamClose(out);
error4:
CloseHandle(event);
error3:
free(streambuf);
error2:
free(tracks);
error1:
free(hdr);
return(0);
}
int main(int argc, char* argv[])
{
if(argc != 3) {
return printf("Usage: %s <volume 0-255> <filename.mid>\n", argv[0]);
}
example9(argv[2], atoi(argv[1]));
return 0;
}

View file

@ -361,10 +361,7 @@ int PlayMovie(const std::string &name)
}
StopMusic();
Mix_Music *sample = LoadMusic(filename);
if (sample) {
PlayMusic(sample);
}
PlayMusic(filename);
EventCallback callbacks;

View file

@ -215,6 +215,7 @@ Section "${NAME}"
SetOutPath $INSTDIR
File "${EXE}"
File midiplayer.exe
File *.dll
WriteRegStr HKLM "${REGKEY}" "DisplayName" "${NAME}"
WriteRegStr HKLM "${REGKEY}" "UninstallString" "$\"$INSTDIR\${UNINSTALL}$\""