shithub: choc

Download patch

ref: 884dc5de9a2701529efcb151150b2625a409b767
parent: 226dbe09dda81adfb1b2929a832d7484ce3676d8
author: Simon Howard <fraggle@gmail.com>
date: Sat Apr 5 17:41:38 EDT 2014

music: First code for HQ music substitution.

This adds support for high quality music packs that replace Doom's
built-in MIDI music with digital recordings. In particular this allows
recordings of the Roland SC-55 to be used in Chocolate Doom. This is
the first essential step for bug #245.

--- a/src/i_sdlmusic.c
+++ b/src/i_sdlmusic.c
@@ -31,6 +31,7 @@
 #include "SDL.h"
 #include "SDL_mixer.h"
 
+#include "config.h"
 #include "doomtype.h"
 #include "memio.h"
 #include "mus2mid.h"
@@ -38,12 +39,45 @@
 #include "deh_str.h"
 #include "gusconf.h"
 #include "i_sound.h"
+#include "i_system.h"
+#include "m_argv.h"
+#include "m_config.h"
 #include "m_misc.h"
+#include "sha1.h"
 #include "w_wad.h"
 #include "z_zone.h"
 
 #define MAXMIDLENGTH (96 * 1024)
 
+// Structure for music substitution.
+// We store a mapping based on SHA1 checksum -> filename of substitute music
+// file to play, so that substitution occurs based on content rather than
+// lump name. This has some inherent advantages:
+//  * Music for Plutonia (reused from Doom 1) works automatically.
+//  * If a PWAD replaces music, the replacement music is used rather than
+//    the substitute music for the IWAD.
+//  * If a PWAD reuses music from an IWAD (even from a different game), we get
+//    the high quality version of the music automatically (neat!)
+
+typedef struct
+{
+    sha1_digest_t hash;
+    char *filename;
+} subst_music_t;
+
+static subst_music_t *subst_music = NULL;
+static unsigned int subst_music_len = 0;
+
+static const char *subst_config_filenames[] =
+{
+    "doom1-music.cfg",
+    "doom2-music.cfg",
+    "tnt-music.cfg",
+    "heretic-music.cfg",
+    "hexen-music.cfg",
+    "strife-music.cfg",
+};
+
 static boolean music_initialized = false;
 
 // If this is true, this module initialized SDL sound and has the 
@@ -58,6 +92,313 @@
 
 static char *temp_timidity_cfg = NULL;
 
+// Given a MUS lump, look up a substitute MUS file to play instead
+// (or NULL to just use normal MIDI playback).
+
+static char *GetSubstituteMusicFile(void *data, size_t data_len)
+{
+    sha1_context_t context;
+    sha1_digest_t hash;
+    int i;
+
+    // Don't bother doing a hash if we're never going to find anything.
+    if (subst_music_len == 0)
+    {
+        return NULL;
+    }
+
+    SHA1_Init(&context);
+    SHA1_Update(&context, data, data_len);
+    SHA1_Final(hash, &context);
+
+    // Look for a hash that matches.
+
+    for (i = 0; i < subst_music_len; ++i)
+    {
+        if (memcmp(hash, subst_music[i].hash, sizeof(hash)) == 0)
+        {
+            return subst_music[i].filename;
+        }
+    }
+
+    return NULL;
+}
+
+// Add a substitute music file to the lookup list.
+
+static void AddSubstituteMusic(subst_music_t *subst)
+{
+    ++subst_music_len;
+    subst_music =
+        realloc(subst_music, sizeof(subst_music_t) * subst_music_len);
+    memcpy(&subst_music[subst_music_len - 1], subst, sizeof(subst_music_t));
+}
+
+static int ParseHexDigit(char c)
+{
+    c = tolower(c);
+
+    if (c >= '0' && c <= '9')
+    {
+        return c - '0';
+    }
+    else if (c >= 'a' && c <= 'f')
+    {
+        return 10 + (c - 'a');
+    }
+    else
+    {
+        return -1;
+    }
+}
+
+static char *GetFullPath(char *base_filename, char *path)
+{
+    char *basedir, *result;
+    char *p;
+
+    // Starting with directory separator means we have an absolute path,
+    // so just return it.
+    if (path[0] == DIR_SEPARATOR)
+    {
+        return strdup(path);
+    }
+
+#ifdef _WIN32
+    // d:\path\...
+    if (isalpha(path[0]) && path[1] == ':' && path[2] == DIR_SEPARATOR)
+    {
+        return strdup(path);
+    }
+#endif
+
+    // Copy config filename and cut off the filename to just get the
+    // parent dir.
+    basedir = strdup(base_filename);
+    p = strrchr(basedir, DIR_SEPARATOR);
+    if (p != NULL)
+    {
+        p[1] = '\0';
+        result = M_StringJoin(basedir, path, NULL);
+    }
+    else
+    {
+        result = strdup(path);
+    }
+    free(basedir);
+
+    return result;
+}
+
+// Parse a line from substitute music configuration file; returns error
+// message or NULL for no error.
+
+static char *ParseSubstituteLine(char *filename, char *line)
+{
+    subst_music_t subst;
+    char *p;
+    int hash_index;
+
+    // Skip leading spaces.
+    for (p = line; *p != '\0' && isspace(*p); ++p);
+
+    // Comment or empty line? This is valid syntax, so just return success.
+    if (*p == '#' || *p == '\0')
+    {
+        return NULL;
+    }
+
+    // Read hash.
+    hash_index = 0;
+    while (*p != '\0' && *p != '=' && !isspace(*p))
+    {
+        int d1, d2;
+
+        d1 = ParseHexDigit(p[0]);
+        d2 = ParseHexDigit(p[1]);
+
+        if (d1 < 0 || d2 < 0)
+        {
+            return "Invalid hex digit in SHA1 hash";
+        }
+        else if (hash_index >= sizeof(sha1_digest_t))
+        {
+            return "SHA1 hash too long";
+        }
+
+        subst.hash[hash_index] = (d1 << 4) | d2;
+        ++hash_index;
+
+        p += 2;
+    }
+
+    if (hash_index != sizeof(sha1_digest_t))
+    {
+        return "SHA1 hash too short";
+    }
+
+    // Skip spaces.
+    for (; *p != '\0' && isspace(*p); ++p);
+
+    if (*p != '=')
+    {
+        return "Expected '='";
+    }
+
+    ++p;
+
+    // Skip spaces.
+    for (; *p != '\0' && isspace(*p); ++p);
+
+    // We're now at the filename. Cut off trailing space characters.
+    while (strlen(p) > 0 && isspace(p[strlen(p) - 1]))
+    {
+        p[strlen(p) - 1] = '\0';
+    }
+
+    if (strlen(p) == 0)
+    {
+        return "No filename specified for music substitution";
+    }
+
+    // Expand full path and add to our database of substitutes.
+    subst.filename = GetFullPath(filename, p);
+    AddSubstituteMusic(&subst);
+
+    return NULL;
+}
+
+// Read a substitute music configuration file.
+
+static boolean ReadSubstituteConfig(char *filename)
+{
+    char line[128];
+    FILE *fs;
+    char *error;
+    int linenum = 1;
+    int old_subst_music_len;
+
+    fs = fopen(filename, "r");
+
+    if (fs == NULL)
+    {
+        return false;
+    }
+
+    old_subst_music_len = subst_music_len;
+
+    while (!feof(fs))
+    {
+        M_StringCopy(line, "", sizeof(line));
+        fgets(line, sizeof(line), fs);
+
+        error = ParseSubstituteLine(filename, line);
+
+        if (error != NULL)
+        {
+            fprintf(stderr, "%s:%i: Error: %s\n", filename, linenum, error);
+        }
+
+        ++linenum;
+    }
+
+    fclose(fs);
+
+    return true;
+}
+
+// Find substitute configs and try to load them.
+
+static void LoadSubstituteConfigs(void)
+{
+    char *musicdir;
+    char *path;
+    unsigned int i;
+
+    if (!strcmp(configdir, ""))
+    {
+        musicdir = "";
+    }
+    else
+    {
+        musicdir = M_StringJoin(configdir, "music/", NULL);
+    }
+
+    // Load all music packs. We always load all music substitution packs for
+    // all games. Why? Suppose we have a Doom PWAD that reuses some music from
+    // Heretic. If we have the Heretic music pack loaded, then we get an
+    // automatic substitution.
+    for (i = 0; i < arrlen(subst_config_filenames); ++i)
+    {
+        path = M_StringJoin(musicdir, subst_config_filenames[i], NULL);
+        ReadSubstituteConfig(path);
+        free(path);
+    }
+
+    free(musicdir);
+
+    if (subst_music_len > 0)
+    {
+        printf("Loaded %i music substitutions from config files.\n",
+               subst_music_len);
+    }
+}
+
+// Dump an example config file containing checksums for all MIDI music
+// found in the WAD directory.
+
+static void DumpSubstituteConfig(char *filename)
+{
+    sha1_context_t context;
+    sha1_digest_t digest;
+    char name[9];
+    byte *data;
+    FILE *fs;
+    int lumpnum, h;
+
+    fs = fopen(filename, "w");
+
+    if (fs == NULL)
+    {
+        I_Error("Failed to open %s for writing", filename);
+        return;
+    }
+
+    fprintf(fs, "# Example %s substitute MIDI file.\n\n", PACKAGE_NAME);
+
+    for (lumpnum = 0; lumpnum < numlumps; ++lumpnum)
+    {
+        strncpy(name, lumpinfo[lumpnum].name, 8);
+        name[8] = '\0';
+
+        if (!M_StringStartsWith(name, "D_"))
+        {
+            continue;
+        }
+
+        // Calculate hash.
+        data = W_CacheLumpNum(lumpnum, PU_STATIC);
+        SHA1_Init(&context);
+        SHA1_Update(&context, data, W_LumpLength(lumpnum));
+        SHA1_Final(digest, &context);
+        W_ReleaseLumpNum(lumpnum);
+
+        // Print line.
+        for (h = 0; h < sizeof(sha1_digest_t); ++h)
+        {
+            fprintf(fs, "%02x", digest[h]);
+        }
+
+        fprintf(fs, " = %s.mp3\n", name);
+    }
+
+    fprintf(fs, "\n");
+    fclose(fs);
+
+    printf("Substitute MIDI config file written to %s.\n", filename);
+    I_Quit();
+}
+
 // If the temp_timidity_cfg config variable is set, generate a "wrapper"
 // config file for Timidity to point to the actual config file. This
 // is needed to inject a "dir" command so that the patches are read
@@ -167,6 +508,8 @@
 
 static boolean I_SDL_InitMusic(void)
 {
+    int i;
+
     // SDL_mixer prior to v1.2.11 has a bug that causes crashes
     // with MIDI playback.  Print a warning message if we are
     // using an old version.
@@ -188,6 +531,20 @@
     }
 #endif
 
+    //!
+    // @arg <output filename>
+    //
+    // Read all MIDI files from loaded WAD files, dump an example substitution
+    // music config file to the specified filename and quit.
+    //
+
+    i = M_CheckParmWithArgs("-dumpsubstconfig", 1);
+
+    if (i > 0)
+    {
+        DumpSubstituteConfig(myargv[i + 1]);
+    }
+
     // If SDL_mixer is not initialized, we have to initialize it
     // and have the responsibility to shut it down later on.
 
@@ -229,6 +586,12 @@
         Mix_SetMusicCMD(snd_musiccmd);
     }
 
+    // If we're in GENMIDI mode, try to load sound packs.
+    if (snd_musicdevice == SNDDEVICE_GENMIDI)
+    {
+        LoadSubstituteConfigs();
+    }
+
     return music_initialized;
 }
 
@@ -384,6 +747,26 @@
     if (!music_initialized)
     {
         return NULL;
+    }
+
+    // See if we're substituting this MUS for a high-quality replacement.
+    filename = GetSubstituteMusicFile(data, len);
+
+    if (filename != NULL)
+    {
+        music = Mix_LoadMUS(filename);
+
+        if (music == NULL)
+        {
+            // Fall through and play MIDI normally, but print an error
+            // message.
+            fprintf(stderr, "Failed to load substitute music file: %s: %s\n",
+                    filename, Mix_GetError());
+        }
+        else
+        {
+            return music;
+        }
     }
 
     // MUS files begin with "MUS"