gzdoom-gles/src/sound/mididevices/music_timiditypp_mididevice.cpp
Christoph Oelckers fb12c58533 - restart the music if the OPL core or the config file for the softsynths is changed, except for the GUS synth, which loads all its data during engine startup and cannot swap it out.
- added arrays to the config to hold entries for the softsynths' config files. This is not active yet, but will later be used to give the user a list of config options instead of having to type it by hand.
2017-04-18 16:43:07 +02:00

821 lines
20 KiB
C++

/*
** music_timiditypp_mididevice.cpp
** Provides access to timidity.exe
**
**---------------------------------------------------------------------------
** Copyright 2001-2017 Randy Heit
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in the
** documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
** derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
*/
#include "i_midi_win32.h"
#include "i_musicinterns.h"
#include "c_cvars.h"
#include "cmdlib.h"
#include "templates.h"
#include "version.h"
#ifndef _WIN32
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <wordexp.h>
#include <signal.h>
int ChildQuit;
void ChildSigHandler (int signum)
{
ChildQuit = waitpid (-1, NULL, WNOHANG);
}
#endif
class TimidityPPMIDIDevice : public PseudoMIDIDevice
{
public:
TimidityPPMIDIDevice(const char *args);
~TimidityPPMIDIDevice();
int Open(MidiCallback, void *userdata);
bool Preprocess(MIDIStreamer *song, bool looping);
bool IsOpen() const;
int Resume();
void Stop();
bool IsOpen();
void TimidityVolumeChanged();
int GetDeviceType() const override { return MDEV_TIMIDITY; }
protected:
bool LaunchTimidity();
FTempFileName DiskName;
#ifdef _WIN32
HANDLE ReadWavePipe;
HANDLE WriteWavePipe;
HANDLE ChildProcess;
bool Validated;
bool ValidateTimidity();
#else // _WIN32
int WavePipe[2];
pid_t ChildProcess;
#endif
FString CommandLine;
size_t LoopPos;
static bool FillStream(SoundStream *stream, void *buff, int len, void *userdata);
#ifdef _WIN32
static const char EventName[];
#endif
};
#ifdef _WIN32
BOOL SafeTerminateProcess(HANDLE hProcess, UINT uExitCode);
static char TimidityTitle[] = "TiMidity (" GAMENAME " Launched)";
const char TimidityPPMIDIDevice::EventName[] = "TiMidity Killer";
CUSTOM_CVAR (String, timidity_exe, "timidity.exe", CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
#else
CUSTOM_CVAR(String, timidity_exe, "timidity", CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
#endif
{
if (currSong != nullptr && currSong->GetDeviceType() == MDEV_TIMIDITY)
{
MIDIDeviceChanged(-1, true);
}
}
CVAR (String, timidity_extargs, "", CVAR_ARCHIVE|CVAR_GLOBALCONFIG) // extra args to pass to Timidity
CVAR (String, timidity_chorus, "0", CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
CVAR (String, timidity_reverb, "0", CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
CVAR (Bool, timidity_stereo, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
CVAR (Bool, timidity_8bit, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
CVAR (Bool, timidity_byteswap, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
// added because Timidity's output is rather loud.
CUSTOM_CVAR (Float, timidity_mastervolume, 1.0f, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
{
if (self < 0.f)
self = 0.f;
else if (self > 4.f)
self = 4.f;
if (currSong != NULL)
currSong->TimidityVolumeChanged();
}
CUSTOM_CVAR (Int, timidity_pipe, 90, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
{ // pipe size in ms
if (timidity_pipe < 0)
{ // a negative size makes no sense
timidity_pipe = 0;
}
}
CUSTOM_CVAR (Int, timidity_frequency, 44100, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
{ // Clamp frequency to Timidity's limits
if (self < 4000)
self = 4000;
else if (self > 65000)
self = 65000;
}
//==========================================================================
//
// TimidityPPMIDIDevice Constructor
//
//==========================================================================
TimidityPPMIDIDevice::TimidityPPMIDIDevice(const char *args)
: DiskName("zmid"),
#ifdef _WIN32
ReadWavePipe(INVALID_HANDLE_VALUE), WriteWavePipe(INVALID_HANDLE_VALUE),
ChildProcess(INVALID_HANDLE_VALUE),
Validated(false)
#else
ChildProcess(-1)
#endif
{
#ifndef _WIN32
WavePipe[0] = WavePipe[1] = -1;
#endif
if (args == NULL || *args == 0) args = timidity_exe;
CommandLine.Format("%s %s -EFchorus=%s -EFreverb=%s -s%d ",
args, *timidity_extargs,
*timidity_chorus, *timidity_reverb, *timidity_frequency);
if (DiskName == NULL)
{
Printf(PRINT_BOLD, "Could not create temp music file\n");
return;
}
}
//==========================================================================
//
// TimidityPPMIDIDevice Destructor
//
//==========================================================================
TimidityPPMIDIDevice::~TimidityPPMIDIDevice ()
{
#if _WIN32
if (WriteWavePipe != INVALID_HANDLE_VALUE)
{
CloseHandle (WriteWavePipe);
WriteWavePipe = INVALID_HANDLE_VALUE;
}
if (ReadWavePipe != INVALID_HANDLE_VALUE)
{
CloseHandle (ReadWavePipe);
ReadWavePipe = INVALID_HANDLE_VALUE;
}
#else
if (WavePipe[1] != -1)
{
close (WavePipe[1]);
WavePipe[1] = -1;
}
if (WavePipe[0] != -1)
{
close (WavePipe[0]);
WavePipe[0] = -1;
}
#endif
}
//==========================================================================
//
// TimidityPPMIDIDevice :: Preprocess
//
//==========================================================================
bool TimidityPPMIDIDevice::Preprocess(MIDIStreamer *song, bool looping)
{
TArray<uint8_t> midi;
bool success;
FILE *f;
if (CommandLine.IsEmpty())
{
return false;
}
// Tell TiMidity++ whether it should loop or not
CommandLine.LockBuffer()[LoopPos] = looping ? 'l' : ' ';
CommandLine.UnlockBuffer();
// Write MIDI song to temporary file
song->CreateSMF(midi, looping ? 0 : 1);
f = fopen(DiskName, "wb");
if (f == NULL)
{
Printf(PRINT_BOLD, "Could not open temp music file\n");
return false;
}
success = (fwrite(&midi[0], 1, midi.Size(), f) == (size_t)midi.Size());
fclose (f);
if (!success)
{
Printf(PRINT_BOLD, "Could not write temp music file\n");
}
return false;
}
//==========================================================================
//
// TimidityPPMIDIDevice :: Open
//
//==========================================================================
int TimidityPPMIDIDevice::Open(MidiCallback callback, void *userdata)
{
int pipeSize;
#ifdef _WIN32
static SECURITY_ATTRIBUTES inheritable = { sizeof(inheritable), NULL, true };
if (!Validated && !ValidateTimidity ())
{
return 101;
}
Validated = true;
#endif // WIN32
pipeSize = (timidity_pipe * timidity_frequency / 1000)
<< (timidity_stereo + !timidity_8bit);
{
#ifdef _WIN32
// Round pipe size up to nearest power of 2 to try and avoid partial
// buffer reads in FillStream() under NT. This does not seem to be an
// issue under 9x.
int bitmask = pipeSize & -pipeSize;
while (bitmask < pipeSize)
bitmask <<= 1;
pipeSize = bitmask;
if (!CreatePipe(&ReadWavePipe, &WriteWavePipe, &inheritable, pipeSize))
#else // WIN32
if (pipe (WavePipe) == -1)
#endif
{
Printf(PRINT_BOLD, "Could not create a data pipe for TiMidity++.\n");
pipeSize = 0;
}
else
{
Stream = GSnd->CreateStream(FillStream, pipeSize,
(timidity_stereo ? 0 : SoundStream::Mono) |
(timidity_8bit ? SoundStream::Bits8 : 0),
timidity_frequency, this);
if (Stream == NULL)
{
Printf(PRINT_BOLD, "Could not create music stream.\n");
pipeSize = 0;
#ifdef _WIN32
CloseHandle(WriteWavePipe);
CloseHandle(ReadWavePipe);
ReadWavePipe = WriteWavePipe = INVALID_HANDLE_VALUE;
#else
close(WavePipe[1]);
close(WavePipe[0]);
WavePipe[0] = WavePipe[1] = -1;
#endif
}
}
if (pipeSize == 0)
{
Printf(PRINT_BOLD, "If your soundcard cannot play more than one\n"
"wave at a time, you will hear no music.\n");
}
else
{
CommandLine += "-o - -Ors";
}
}
if (pipeSize == 0)
{
CommandLine += "-Od";
}
CommandLine += timidity_stereo ? 'S' : 'M';
CommandLine += timidity_8bit ? '8' : '1';
if (timidity_byteswap)
{
CommandLine += 'x';
}
LoopPos = CommandLine.Len() + 4;
CommandLine += " -idl ";
CommandLine += DiskName.GetName();
return 0;
}
//==========================================================================
//
// TimidityPPMIDIDevice :: ValidateTimidity
//
// Check that this TiMidity++ knows about the TiMidity Killer event.
// If not, then we can't use it, because Win32 provides no other way
// to conveniently signal it to quit. The check is done by simply
// searching for the event's name somewhere in the executable.
//
//==========================================================================
#ifdef _WIN32
bool TimidityPPMIDIDevice::ValidateTimidity()
{
char foundPath[MAX_PATH];
char *filePart;
DWORD pathLen;
DWORD fileLen;
HANDLE diskFile;
HANDLE mapping;
const uint8_t *exeBase;
const uint8_t *exeEnd;
const uint8_t *exe;
bool good;
pathLen = SearchPath (NULL, timidity_exe, NULL, MAX_PATH, foundPath, &filePart);
if (pathLen == 0)
{
Printf(PRINT_BOLD, "Please set the timidity_exe cvar to the location of TiMidity++\n");
return false;
}
if (pathLen > MAX_PATH)
{
Printf(PRINT_BOLD, "The path to TiMidity++ is too long\n");
return false;
}
diskFile = CreateFile (foundPath, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL);
if (diskFile == INVALID_HANDLE_VALUE)
{
Printf(PRINT_BOLD, "Could not access %s\n", foundPath);
return false;
}
fileLen = GetFileSize (diskFile, NULL);
mapping = CreateFileMapping (diskFile, NULL, PAGE_READONLY, 0, 0, NULL);
if (mapping == NULL)
{
Printf(PRINT_BOLD, "Could not create mapping for %s\n", foundPath);
CloseHandle (diskFile);
return false;
}
exeBase = (const uint8_t *)MapViewOfFile (mapping, FILE_MAP_READ, 0, 0, 0);
if (exeBase == NULL)
{
Printf(PRINT_BOLD, "Could not map %s\n", foundPath);
CloseHandle (mapping);
CloseHandle (diskFile);
return false;
}
good = false;
try
{
for (exe = exeBase, exeEnd = exeBase+fileLen; exe < exeEnd; )
{
const char *tSpot = (const char *)memchr(exe, 'T', exeEnd - exe);
if (tSpot == NULL)
{
break;
}
if (memcmp(tSpot+1, EventName+1, sizeof(EventName)-1) == 0)
{
good = true;
break;
}
exe = (const uint8_t *)tSpot + 1;
}
}
catch (...)
{
Printf(PRINT_BOLD, "Error reading %s\n", foundPath);
}
if (!good)
{
Printf(PRINT_BOLD, GAMENAME " requires a special version of TiMidity++\n");
}
UnmapViewOfFile((LPVOID)exeBase);
CloseHandle(mapping);
CloseHandle(diskFile);
return good;
}
#endif // _WIN32
//==========================================================================
//
// TimidityPPMIDIDevice :: LaunchTimidity
//
//==========================================================================
bool TimidityPPMIDIDevice::LaunchTimidity ()
{
if (CommandLine.IsEmpty())
{
return false;
}
DPrintf (DMSG_NOTIFY, "cmd: \x1cG%s\n", CommandLine.GetChars());
#ifdef _WIN32
STARTUPINFO startup = { sizeof(startup), };
PROCESS_INFORMATION procInfo;
startup.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
startup.hStdInput = INVALID_HANDLE_VALUE;
startup.hStdOutput = WriteWavePipe != INVALID_HANDLE_VALUE ?
WriteWavePipe : GetStdHandle (STD_OUTPUT_HANDLE);
startup.hStdError = GetStdHandle (STD_ERROR_HANDLE);
startup.lpTitle = TimidityTitle;
startup.wShowWindow = SW_SHOWMINNOACTIVE;
if (CreateProcess(NULL, CommandLine.LockBuffer(), NULL, NULL, true,
DETACHED_PROCESS, NULL, NULL, &startup, &procInfo))
{
ChildProcess = procInfo.hProcess;
//SetThreadPriority (procInfo.hThread, THREAD_PRIORITY_HIGHEST);
CloseHandle(procInfo.hThread); // Don't care about the created thread
CommandLine.UnlockBuffer();
return true;
}
CommandLine.UnlockBuffer();
char hres[9];
LPTSTR msgBuf;
HRESULT err = GetLastError();
if (!FormatMessage (FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, err, 0, (LPTSTR)&msgBuf, 0, NULL))
{
mysnprintf(hres, countof(hres), "%08lx", err);
msgBuf = hres;
}
Printf(PRINT_BOLD, "Could not run timidity with the command line:\n%s\n"
"Reason: %s\n", CommandLine.GetChars(), msgBuf);
if (msgBuf != hres)
{
LocalFree (msgBuf);
}
return false;
#else
if (WavePipe[0] != -1 && WavePipe[1] == -1 && Stream != NULL)
{
// Timidity was previously launched, so the write end of the pipe
// is closed, and the read end is still open. Close the pipe
// completely and reopen it.
close (WavePipe[0]);
WavePipe[0] = -1;
delete Stream;
Stream = NULL;
Open (NULL, NULL);
}
int forkres;
wordexp_t words = {};
switch (wordexp (CommandLine.GetChars(), &words, 0))
{
case 0: // all good
break;
case WRDE_NOSPACE:
wordfree (&words);
default:
return false;
}
forkres = fork ();
if (forkres == 0)
{
close (WavePipe[0]);
dup2 (WavePipe[1], STDOUT_FILENO);
freopen ("/dev/null", "r", stdin);
// freopen ("/dev/null", "w", stderr);
close (WavePipe[1]);
execvp (words.we_wordv[0], words.we_wordv);
fprintf(stderr,"execvp failed\n");
_exit (0); // if execvp succeeds, we never get here
}
else if (forkres < 0)
{
Printf (PRINT_BOLD, "Could not fork when trying to start timidity\n");
}
else
{
// printf ("child is %d\n", forkres);
ChildProcess = forkres;
close (WavePipe[1]);
WavePipe[1] = -1;
/* usleep(1000000);
if (waitpid(ChildProcess, NULL, WNOHANG) == ChildProcess)
{
fprintf(stderr,"Launching timidity failed\n");
}*/
}
wordfree (&words);
return ChildProcess != -1;
#endif // _WIN32
}
//==========================================================================
//
// TimidityPPMIDIDevice :: FillStream
//
//==========================================================================
bool TimidityPPMIDIDevice::FillStream(SoundStream *stream, void *buff, int len, void *userdata)
{
TimidityPPMIDIDevice *song = (TimidityPPMIDIDevice *)userdata;
#ifdef _WIN32
DWORD avail, got, didget;
if (!PeekNamedPipe(song->ReadWavePipe, NULL, 0, NULL, &avail, NULL) || avail == 0)
{ // If nothing is available from the pipe, play silence.
memset (buff, 0, len);
}
else
{
didget = 0;
for (;;)
{
ReadFile(song->ReadWavePipe, (uint8_t *)buff+didget, len-didget, &got, NULL);
didget += got;
if (didget >= (DWORD)len)
break;
// Give TiMidity++ a chance to output something more to the pipe
Sleep (10);
if (!PeekNamedPipe(song->ReadWavePipe, NULL, 0, NULL, &avail, NULL) || avail == 0)
{
memset ((uint8_t *)buff+didget, 0, len-didget);
break;
}
}
}
#else
ssize_t got;
fd_set rfds;
struct timeval tv;
if (ChildQuit == song->ChildProcess)
{
ChildQuit = 0;
fprintf(stderr, "child gone\n");
song->ChildProcess = -1;
return false;
}
FD_ZERO(&rfds);
FD_SET(song->WavePipe[0], &rfds);
tv.tv_sec = 0;
tv.tv_usec = 50;
// fprintf(stderr,"select\n");
if (select(1, &rfds, NULL, NULL, &tv) <= 0 && 0)
{ // Nothing available, so play silence.
// fprintf(stderr,"nothing\n");
// memset(buff, 0, len);
return true;
}
// fprintf(stderr,"something\n");
got = read(song->WavePipe[0], (uint8_t *)buff, len);
if (got < len)
{
memset((uint8_t *)buff+got, 0, len-got);
}
#endif
return true;
}
//==========================================================================
//
// TimidityPPMIDIDevice :: TimidityVolumeChanged
//
//==========================================================================
void TimidityPPMIDIDevice::TimidityVolumeChanged()
{
if (Stream != NULL)
{
Stream->SetVolume(timidity_mastervolume);
}
}
//==========================================================================
//
// TimidityPPMIDIDevice :: IsOpen
//
//==========================================================================
bool TimidityPPMIDIDevice::IsOpen() const
{
#ifdef _WIN32
if (ChildProcess != INVALID_HANDLE_VALUE)
{
if (WaitForSingleObject(ChildProcess, 0) != WAIT_TIMEOUT)
{ // Timidity++ has quit
CloseHandle(ChildProcess);
const_cast<TimidityPPMIDIDevice *>(this)->ChildProcess = INVALID_HANDLE_VALUE;
#else
if (ChildProcess != -1)
{
if (waitpid (ChildProcess, NULL, WNOHANG) == ChildProcess)
{
const_cast<TimidityPPMIDIDevice *>(this)->ChildProcess = -1;
#endif
return false;
}
return true;
}
return false;
}
//==========================================================================
//
// TimidityPPMIDIDevice :: Resume
//
//==========================================================================
int TimidityPPMIDIDevice::Resume()
{
if (!Started)
{
if (LaunchTimidity())
{
// Assume success if not mixing with the sound system
if (Stream == NULL || Stream->Play(true, timidity_mastervolume))
{
Started = true;
return 0;
}
}
return 1;
}
return 0;
}
//==========================================================================
//
// TimidityPPMIDIDevice :: Stop
//
//==========================================================================
void TimidityPPMIDIDevice::Stop ()
{
if (Started)
{
if (Stream != NULL)
{
Stream->Stop();
}
#ifdef _WIN32
if (ChildProcess != INVALID_HANDLE_VALUE)
{
if (!SafeTerminateProcess(ChildProcess, 666) && GetLastError() != ERROR_PROCESS_ABORTED)
{
TerminateProcess(ChildProcess, 666);
}
CloseHandle(ChildProcess);
ChildProcess = INVALID_HANDLE_VALUE;
}
#else
if (ChildProcess != -1)
{
if (kill(ChildProcess, SIGTERM) != 0)
{
kill(ChildProcess, SIGKILL);
}
waitpid(ChildProcess, NULL, 0);
ChildProcess = -1;
}
#endif
}
Started = false;
}
#ifdef _WIN32
/*
Safely terminate a process by creating a remote thread
in the process that calls ExitProcess
Source is a Dr Dobbs article circa 1999.
*/
typedef HANDLE (WINAPI *CreateRemoteThreadProto)(HANDLE,LPSECURITY_ATTRIBUTES,SIZE_T,LPTHREAD_START_ROUTINE,LPVOID,DWORD,LPDWORD);
BOOL SafeTerminateProcess(HANDLE hProcess, UINT uExitCode)
{
DWORD dwTID, dwCode;
HRESULT dwErr = 0;
HANDLE hRT = NULL;
HINSTANCE hKernel = GetModuleHandle("Kernel32");
BOOL bSuccess = FALSE;
// Detect the special case where the process is already dead...
if ( GetExitCodeProcess(hProcess, &dwCode) && (dwCode == STILL_ACTIVE) )
{
FARPROC pfnExitProc;
CreateRemoteThreadProto pfCreateRemoteThread;
pfnExitProc = GetProcAddress(hKernel, "ExitProcess");
// CreateRemoteThread does not exist on 9x systems.
pfCreateRemoteThread = (CreateRemoteThreadProto)GetProcAddress(hKernel, "CreateRemoteThread");
if (pfCreateRemoteThread == NULL)
{
dwErr = ERROR_INVALID_FUNCTION;
}
else
{
hRT = pfCreateRemoteThread(hProcess,
NULL,
0,
(LPTHREAD_START_ROUTINE)pfnExitProc,
(PVOID)(UINT_PTR)uExitCode, 0, &dwTID);
if ( hRT == NULL )
dwErr = GetLastError();
}
}
else
{
dwErr = ERROR_PROCESS_ABORTED;
}
if ( hRT )
{
// Must wait for process to terminate to guarantee that it has exited...
DWORD res = WaitForSingleObject(hProcess, 1000);
CloseHandle(hRT);
bSuccess = (res == WAIT_OBJECT_0);
dwErr = WAIT_TIMEOUT;
}
if ( !bSuccess )
SetLastError(dwErr);
return bSuccess;
}
#endif
MIDIDevice *CreateTimidityPPMIDIDevice(const char *args)
{
return new TimidityPPMIDIDevice(args);
}