gzdoom/src/sound/music_midi_timidity.cpp
Chris Robinson 1a40c95f84 Rename some FMOD references to be generic sound system
Since they relates to other sound backends as well, there's no need to single
out FMOD in these places.
2014-06-26 16:58:18 -07:00

725 lines
17 KiB
C++

#include "i_musicinterns.h"
#include "c_cvars.h"
#include "cmdlib.h"
#include "templates.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
#ifdef _WIN32
BOOL SafeTerminateProcess(HANDLE hProcess, UINT uExitCode);
static char TimidityTitle[] = "TiMidity (ZDoom Launched)";
const char TimidityPPMIDIDevice::EventName[] = "TiMidity Killer";
CVAR (String, timidity_exe, "timidity.exe", CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
#else
CVAR (String, timidity_exe, "timidity", CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
#endif
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, 22050, 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()
: 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 (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<BYTE> 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(void (*callback)(unsigned int, void *, DWORD, DWORD), void *userdata)
{
int pipeSize;
#ifdef _WIN32
static SECURITY_ATTRIBUTES inheritable = { sizeof(inheritable), NULL, TRUE };
if (!Validated && !ValidateTimidity ())
{
return 101;
}
Validated = true;
#endif // WIN32
CommandLine.Format("%s %s -EFchorus=%s -EFreverb=%s -s%d ",
*timidity_exe, *timidity_extargs,
*timidity_chorus, *timidity_reverb, *timidity_frequency);
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 BYTE *exeBase;
const BYTE *exeEnd;
const BYTE *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 BYTE *)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 BYTE *)tSpot + 1;
}
}
catch (...)
{
Printf(PRINT_BOLD, "Error reading %s\n", foundPath);
}
if (!good)
{
Printf(PRINT_BOLD, "ZDoom 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 ("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, (BYTE *)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 ((BYTE *)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], (BYTE *)buff, len);
if (got < len)
{
memset((BYTE *)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 process to terminate to guarantee that it has exited...
WaitForSingleObject(hProcess, INFINITE);
CloseHandle(hRT);
bSuccess = TRUE;
}
if ( !bSuccess )
SetLastError(dwErr);
return bSuccess;
}
#endif