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"