fruitjam-doom/midiproc/main.c
Fabian Greffrath 241d056f60 midiproc: some further improvements
Recent versions of SDL_Mixer allow for rendering MIDI songs using
the fluidsynth backend and recent versions of fluidsynth allow for
using soundfonts in SF3 format (which contain OGG compressed samples).

The midiproc process currently assumes that it is possible to remove
the temporary music file immediately after Mix_LoadMUS() has been
called, which is true if the music is rendered using Windows's limited
intrnal MIDI playback. However, when using an advanced backend like
fluidsynth, this file maybe locked by this library. Furthermore,
midiproc currently assumes that it is possible to override the
temporary music file as soon as Mix_HaltMusic() has been called which
is again not true if fluidsynth is used. In this case, this is only
possible after Mix_FreeMusic() has been called (and returned).
Additionally, we increase the time-out value after which we give up
waiting for the midiproc process to send an acknowledgement to 1s to
give it some time to load the fluidsynth library and a soundfont
(although 1s will still not be enough for soundfonts in SF3 format
which reportedly load up to the order of 10s!).
2017-12-11 14:46:09 +01:00

467 lines
9.7 KiB
C

//
// Copyright(C) 2012 James Haley
// Copyright(C) 2017 Alex Mayfield
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// DESCRIPTION:
//
// Win32/SDL_mixer MIDI Server
//
// Uses pipes to communicate with Doom. This allows this separate process to
// have its own independent volume control even under Windows Vista and up's
// broken, stupid, completely useless mixer model that can't assign separate
// volumes to different devices for the same process.
//
// Seriously, how did they screw up something so fundamental?
//
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include "SDL.h"
#include "SDL_mixer.h"
#include "buffer.h"
#include "proto.h"
#include "config.h"
#include "doomtype.h"
static HANDLE midi_process_in; // Standard In.
static HANDLE midi_process_out; // Standard Out.
// Sound sample rate to use for digital output (Hz)
static int snd_samplerate = 0;
// Currently playing music track.
static Mix_Music *music = NULL;
//=============================================================================
//
// Private functions
//
//
// Write an unsigned integer into a simple CHAR buffer.
//
static boolean WriteInt16(CHAR *out, size_t osize, unsigned int in)
{
if (osize < 2)
{
return false;
}
out[0] = (in >> 8) & 0xff;
out[1] = in & 0xff;
return true;
}
//
// Cleanly close our in-use pipes.
//
static void FreePipes(void)
{
if (midi_process_in != NULL)
{
CloseHandle(midi_process_in);
midi_process_in = NULL;
}
if (midi_process_out != NULL)
{
CloseHandle(midi_process_out);
midi_process_out = NULL;
}
}
//
// Unregisters the currently playing song. This is never called from the
// protocol, we simply do this before playing a new song.
//
static void UnregisterSong()
{
if (music == NULL)
{
return;
}
Mix_FreeMusic(music);
music = NULL;
}
//
// Cleanly shut down SDL.
//
static void ShutdownSDL(void)
{
UnregisterSong();
Mix_CloseAudio();
SDL_Quit();
}
//=============================================================================
//
// SDL_mixer Interface
//
static boolean RegisterSong(const char *filename)
{
UnregisterSong();
music = Mix_LoadMUS(filename);
// Remove the temporary MIDI file
remove(filename);
if (music == NULL)
{
return false;
}
return true;
}
static void SetVolume(int vol)
{
Mix_VolumeMusic(vol);
}
static void PlaySong(int loops)
{
Mix_PlayMusic(music, loops);
// [AM] BUG: In my testing, setting the volume of a MIDI track while there
// is no song playing appears to be a no-op. This can happen when
// you're mixing midiproc with vanilla SDL_Mixer, such as when you
// are alternating between a digital music pack (in the parent
// process) and MIDI (in this process).
//
// To work around this bug, we set the volume to itself after the MIDI
// has started playing.
Mix_VolumeMusic(Mix_VolumeMusic(-1));
}
static void StopSong()
{
Mix_HaltMusic();
}
//=============================================================================
//
// Pipe Server Interface
//
static boolean MidiPipe_RegisterSong(buffer_reader_t *reader)
{
CHAR buffer[2];
DWORD bytes_written;
char *filename = Reader_ReadString(reader);
if (filename == NULL)
{
return false;
}
if (!RegisterSong(filename))
{
return false;
}
if (!WriteInt16(buffer, sizeof(buffer),
MIDIPIPE_PACKET_TYPE_REGISTER_SONG_ACK))
{
return false;
}
WriteFile(midi_process_out, buffer, sizeof(buffer),
&bytes_written, NULL);
return true;
}
boolean MidiPipe_SetVolume(buffer_reader_t *reader)
{
int vol;
boolean ok = Reader_ReadInt32(reader, (uint32_t*)&vol);
if (!ok)
{
return false;
}
SetVolume(vol);
return true;
}
boolean MidiPipe_PlaySong(buffer_reader_t *reader)
{
int loops;
boolean ok = Reader_ReadInt32(reader, (uint32_t*)&loops);
if (!ok)
{
return false;
}
PlaySong(loops);
return true;
}
boolean MidiPipe_StopSong()
{
StopSong();
UnregisterSong();
return true;
}
boolean MidiPipe_Shutdown()
{
exit(EXIT_SUCCESS);
}
//=============================================================================
//
// Server Implementation
//
//
// Parses a command and directs to the proper read function.
//
boolean ParseCommand(buffer_reader_t *reader, uint16_t command)
{
switch (command)
{
case MIDIPIPE_PACKET_TYPE_REGISTER_SONG:
return MidiPipe_RegisterSong(reader);
case MIDIPIPE_PACKET_TYPE_SET_VOLUME:
return MidiPipe_SetVolume(reader);
case MIDIPIPE_PACKET_TYPE_PLAY_SONG:
return MidiPipe_PlaySong(reader);
case MIDIPIPE_PACKET_TYPE_STOP_SONG:
return MidiPipe_StopSong();
case MIDIPIPE_PACKET_TYPE_SHUTDOWN:
return MidiPipe_Shutdown();
default:
return false;
}
}
//
// Server packet parser
//
boolean ParseMessage(buffer_t *buf)
{
int bytes_read;
uint16_t command;
buffer_reader_t *reader = NewReader(buf);
// Attempt to read a command out of the buffer.
if (!Reader_ReadInt16(reader, &command))
{
goto fail;
}
// Attempt to parse a complete message.
if (!ParseCommand(reader, command))
{
goto fail;
}
// We parsed a complete message! We can now safely shift
// the prior message off the front of the buffer.
bytes_read = Reader_BytesRead(reader);
DeleteReader(reader);
Buffer_Shift(buf, bytes_read);
return true;
fail:
// We did not read a complete packet. Delete our reader and try again
// with more data.
DeleteReader(reader);
return false;
}
//
// The main pipe "listening" loop
//
boolean ListenForever()
{
BOOL wok = FALSE;
CHAR pipe_buffer[8192];
DWORD pipe_buffer_read = 0;
boolean ok = false;
buffer_t *buffer = NewBuffer();
for (;;)
{
// Wait until we see some data on the pipe.
wok = PeekNamedPipe(midi_process_in, NULL, 0, NULL,
&pipe_buffer_read, NULL);
if (!wok)
{
break;
}
else if (pipe_buffer_read == 0)
{
SDL_Delay(1);
continue;
}
// Read data off the pipe and add it to the buffer.
wok = ReadFile(midi_process_in, pipe_buffer, sizeof(pipe_buffer),
&pipe_buffer_read, NULL);
if (!wok)
{
break;
}
ok = Buffer_Push(buffer, pipe_buffer, pipe_buffer_read);
if (!ok)
{
break;
}
do
{
// Read messages off the buffer until we can't anymore.
ok = ParseMessage(buffer);
} while (ok);
}
return false;
}
//=============================================================================
//
// Main Program
//
//
// InitSDL
//
// Start up SDL and SDL_mixer.
//
boolean InitSDL()
{
if (SDL_Init(SDL_INIT_AUDIO) == -1)
{
return false;
}
if (Mix_OpenAudio(snd_samplerate, MIX_DEFAULT_FORMAT, 2, 2048) < 0)
{
return false;
}
atexit(ShutdownSDL);
return true;
}
//
// InitPipes
//
// Ensure that we can communicate.
//
boolean InitPipes()
{
midi_process_in = GetStdHandle(STD_INPUT_HANDLE);
if (midi_process_in == INVALID_HANDLE_VALUE)
{
goto fail;
}
midi_process_out = GetStdHandle(STD_OUTPUT_HANDLE);
if (midi_process_out == INVALID_HANDLE_VALUE)
{
goto fail;
}
atexit(FreePipes);
return true;
fail:
FreePipes();
return false;
}
//
// main
//
// Application entry point.
//
int main(int argc, char *argv[])
{
// Make sure we're not launching this process by itself.
if (argc < 3)
{
MessageBox(NULL, TEXT("This program is tasked with playing Native ")
TEXT("MIDI music, and is intended to be launched by ")
TEXT(PACKAGE_NAME) TEXT("."),
TEXT(PACKAGE_STRING), MB_OK | MB_ICONASTERISK);
return EXIT_FAILURE;
}
// Make sure our Choccolate Doom and midiproc version are lined up.
if (strcmp(PACKAGE_STRING, argv[1]) != 0)
{
char message[1024];
_snprintf(message, sizeof(message),
"It appears that the version of %s and %smidiproc are out "
"of sync. Please reinstall %s.\r\n\r\n"
"Server Version: %s\r\nClient Version: %s",
PACKAGE_NAME, PROGRAM_PREFIX, PACKAGE_NAME,
PACKAGE_STRING, argv[1]);
message[sizeof(message) - 1] = '\0';
MessageBox(NULL, TEXT(message),
TEXT(PACKAGE_STRING), MB_OK | MB_ICONASTERISK);
return EXIT_FAILURE;
}
// Parse out the sample rate - if we can't, default to 44100.
snd_samplerate = strtol(argv[2], NULL, 10);
if (snd_samplerate == LONG_MAX || snd_samplerate == LONG_MIN ||
snd_samplerate == 0)
{
snd_samplerate = 44100;
}
if (!InitPipes())
{
return EXIT_FAILURE;
}
if (!InitSDL())
{
return EXIT_FAILURE;
}
if (!ListenForever())
{
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
#endif // #ifdef _WIN32