diff --git a/src/sound/sound_server.cpp b/src/sound/sound_server.cpp index dc98e3d84..dbec878bc 100644 --- a/src/sound/sound_server.cpp +++ b/src/sound/sound_server.cpp @@ -35,6 +35,8 @@ -- Includes ----------------------------------------------------------------------------*/ +#include <numeric> + #include "stratagus.h" #include "sound_server.h" @@ -63,104 +65,154 @@ static int MusicVolume = 0; static void (*MusicFinishedCallback)(); #ifdef USE_WIN32 -static volatile bool threadWaiting = false; -static std::string externalFile; -static HANDLE hWaitingThread; +static bool externalPlayerIsPlaying = false; + +static HANDLE g_hStatusThread; +static HANDLE g_hDebugThread; +static HANDLE g_hChildStd_IN_Wr; static PROCESS_INFORMATION pi; -DWORD WINAPI MyThreadFunction(LPVOID lpParam) { - WaitForSingleObject(pi.hProcess, INFINITE); - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - if (threadWaiting) { - MusicFinishedCallback(); - threadWaiting = false; +static DWORD WINAPI StatusThreadFunction(LPVOID lpParam) { + CHAR chStatus; + DWORD dwRead = 1; + while (1) { + if (!ReadFile((HANDLE)lpParam, &chStatus, 1, &dwRead, NULL) || dwRead == 0) { + CloseHandle((HANDLE)lpParam); + break; + } + // any write means we finished + if (externalPlayerIsPlaying) { + externalPlayerIsPlaying = false; + MusicFinishedCallback(); + } } return 0; } -static void killPlayingProcess() { - if (threadWaiting) { - threadWaiting = false; - TerminateProcess(pi.hProcess, 0); - WaitForSingleObject(hWaitingThread, INFINITE); - threadWaiting = false; - } else { +static DWORD WINAPI DebugThreadFunction(LPVOID lpParam) { + DWORD dwRead = 1; + while (1) { + char *chStatus[1024] = {'\0'}; + if (!ReadFile((HANDLE)lpParam, &chStatus, 1024, &dwRead, NULL) || dwRead == 0) { + CloseHandle((HANDLE)lpParam); + break; + } + DebugPrint("%s" _C_ chStatus); + } + return 0; +} + +static void KillPlayingProcess() { + externalPlayerIsPlaying = false; + if (g_hChildStd_IN_Wr) { TerminateProcess(pi.hProcess, 0); + CloseHandle(g_hChildStd_IN_Wr); + g_hChildStd_IN_Wr = NULL; + WaitForSingleObject(StatusThreadFunction, 0); + WaitForSingleObject(DebugThreadFunction, 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; + // try to communicate with the running midiplayer if we can + if (g_hChildStd_IN_Wr != NULL) { + // already playing, just send the new song + // XXX: timfel: disabled, since the midiplayer behaves weirdly when it receives the next file, just kill and restart + KillPlayingProcess(); + /* + // negative value signals a new filename + int fileSize = full_filename.size() & 0xffff; + char loSize = fileSize & 0xff; + char hiSize = (fileSize >> 8) & 0xff; + char buf[2] = {loSize, hiSize}; + externalPlayerIsPlaying = true; + if (!WriteFile(g_hChildStd_IN_Wr, buf, 2, NULL, NULL)) { + KillPlayingProcess(); + } else { + // then write the filename + if (!WriteFile(g_hChildStd_IN_Wr, full_filename.c_str(), fileSize, NULL, NULL)) { + KillPlayingProcess(); + } else { + return true; + } + } + */ } + // need to start an external player first - 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, 127), full_filename.c_str()); - DebugPrint("Using external command to play midi on windows: %s\n" _C_ cmdline); - killPlayingProcess(); + // setup pipes to player + HANDLE hChildStd_IN_Rd = NULL; + HANDLE hChildStd_OUT_Rd = NULL; + HANDLE hChildStd_OUT_Wr = NULL; + HANDLE hChildStd_ERR_Rd = NULL; + HANDLE hChildStd_ERR_Wr = NULL; + SECURITY_ATTRIBUTES saAttr; + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + CreatePipe(&hChildStd_OUT_Rd, &hChildStd_OUT_Wr, &saAttr, 0); + CreatePipe(&hChildStd_ERR_Rd, &hChildStd_ERR_Wr, &saAttr, 0); + CreatePipe(&hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr, 0); + SetHandleInformation(g_hChildStd_IN_Wr, HANDLE_FLAG_INHERIT, 0); + + // start the process + std::vector<std::string> args = QuoteArguments({ "stratagus-midiplayer.exe", std::to_string(std::min(MusicVolume, 127)), full_filename }); + std::string cmd = std::accumulate(std::next(args.begin()), args.end(), args[0], [](std::string a, std::string b) { return a + " " + b; }); + DebugPrint("Using external command to play midi on windows: %s\n" _C_ cmd.c_str()); STARTUPINFO si; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); + si.hStdError = hChildStd_ERR_Wr; + si.hStdOutput = hChildStd_OUT_Wr; + si.hStdInput = hChildStd_IN_Rd; + si.dwFlags |= STARTF_USESTDHANDLES; 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; + char* cmdline = strdup(cmd.c_str()); + if (CreateProcess(NULL, cmdline, NULL, NULL, TRUE, /* Handles are inherited */ CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) { + CloseHandle(hChildStd_OUT_Wr); + CloseHandle(hChildStd_ERR_Wr); + CloseHandle(hChildStd_IN_Rd); + externalPlayerIsPlaying = true; + g_hStatusThread = CreateThread(NULL, 0, StatusThreadFunction, hChildStd_OUT_Rd, 0, NULL); + g_hDebugThread = CreateThread(NULL, 0, DebugThreadFunction, hChildStd_ERR_Rd, 0, NULL); } else { result = false; DebugPrint("CreateProcess failed (%d).\n" _C_ GetLastError()); } - delete[] cmdline; + free(cmdline); return result; } - killPlayingProcess(); + KillPlayingProcess(); return false; } static bool External_IsPlaying() { - return threadWaiting; + return externalPlayerIsPlaying; } static bool External_Stop() { if (External_IsPlaying()) { - killPlayingProcess(); + KillPlayingProcess(); return true; } return false; } static bool External_Volume(int volume, int oldVolume) { - if (External_IsPlaying() && externalFile.size() > 0) { - if (oldVolume != volume) { + if (External_IsPlaying()) { + char buf[2] = {0, volume & 0xFF}; + if (!WriteFile(g_hChildStd_IN_Wr, buf, 2, NULL, NULL)) { External_Stop(); - External_Play(externalFile); + return false; } return true; } diff --git a/src/sound/win32/midiplayer.c b/src/sound/win32/midiplayer.c index bc6f35fce..2d8cd49e7 100644 --- a/src/sound/win32/midiplayer.c +++ b/src/sound/win32/midiplayer.c @@ -16,6 +16,16 @@ #include <mmsystem.h> #include <stdio.h> #include <stdlib.h> +#include <string.h> +#include <fcntl.h> +#include <io.h> + +HMIXER sMixerHandle; +HMIDISTRM sOut; +HANDLE hWaitingThread; +int sVolume = 127; +char *sFilename = NULL; +unsigned int sDeviceId = 0; #define MAX_BUFFER_SIZE (512 * 12) HANDLE event; @@ -142,7 +152,7 @@ static int is_track_end(const struct evt* e) static void CALLBACK example9_callback(HMIDIOUT out, UINT msg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2) { - switch (msg) + switch (msg) { case MOM_DONE: SetEvent(event); @@ -283,7 +293,7 @@ static unsigned int get_buffer(struct trk* tracks, unsigned int ntracks, unsigne return 1; } -unsigned int example9(char* filename, int volume) +unsigned int example9(char *filename) { unsigned char* midibuf = NULL; unsigned int midilen = 0; @@ -299,15 +309,14 @@ unsigned int example9(char* filename, int volume) 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); + fprintf(stderr, "could not open %s\n", filename); + fflush(stderr); return 0; } @@ -338,52 +347,57 @@ unsigned int example9(char* filename, int volume) if ((event = CreateEvent(0, FALSE, FALSE, 0)) == NULL) goto error3; - if (midiStreamOpen(&out, &device, 1, (DWORD)example9_callback, 0, CALLBACK_FUNCTION) != MMSYSERR_NOERROR) + if (midiStreamOpen(&sOut, &sDeviceId, 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) + if(midiStreamProperty(sOut, (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) + if(midiOutPrepareHeader((HMIDIOUT)sOut, &mhdr, sizeof(MIDIHDR)) != MMSYSERR_NOERROR) goto error5; - if(midiStreamRestart(out) != MMSYSERR_NOERROR) + if(midiStreamRestart(sOut) != MMSYSERR_NOERROR) goto error6; - if (midiOutSetVolume((HMIDIOUT)out, (DWORD)((volume & 0xFF) << 8) | (volume & 0xFF)) != MMSYSERR_NOERROR) { - printf("Cannot set volume, will have to use windows application volume control"); - } - - printf("buffering...\n"); + fprintf(stderr, "buffering...\n"); + fflush(stderr); get_buffer(tracks, ntracks, streambuf, &streamlen); while(streamlen > 0) { mhdr.dwBytesRecorded = streamlen; - if(midiStreamOut(out, &mhdr, sizeof(MIDIHDR)) != MMSYSERR_NOERROR) + if(midiStreamOut(sOut, &mhdr, sizeof(MIDIHDR)) != MMSYSERR_NOERROR) goto error7; WaitForSingleObject(event, INFINITE); - printf("buffering...\n"); + if (sFilename != filename) { + fprintf(stderr, "switch to new file %s.\n", sFilename); + fflush(stderr); + break; + } + + fprintf(stderr, "buffering...\n"); + fflush(stderr); get_buffer(tracks, ntracks, streambuf, &streamlen); } - printf("done.\n"); + fprintf(stderr, "done.\n"); + fflush(stderr); error7: - midiOutReset((HMIDIOUT)out); + midiOutReset((HMIDIOUT)sOut); error6: - midiOutUnprepareHeader((HMIDIOUT)out, &mhdr, sizeof(MIDIHDR)); + midiOutUnprepareHeader((HMIDIOUT)sOut, &mhdr, sizeof(MIDIHDR)); error5: - midiStreamClose(out); + midiStreamClose(sOut); error4: CloseHandle(event); @@ -400,13 +414,244 @@ error1: return(0); } +DWORD WINAPI MyThreadFunction(LPVOID lpParam) { + while (1) { + MMRESULT r = midiOutSetVolume((HMIDIOUT)sOut, (DWORD)((sVolume & 0xFF) << 8) | (sVolume & 0xFF)); + if (r != MMSYSERR_NOERROR) { + fprintf(stderr, "Cannot set volume via midi..."); + switch (r) { + case MMSYSERR_INVALHANDLE: + fprintf(stderr, "Reason: handle not valid\n"); + break; + case MMSYSERR_NOMEM: + fprintf(stderr, "Reason: memory error\n"); + break; + case MMSYSERR_NOTSUPPORTED: + fprintf(stderr, "Reason: not supported\n"); + break; + default: + fprintf(stderr, "Reason: unknown\n"); + } + fflush(stderr); + MMRESULT err = mixerOpen(&sMixerHandle, sDeviceId, 0, 0, 0); + if (err) { + fprintf(stderr, "ERROR: Can't open Mixer Device! -- %08X\n", err); + switch (err) { + case MMSYSERR_ALLOCATED: + fprintf(stderr, "MMSYSERR_ALLOCATED\n"); + break; + case MMSYSERR_BADDEVICEID: + fprintf(stderr, "MMSYSERR_BADDEVICEID\n"); + break; + case MMSYSERR_INVALFLAG: + fprintf(stderr, "MMSYSERR_INVALFLAG\n"); + break; + case MMSYSERR_INVALHANDLE: + fprintf(stderr, "MMSYSERR_INVALHANDLE\n"); + break; + case MMSYSERR_INVALPARAM: + fprintf(stderr, "MMSYSERR_INVALPARAM\n"); + break; + case MMSYSERR_NODRIVER: + fprintf(stderr, "MMSYSERR_NODRIVER\n"); + break; + case MMSYSERR_NOMEM: + fprintf(stderr, "MMSYSERR_NOMEM\n"); + break; + default: + fprintf(stderr, "Reason: unknown\n"); + } + fflush(stderr); + } else { + MIXERCAPS mixcaps; + MIXERLINE mixerline; + MMRESULT err; + unsigned long i; + /* Get info about the first Mixer Device */ + if (!(err = mixerGetDevCaps((UINT)sMixerHandle, &mixcaps, sizeof(MIXERCAPS)))) { + /* Print out the name of each destination line */ + for (i = 0; i < mixcaps.cDestinations; i++) { + mixerline.cbStruct = sizeof(MIXERLINE); + mixerline.dwDestination = i; + + if (!(err = mixerGetLineInfo((HMIXEROBJ)sMixerHandle, &mixerline, MIXER_GETLINEINFOF_DESTINATION))) + { + fprintf(stderr, "Destination #%lu = %s\n", i, mixerline.szName); + fflush(stderr); + } + } + } + mixerline.cbStruct = sizeof(MIXERLINE); + mixerline.dwDestination = 0; + mixerline.dwSource = 0; + + if ((err = mixerGetLineInfo((HMIXEROBJ)sMixerHandle, &mixerline, MIXER_GETLINEINFOF_DESTINATION))) { + /* An error */ + fprintf(stderr, "Error #%d calling mixerGetLineInfo()\n", err); + switch (err) { + case MIXERR_INVALLINE: + fprintf(stderr, "MIXERR_INVALLINE\n"); + break; + case MMSYSERR_BADDEVICEID: + fprintf(stderr, "MMSYSERR_BADDEVICEID\n"); + break; + case MMSYSERR_INVALFLAG: + fprintf(stderr, "MMSYSERR_INVALFLAG\n"); + break; + case MMSYSERR_INVALHANDLE: + fprintf(stderr, "MMSYSERR_INVALHANDLE\n"); + break; + case MMSYSERR_INVALPARAM: + fprintf(stderr, "MMSYSERR_INVALPARAM\n"); + break; + case MMSYSERR_NODRIVER: + fprintf(stderr, "MMSYSERR_NODRIVER\n"); + break; + default: + fprintf(stderr, "Reason: unknown\n"); + } + fflush(stderr); + } else { + fprintf(stderr, "Mixerline: %s\n", mixerline.szName); + MIXERCONTROL mixerControlArray; + MIXERLINECONTROLS mixerLineControls; + mixerLineControls.cbStruct = sizeof(MIXERLINECONTROLS); + /* Tell mixerGetLineControls() for which line we're retrieving info. + We do this by putting the desired line's ID number in dwLineID */ + mixerLineControls.dwLineID = mixerline.dwLineID; + /* We want to fetch info on only 1 control */ + mixerLineControls.cControls = 1; + /* Tell mixerGetLineControls() for which type of control we're + retrieving info. We do this by putting the desired control type + in dwControlType */ + mixerLineControls.dwControlType = MIXERCONTROL_CONTROLTYPE_VOLUME; + /* Give mixerGetLineControls() the address of the MIXERCONTROL + struct to hold info */ + mixerLineControls.pamxctrl = &mixerControlArray; + /* Tell mixerGetLineControls() how big the MIXERCONTROL is. This + saves having to initialize the cbStruct of the MIXERCONTROL itself */ + mixerLineControls.cbmxctrl = sizeof(MIXERCONTROL); + /* Retrieve info on only any volume slider control for this line */ + if ((err = mixerGetLineControls((HMIXEROBJ)sMixerHandle, &mixerLineControls, MIXER_GETLINECONTROLSF_ONEBYTYPE))) { + /* An error */ + fprintf(stderr, "Error #%d calling mixerGetLineControls()\n", err); + fflush(stderr); + } else { + fprintf(stderr, "Mixercontrol: %s\n", mixerControlArray.szName); + fflush(stderr); + // try via mixer controls + MIXERCONTROLDETAILS_UNSIGNED value; + MIXERCONTROLDETAILS mixerControlDetails; + MMRESULT err; + mixerControlDetails.cbStruct = sizeof(MIXERCONTROLDETAILS); + /* Tell mixerSetControlDetails() which control whose value we + want to set. We do this by putting the desired control's + ID number in dwControlID. The "Speaker Out" line's + volume slider has an ID of 0x00000000 */ + mixerControlDetails.dwControlID = mixerControlArray.dwControlID; + /* This is always 1 for a MIXERCONTROL_CONTROLF_UNIFORM control */ + mixerControlDetails.cChannels = 1; + /* This is always 0 except for a MIXERCONTROL_CONTROLF_MULTIPLE control */ + mixerControlDetails.cMultipleItems = 0; + /* Give mixerSetControlDetails() the address of the + MIXERCONTROLDETAILS_UNSIGNED struct into which we place the value */ + mixerControlDetails.paDetails = &value; + /* Tell mixerSetControlDetails() how big the MIXERCONTROLDETAILS_UNSIGNED is */ + mixerControlDetails.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED); + /* Store the value */ + value.dwValue = (sVolume & 0xFF) << 8; + /* Set the value of the volume slider control for this line */ + if ((err = mixerSetControlDetails((HMIXEROBJ)sMixerHandle, &mixerControlDetails, MIXER_SETCONTROLDETAILSF_VALUE))) { + fprintf(stderr, "Error #%d calling mixerSetControlDetails() ", err); + switch (err) { + case MIXERR_INVALCONTROL: + fprintf(stderr, "Invalid control\n"); + break; + case MMSYSERR_BADDEVICEID: + fprintf(stderr, "bad device id\n"); + break; + case MMSYSERR_INVALFLAG: + fprintf(stderr, "invalid flag\n"); + break; + case MMSYSERR_INVALHANDLE: + fprintf(stderr, "Invalid handle\n"); + break; + case MMSYSERR_INVALPARAM: + fprintf(stderr, "Invalid param\n"); + break; + case MMSYSERR_NODRIVER: + fprintf(stderr, "No driver\n"); + break; + default: + fprintf(stderr, "\n"); + } + fprintf(stderr, "Will have to use windows application volume control.\n"); + fflush(stderr); + } + } + } + } + } + + char data1, data2; + fprintf(stderr, "read from stdin..."); + fflush(stderr); + if (fread(&data1, sizeof(char), 1, stdin) != 1) { + exit(0); + } else { + if (fread(&data2, sizeof(char), 1, stdin) != 1) { + exit(0); + } + } + fprintf(stderr, "got %x %x\n", data1, data2); + fflush(stderr); + if (data1 > 0) { + // new filename + short length = data1 | (data2 << 8); + fprintf(stderr, "New filename sending: %d bytes\n", length); + fflush(stderr); + static char buf[0xffff]; + int lastRead, totalRead = 0; + while ((lastRead = fread(buf + totalRead, sizeof(char), length - totalRead, stdin)) > 0 && totalRead < length) { + totalRead += lastRead; + } + if (totalRead < length) { + fprintf(stderr, "not enough data, got %d, expected %d bytes\n", totalRead, length); + fflush(stderr); + exit(0); + } + buf[length] = '\0'; + sFilename = buf; + fprintf(stderr, "New filename received: %s\n", sFilename); + fflush(stderr); + SetEvent(event); + } else { + fprintf(stderr, "New volume received: %d\n", data2); + fflush(stderr); + sVolume = data2; + } + } + return 0; +} + int main(int argc, char* argv[]) { if(argc != 3) { - return printf("Usage: %s <volume 0-255> <filename.mid>\n", argv[0]); + return fprintf(stderr, "Usage: %s <volume 0-255> <filename.mid>\n", argv[0]); + } + + sVolume = atoi(argv[1]); + _setmode(_fileno(stdin), _O_BINARY); + hWaitingThread = CreateThread(NULL, 0, MyThreadFunction, NULL, 0, NULL); + + sFilename = argv[2]; + + while (1) { + example9(sFilename); + fprintf(stdout, "1"); + fflush(stdout); + WaitForSingleObject(event, INFINITE); } - - example9(argv[2], atoi(argv[1])); return 0; }