diff --git a/CMakeLists.txt b/CMakeLists.txt index a7a45d4cd..94ed880db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/src/include/sound_server.h b/src/include/sound_server.h index 0aab5e7f8..d86906fe6 100644 --- a/src/include/sound_server.h +++ b/src/include/sound_server.h @@ -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(); diff --git a/src/sound/sound_server.cpp b/src/sound/sound_server.cpp index 409acfe4a..55ca83a9e 100644 --- a/src/sound/sound_server.cpp +++ b/src/sound/sound_server.cpp @@ -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); } diff --git a/src/sound/win32/midiplayer.c b/src/sound/win32/midiplayer.c new file mode 100644 index 000000000..3aecb17a2 --- /dev/null +++ b/src/sound/win32/midiplayer.c @@ -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*)∈ + + 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; +} diff --git a/src/video/movie.cpp b/src/video/movie.cpp index bf89def97..f1f849108 100644 --- a/src/video/movie.cpp +++ b/src/video/movie.cpp @@ -361,10 +361,7 @@ int PlayMovie(const std::string &name) } StopMusic(); - Mix_Music *sample = LoadMusic(filename); - if (sample) { - PlayMusic(sample); - } + PlayMusic(filename); EventCallback callbacks; diff --git a/src/win32/stratagus.nsi b/src/win32/stratagus.nsi index 53572fcf4..2e1355eb2 100644 --- a/src/win32/stratagus.nsi +++ b/src/win32/stratagus.nsi @@ -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}$\""