shithub: choc

Download patch

ref: 5383fa67ab4ddcd01c9352b47a3ad34f2ebf9da7
parent: 52ccdb80987010c768d6f65a1b6c87843a1fcdf3
parent: 83a7dfb9af4b8819b6d578044809fc91cc21661a
author: Jonathan Dowland <jon+github@alcopop.org>
date: Mon Jul 17 09:30:37 EDT 2017

Merge pull request #881 from AlexMax/chocolate-midivolume

Separately controllable MIDI volume on Windows Vista+

diff: cannot open b/midiproc//null: file does not exist: 'b/midiproc//null'
--- a/Makefile.am
+++ b/Makefile.am
@@ -70,7 +70,7 @@
 
 MAINTAINERCLEANFILES =  $(AUX_DIST_GEN)
 
-SUBDIRS=textscreen opl pcsound data src man
+SUBDIRS=textscreen midiproc opl pcsound data src man
 
 DIST_SUBDIRS=pkg $(SUBDIRS)
 
--- a/configure.ac
+++ b/configure.ac
@@ -156,6 +156,7 @@
 man/bash-completion/heretic.template
 man/bash-completion/hexen.template
 man/bash-completion/strife.template
+midiproc/Makefile
 opl/Makefile
 opl/examples/Makefile
 pcsound/Makefile
--- /dev/null
+++ b/midiproc/.gitignore
@@ -1,0 +1,6 @@
+Makefile.in
+Makefile
+*.exe
+.deps
+tags
+TAGS
--- /dev/null
+++ b/midiproc/Makefile.am
@@ -1,0 +1,11 @@
+
+AM_CFLAGS=-I$(top_srcdir)/src @SDLMIXER_CFLAGS@
+
+if HAVE_WINDRES
+
+noinst_PROGRAMS = @PROGRAM_PREFIX@midiproc
+
+@PROGRAM_PREFIX@midiproc_LDADD = @SDLMIXER_LIBS@
+@PROGRAM_PREFIX@midiproc_SOURCES = buffer.c buffer.h main.c proto.h
+
+endif
--- /dev/null
+++ b/midiproc/buffer.c
@@ -1,0 +1,253 @@
+//
+// 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:
+//     A simple buffer and reader implementation.
+//
+
+#ifdef _WIN32
+
+#include "buffer.h"
+
+#include <stdlib.h>
+
+//
+// Create a new buffer.
+//
+buffer_t *NewBuffer()
+{
+    buffer_t *buf = malloc(sizeof(buffer_t));
+
+    buf->buffer_end = buf->buffer + BUFFER_SIZE;
+    Buffer_Clear(buf);
+
+    return buf;
+}
+
+//
+// Free a buffer.
+//
+void DeleteBuffer(buffer_t *buf)
+{
+    free(buf);
+}
+
+//
+// Return the data in the buffer.
+//
+int Buffer_Data(buffer_t *buf, byte **data)
+{
+    *data = buf->data;
+    return buf->data_len;
+}
+
+//
+// Push data onto the end of the buffer.
+//
+boolean Buffer_Push(buffer_t *buf, const void *data, int len)
+{
+    ptrdiff_t space_begin, space_end;
+
+    if (len <= 0)
+    {
+        // Do nothing, successfully.
+        return true;
+    }
+
+    space_begin = buf->data - buf->buffer;
+    space_end = buf->buffer_end - buf->data_end;
+
+    if (len > space_end)
+    {
+        if (len > space_begin + space_end)
+        {
+            // Don't overflow the buffer.
+            return false;
+        }
+
+        // Move our data to the front of the buffer.
+        memmove(buf->buffer, buf->data, buf->data_len);
+        buf->data = buf->buffer;
+        buf->data_end = buf->buffer + buf->data_len;
+    }
+
+    // Append to the buffer.
+    memcpy(buf->data_end, data, len);
+    buf->data_len += len;
+    buf->data_end = buf->data + buf->data_len;
+
+    return true;
+}
+
+
+//
+// Shift len bytes off of the front of the buffer.
+//
+void Buffer_Shift(buffer_t *buf, int len)
+{
+    ptrdiff_t max_shift;
+
+    if (len <= 0)
+    {
+        // Do nothing.
+        return;
+    }
+
+    max_shift = buf->data_end - buf->data;
+    if (len >= max_shift)
+    {
+        // If the operation would clear the buffer, just zero everything.
+        Buffer_Clear(buf);
+    }
+    else
+    {
+        buf->data += len;
+        buf->data_len -= len;
+    }
+}
+
+//
+// Clear the buffer.
+//
+void Buffer_Clear(buffer_t *buf)
+{
+    buf->data = buf->buffer;
+    buf->data_end = buf->buffer;
+    buf->data_len = 0;
+}
+
+//
+// Create a new buffer reader.
+//
+// WARNING: This reader will invalidate if the underlying buffer changes.
+//          Use it, then delete it before you touch the underlying buffer again.
+//
+buffer_reader_t *NewReader(buffer_t* buffer)
+{
+    buffer_reader_t *reader = malloc(sizeof(buffer_reader_t));
+
+    reader->buffer = buffer;
+    reader->pos = buffer->data;
+
+    return reader;
+}
+
+//
+// Delete a buffer reader.
+//
+void DeleteReader(buffer_reader_t *reader)
+{
+    free(reader);
+}
+
+//
+// Count the number of bytes read thus far.
+//
+int Reader_BytesRead(buffer_reader_t *reader)
+{
+    return reader->pos - reader->buffer->data;
+}
+
+//
+// Read an unsigned byte from a buffer.
+//
+boolean Reader_ReadInt8(buffer_reader_t *reader, uint8_t *out)
+{
+    byte *data, *data_end;
+    int len = Buffer_Data(reader->buffer, &data);
+
+    data_end = data + len;
+
+    if (data_end - reader->pos < 1)
+    {
+        return false;
+    }
+
+    *out = (uint8_t)*reader->pos;
+    reader->pos += 1;
+
+    return true;
+}
+
+//
+// Read an unsigned short from a buffer.
+//
+boolean Reader_ReadInt16(buffer_reader_t *reader, uint16_t *out)
+{
+    byte *data, *data_end, *dp;
+    int len = Buffer_Data(reader->buffer, &data);
+
+    data_end = data + len;
+    dp = reader->pos;
+
+    if (data_end - reader->pos < 2)
+    {
+        return false;
+    }
+
+    *out = (uint16_t)((dp[0] << 8) | dp[1]);
+    reader->pos += 2;
+
+    return true;
+}
+
+//
+// Read an unsigned int from a buffer.
+//
+boolean Reader_ReadInt32(buffer_reader_t *reader, uint32_t *out)
+{
+    byte *data, *data_end, *dp;
+    int len = Buffer_Data(reader->buffer, &data);
+
+    data_end = data + len;
+    dp = reader->pos;
+
+    if (data_end - reader->pos < 4)
+    {
+        return false;
+    }
+
+    *out = (uint32_t)((dp[0] << 24) | (dp[1] << 16) | (dp[2] << 8) | dp[3]);
+    reader->pos += 4;
+
+    return true;
+}
+
+//
+// Read a string from a buffer.
+//
+char *Reader_ReadString(buffer_reader_t *reader)
+{
+    byte *data, *data_start, *data_end, *dp;
+    int len = Buffer_Data(reader->buffer, &data);
+
+    data_start = reader->pos;
+    data_end = data + len;
+    dp = reader->pos;
+
+    while (dp < data_end && *dp != '\0')
+    {
+        dp++;
+    }
+
+    if (dp >= data_end)
+    {
+        // Didn't see a null terminator, not a complete string.
+        return NULL;
+    }
+
+    reader->pos = dp + 1;
+    return (char*)data_start;
+}
+
+#endif // #ifdef _WIN32
--- /dev/null
+++ b/midiproc/buffer.h
@@ -1,0 +1,54 @@
+//
+// 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:
+//     A simple buffer and reader implementation.
+//
+
+#ifndef __BUFFER__
+#define __BUFFER__
+
+#include "../src/doomtype.h"
+
+#define BUFFER_SIZE 1024
+
+typedef struct {
+    byte  buffer[BUFFER_SIZE]; // Buffer.
+    byte *buffer_end;          // End of Buffer.
+    byte *data;                // Start of actual data.
+    byte *data_end;            // End of actual data.
+    int   data_len;            // Length of actual data.
+} buffer_t;
+
+typedef struct {
+    buffer_t *buffer;
+    byte     *pos;
+} buffer_reader_t;
+
+buffer_t *NewBuffer();
+void DeleteBuffer(buffer_t* buf);
+int Buffer_Data(buffer_t *buf, byte **data);
+boolean Buffer_Push(buffer_t *buf, const void *data, int len);
+void Buffer_Shift(buffer_t *buf, int len);
+void Buffer_Clear(buffer_t *buf);
+
+buffer_reader_t *NewReader(buffer_t* buffer);
+void DeleteReader(buffer_reader_t *reader);
+int Reader_BytesRead(buffer_reader_t *reader);
+boolean Reader_ReadInt8(buffer_reader_t *reader, uint8_t *out);
+boolean Reader_ReadInt16(buffer_reader_t *reader, uint16_t *out);
+boolean Reader_ReadInt32(buffer_reader_t *reader, uint32_t *out);
+char *Reader_ReadString(buffer_reader_t *reader);
+
+#endif
+
--- /dev/null
+++ b/midiproc/main.c
@@ -1,0 +1,459 @@
+//
+// 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);
+}
+
+//
+// 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);
+
+    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;
+    }
+
+    RegisterSong(filename);
+
+    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();
+
+    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
--- /dev/null
+++ b/midiproc/proto.h
@@ -1,0 +1,31 @@
+//
+// 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:
+//     Headers for all types of midipipe messages.
+//
+
+#ifndef __PROTO__
+#define __PROTO__
+
+typedef enum {
+    MIDIPIPE_PACKET_TYPE_REGISTER_SONG,
+    MIDIPIPE_PACKET_TYPE_REGISTER_SONG_ACK,
+    MIDIPIPE_PACKET_TYPE_SET_VOLUME,
+    MIDIPIPE_PACKET_TYPE_PLAY_SONG,
+    MIDIPIPE_PACKET_TYPE_STOP_SONG,
+    MIDIPIPE_PACKET_TYPE_SHUTDOWN
+} net_midipipe_packet_type_t;
+
+#endif
+
--- a/pkg/win32/GNUmakefile
+++ b/pkg/win32/GNUmakefile
@@ -43,14 +43,17 @@
 	./cp-with-libs --ldflags="$(LDFLAGS)" \
 	               $(TOPLEVEL)/src/$(PROGRAM_PREFIX)setup.exe \
 	               $@/$(PROGRAM_PREFIX)$*-setup.exe
+	./cp-with-libs --ldflags="$(LDFLAGS)" \
+	               $(TOPLEVEL)/midiproc/$(PROGRAM_PREFIX)midiproc.exe \
+	               $@/$(PROGRAM_PREFIX)midiproc.exe
 
 	$(STRIP) $@/*.exe $@/*.dll
-	
+
 	for f in $(DOC_FILES); do                                \
 		cp $(TOPLEVEL)/$$f $@/$$(basename $$f .md).txt;  \
 	done
 	cp $(TOPLEVEL)/man/CMDLINE.$* $@/CMDLINE.txt
-	
+
 	$(TOPLEVEL)/man/simplecpp -D_WIN32 -DPRECOMPILED  \
 	             -D$(shell echo $* | tr a-z A-Z)      \
 	         < $(TOPLEVEL)/man/INSTALL.template       \
@@ -59,4 +62,3 @@
 clean:
 	rm -f $(ZIPS)
 	rm -rf staging-*
-
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -68,6 +68,7 @@
 i_input.c            i_input.h             \
 i_joystick.c         i_joystick.h          \
                      i_swap.h              \
+i_midipipe.c         i_midipipe.h          \
 i_oplmusic.c                               \
 i_pcsound.c                                \
 i_sdlmusic.c                               \
--- /dev/null
+++ b/src/i_midipipe.c
@@ -1,0 +1,475 @@
+//
+// Copyright(C) 2013 James Haley et al.
+// 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:
+//     Client Interface to Midi Server
+//
+
+#if _WIN32
+
+#include <stdlib.h>
+#include <sys/stat.h>
+
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+
+#include "i_midipipe.h"
+
+#include "config.h"
+#include "i_sound.h"
+#include "i_timer.h"
+#include "m_misc.h"
+#include "net_packet.h"
+
+#include "../midiproc/proto.h"
+
+#if defined(_DEBUG)
+#define DEBUGOUT(s) puts(s)
+#else
+#define DEBUGOUT(s)
+#endif
+
+//=============================================================================
+//
+// Public Data
+//
+
+// True if the midi proces was initialized at least once and has not been
+// explicitly shut down.  This remains true if the server is momentarily
+// unreachable.
+boolean midi_server_initialized;
+
+// True if the current track is being handled via the MIDI server.
+boolean midi_server_registered;
+
+//=============================================================================
+//
+// Data
+//
+
+#define MIDIPIPE_MAX_WAIT 500 // Max amount of ms to wait for expected data.
+
+static HANDLE  midi_process_in_reader;  // Input stream for midi process.
+static HANDLE  midi_process_in_writer;
+static HANDLE  midi_process_out_reader; // Output stream for midi process.
+static HANDLE  midi_process_out_writer;
+
+//=============================================================================
+//
+// Private functions
+//
+
+//
+// FreePipes
+//
+// Free all pipes in use by this module.
+//
+static void FreePipes()
+{
+    if (midi_process_in_reader != NULL)
+    {
+        CloseHandle(midi_process_in_reader);
+        midi_process_in_reader = NULL;
+    }
+    if (midi_process_in_writer != NULL)
+    {
+        CloseHandle(midi_process_in_writer);
+        midi_process_in_writer = NULL;
+    }
+    if (midi_process_out_reader != NULL)
+    {
+        CloseHandle(midi_process_out_reader);
+        midi_process_in_reader = NULL;
+    }
+    if (midi_process_out_writer != NULL)
+    {
+        CloseHandle(midi_process_out_writer);
+        midi_process_out_writer = NULL;
+    }
+}
+
+//
+// UsingNativeMidi
+//
+// Enumerate all music decoders and return true if NATIVEMIDI is one of them.
+//
+// If this is the case, using the MIDI server is probably necessary.  If not,
+// we're likely using Timidity and thus don't need to start the server.
+//
+static boolean UsingNativeMidi()
+{
+    int i;
+    int decoders = Mix_GetNumMusicDecoders();
+
+    for (i = 0; i < decoders; i++)
+    {
+        if (strcmp(Mix_GetMusicDecoder(i), "NATIVEMIDI") == 0)
+        {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+//
+// WritePipe
+//
+// Writes packet data to the subprocess' standard in.
+//
+static boolean WritePipe(net_packet_t *packet)
+{
+    DWORD bytes_written;
+    BOOL ok = WriteFile(midi_process_in_writer, packet->data, packet->len,
+                        &bytes_written, NULL);
+
+    return ok;
+}
+
+//
+// ExpectPipe
+//
+// Expect the contents of a packet off of the subprocess' stdout.  If the
+// response is unexpected, or doesn't arrive within a specific amuont of time,
+// assume the subprocess is in an unknown state.
+//
+static boolean ExpectPipe(net_packet_t *packet)
+{
+    int start;
+    BOOL ok;
+    CHAR pipe_buffer[8192];
+    DWORD pipe_buffer_read = 0;
+
+    if (packet->len > sizeof(pipe_buffer))
+    {
+        // The size of the packet we're expecting is larger than our buffer
+        // size, so bail out now.
+        return false;
+    }
+
+    start = I_GetTimeMS();
+
+    do
+    {
+        // Wait until we see exactly the amount of data we expect on the pipe.
+        ok = PeekNamedPipe(midi_process_out_reader, NULL, 0, NULL,
+                           &pipe_buffer_read, NULL);
+        if (!ok)
+        {
+            break;
+        }
+        else if (pipe_buffer_read < packet->len)
+        {
+            I_Sleep(1);
+            continue;
+        }
+
+        // Read precisely the number of bytes we're expecting, and no more.
+        ok = ReadFile(midi_process_out_reader, pipe_buffer, packet->len,
+                      &pipe_buffer_read, NULL);
+        if (!ok || pipe_buffer_read != packet->len)
+        {
+            break;
+        }
+
+        // Compare our data buffer to the packet.
+        if (memcmp(packet->data, pipe_buffer, packet->len) != 0)
+        {
+            break;
+        }
+
+        return true;
+
+        // Continue looping as long as we don't exceed our maximum wait time.
+    } while (I_GetTimeMS() - start <= MIDIPIPE_MAX_WAIT);
+
+    // TODO: Deal with the wedged process?
+    return false;
+}
+
+//
+// RemoveFileSpec
+//
+// A reimplementation of PathRemoveFileSpec that doesn't bring in Shlwapi
+//
+void RemoveFileSpec(TCHAR *path, size_t size)
+{
+    TCHAR *fp = NULL;
+
+    fp = &path[size];
+    while (path <= fp && *fp != DIR_SEPARATOR)
+    {
+        fp--;
+    }
+    *(fp + 1) = '\0';
+}
+
+//=============================================================================
+//
+// Protocol Commands
+//
+
+//
+// I_MidiPipe_RegisterSong
+//
+// Tells the MIDI subprocess to load a specific filename for playing.  This
+// function blocks until there is an acknowledgement from the server.
+//
+boolean I_MidiPipe_RegisterSong(char *filename)
+{
+    boolean ok;
+    net_packet_t *packet;
+
+    packet = NET_NewPacket(64);
+    NET_WriteInt16(packet, MIDIPIPE_PACKET_TYPE_REGISTER_SONG);
+    NET_WriteString(packet, filename);
+    ok = WritePipe(packet);
+    NET_FreePacket(packet);
+
+    if (!ok)
+    {
+        DEBUGOUT("I_MidiPipe_RegisterSong failed");
+        return false;
+    }
+
+    packet = NET_NewPacket(2);
+    NET_WriteInt16(packet, MIDIPIPE_PACKET_TYPE_REGISTER_SONG_ACK);
+    ok = ExpectPipe(packet);
+    NET_FreePacket(packet);
+
+    if (!ok)
+    {
+        DEBUGOUT("I_MidiPipe_RegisterSong ack failed");
+        return false;
+    }
+
+    midi_server_registered = true;
+
+    DEBUGOUT("I_MidiPipe_RegisterSong succeeded");
+    return true;
+}
+
+//
+// I_MidiPipe_SetVolume
+//
+// Tells the MIDI subprocess to set a specific volume for the song.
+//
+void I_MidiPipe_SetVolume(int vol)
+{
+    boolean ok;
+    net_packet_t *packet;
+
+    packet = NET_NewPacket(6);
+    NET_WriteInt16(packet, MIDIPIPE_PACKET_TYPE_SET_VOLUME);
+    NET_WriteInt32(packet, vol);
+    ok = WritePipe(packet);
+    NET_FreePacket(packet);
+
+    if (!ok)
+    {
+        DEBUGOUT("I_MidiPipe_SetVolume failed");
+        return;
+    }
+
+    DEBUGOUT("I_MidiPipe_SetVolume succeeded");
+}
+
+//
+// I_MidiPipe_PlaySong
+//
+// Tells the MIDI subprocess to play the currently loaded song.
+//
+void I_MidiPipe_PlaySong(int loops)
+{
+    boolean ok;
+    net_packet_t *packet;
+
+    packet = NET_NewPacket(6);
+    NET_WriteInt16(packet, MIDIPIPE_PACKET_TYPE_PLAY_SONG);
+    NET_WriteInt32(packet, loops);
+    ok = WritePipe(packet);
+    NET_FreePacket(packet);
+
+    if (!ok)
+    {
+        DEBUGOUT("I_MidiPipe_PlaySong failed");
+        return;
+    }
+
+    DEBUGOUT("I_MidiPipe_PlaySong succeeded");
+}
+
+//
+// I_MidiPipe_StopSong
+//
+// Tells the MIDI subprocess to stop playing the currently loaded song.
+//
+void I_MidiPipe_StopSong()
+{
+    boolean ok;
+    net_packet_t *packet;
+
+    packet = NET_NewPacket(2);
+    NET_WriteInt16(packet, MIDIPIPE_PACKET_TYPE_STOP_SONG);
+    ok = WritePipe(packet);
+    NET_FreePacket(packet);
+
+    midi_server_registered = false;
+
+    if (!ok)
+    {
+        DEBUGOUT("I_MidiPipe_StopSong failed");
+        return;
+    }
+
+    DEBUGOUT("I_MidiPipe_StopSong succeeded");
+}
+
+//
+// I_MidiPipe_ShutdownServer
+//
+// Tells the MIDI subprocess to shutdown.
+//
+void I_MidiPipe_ShutdownServer()
+{
+    boolean ok;
+    net_packet_t *packet;
+
+    packet = NET_NewPacket(2);
+    NET_WriteInt16(packet, MIDIPIPE_PACKET_TYPE_SHUTDOWN);
+    ok = WritePipe(packet);
+    NET_FreePacket(packet);
+
+    FreePipes();
+
+    midi_server_initialized = false;
+
+    if (!ok)
+    {
+        DEBUGOUT("I_MidiPipe_ShutdownServer failed");
+        return;
+    }
+
+    DEBUGOUT("I_MidiPipe_ShutdownServer succeeded");
+}
+
+//=============================================================================
+//
+// Public Interface
+//
+
+//
+// I_MidiPipeInitServer
+//
+// Start up the MIDI server.
+//
+boolean I_MidiPipe_InitServer()
+{
+    TCHAR dirname[MAX_PATH + 1];
+    DWORD dirname_len;
+    char *module = NULL;
+    char *cmdline = NULL;
+    char snd_samplerate_buf[8];
+    SECURITY_ATTRIBUTES sec_attrs;
+    PROCESS_INFORMATION proc_info;
+    STARTUPINFO startup_info;
+    BOOL ok;
+
+    if (!UsingNativeMidi() || strlen(snd_musiccmd) > 0)
+    {
+        // If we're not using native MIDI, or if we're playing music through
+        // an exteranl program, we don't need to start the server.
+        return false;
+    }
+
+    // Get directory name
+    memset(dirname, 0, sizeof(dirname));
+    dirname_len = GetModuleFileName(NULL, dirname, MAX_PATH);
+    if (dirname_len == 0)
+    {
+        return false;
+    }
+    RemoveFileSpec(dirname, dirname_len);
+
+    // Define the module.
+    module = PROGRAM_PREFIX "midiproc.exe";
+
+    // Define the command line.  Version and Sample Rate follow the
+    // executable name.
+    M_snprintf(snd_samplerate_buf, sizeof(snd_samplerate_buf),
+               "%d", snd_samplerate);
+    cmdline = M_StringJoin(module, " \"" PACKAGE_STRING "\"", " ",
+                           snd_samplerate_buf, NULL);
+
+    // Set up pipes
+    memset(&sec_attrs, 0, sizeof(SECURITY_ATTRIBUTES));
+    sec_attrs.nLength = sizeof(SECURITY_ATTRIBUTES);
+    sec_attrs.bInheritHandle = TRUE;
+    sec_attrs.lpSecurityDescriptor = NULL;
+
+    if (!CreatePipe(&midi_process_in_reader, &midi_process_in_writer, &sec_attrs, 0))
+    {
+        DEBUGOUT("Could not initialize midiproc stdin");
+        return false;
+    }
+
+    if (!SetHandleInformation(midi_process_in_writer, HANDLE_FLAG_INHERIT, 0))
+    {
+        DEBUGOUT("Could not disinherit midiproc stdin");
+        return false;
+    }
+
+    if (!CreatePipe(&midi_process_out_reader, &midi_process_out_writer, &sec_attrs, 0))
+    {
+        DEBUGOUT("Could not initialize midiproc stdout/stderr");
+        return false;
+    }
+
+    if (!SetHandleInformation(midi_process_out_reader, HANDLE_FLAG_INHERIT, 0))
+    {
+        DEBUGOUT("Could not disinherit midiproc stdin");
+        return false;
+    }
+
+    // Launch the subprocess
+    memset(&proc_info, 0, sizeof(proc_info));
+    memset(&startup_info, 0, sizeof(startup_info));
+    startup_info.cb = sizeof(startup_info);
+    startup_info.hStdInput = midi_process_in_reader;
+    startup_info.hStdOutput = midi_process_out_writer;
+    startup_info.dwFlags = STARTF_USESTDHANDLES;
+
+    ok = CreateProcess(TEXT(module), TEXT(cmdline), NULL, NULL, TRUE,
+                       0, NULL, dirname, &startup_info, &proc_info);
+
+    if (!ok)
+    {
+        FreePipes();
+        free(cmdline);
+
+        return false;
+    }
+
+    // Since the server has these handles, we don't need them anymore.
+    CloseHandle(midi_process_in_reader);
+    midi_process_in_reader = NULL;
+    CloseHandle(midi_process_out_writer);
+    midi_process_out_writer = NULL;
+
+    midi_server_initialized = true;
+    return true;
+}
+
+#endif
+
--- /dev/null
+++ b/src/i_midipipe.h
@@ -1,0 +1,42 @@
+//
+// Copyright(C) 2013 James Haley et al.
+// 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:
+//     Client Interface to Midi Server
+//
+
+#ifndef __I_MIDIPIPE__
+#define __I_MIDIPIPE__
+
+#if _WIN32
+
+#include "SDL_mixer.h"
+
+#include "doomtype.h"
+
+extern boolean midi_server_initialized;
+extern boolean midi_server_registered;
+
+boolean I_MidiPipe_RegisterSong(char *filename);
+void I_MidiPipe_SetVolume(int vol);
+void I_MidiPipe_PlaySong(int loops);
+void I_MidiPipe_StopSong();
+void I_MidiPipe_ShutdownServer();
+
+boolean I_MidiPipe_InitServer();
+
+#endif
+
+#endif
+
--- a/src/i_sdlmusic.c
+++ b/src/i_sdlmusic.c
@@ -25,6 +25,8 @@
 #include "SDL.h"
 #include "SDL_mixer.h"
 
+#include "i_midipipe.h"
+
 #include "config.h"
 #include "doomtype.h"
 #include "memio.h"
@@ -876,6 +878,9 @@
 {
     if (music_initialized)
     {
+#if defined(_WIN32)
+        I_MidiPipe_ShutdownServer();
+#endif
         Mix_HaltMusic();
         music_initialized = false;
 
@@ -972,6 +977,11 @@
         LoadSubstituteConfigs();
     }
 
+#if defined(_WIN32)
+    // [AM] Start up midiproc to handle playing MIDI music.
+    I_MidiPipe_InitServer();
+#endif
+
     return music_initialized;
 }
 
@@ -993,6 +1003,9 @@
         vol = (current_music_volume * MIX_MAX_VOLUME) / 127;
     }
 
+#if defined(_WIN32)
+    I_MidiPipe_SetVolume(vol);
+#endif
     Mix_VolumeMusic(vol);
 }
 
@@ -1017,7 +1030,11 @@
         return;
     }
 
+#if defined(_WIN32)
+    if (handle == NULL && !midi_server_registered)
+#else
     if (handle == NULL)
+#endif
     {
         return;
     }
@@ -1044,7 +1061,18 @@
         SDL_UnlockAudio();
     }
 
+#if defined(_WIN32)
+    if (midi_server_registered)
+    {
+        I_MidiPipe_PlaySong(loops);
+    }
+    else
+    {
+        Mix_PlayMusic(current_track_music, loops);
+    }
+#else
     Mix_PlayMusic(current_track_music, loops);
+#endif
 }
 
 static void I_SDL_PauseSong(void)
@@ -1078,7 +1106,19 @@
         return;
     }
 
+#if defined(_WIN32)
+    if (midi_server_registered)
+    {
+        I_MidiPipe_StopSong();
+    }
+    else
+    {
+        Mix_HaltMusic();
+    }
+#else
     Mix_HaltMusic();
+#endif
+
     playing_substitute = false;
     current_track_music = NULL;
 }
@@ -1189,14 +1229,35 @@
     // by now, but Mix_SetMusicCMD() only works with Mix_LoadMUS(), so
     // we have to generate a temporary file.
 
+#if defined(_WIN32)
+    // [AM] If we do not have an external music command defined, play
+    //      music with the MIDI server.
+    if (midi_server_initialized)
+    {
+        music = NULL;
+        if (!I_MidiPipe_RegisterSong(filename))
+        {
+            fprintf(stderr, "Error loading midi: %s\n",
+                "Could not communicate with midiproc.");
+        }
+    }
+    else
+    {
+        music = Mix_LoadMUS(filename);
+        if (music == NULL)
+        {
+            // Failed to load
+            fprintf(stderr, "Error loading midi: %s\n", Mix_GetError());
+        }
+    }
+#else
     music = Mix_LoadMUS(filename);
-
     if (music == NULL)
     {
         // Failed to load
-
         fprintf(stderr, "Error loading midi: %s\n", Mix_GetError());
     }
+#endif
 
     // Remove the temporary MIDI file; however, when using an external
     // MIDI program we can't delete the file. Otherwise, the program