From 884dc5de9a2701529efcb151150b2625a409b767 Mon Sep 17 00:00:00 2001 From: Simon Howard Date: Sat, 5 Apr 2014 21:41:38 -0400 Subject: 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. --- src/i_sdlmusic.c | 383 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 383 insertions(+) (limited to 'src/i_sdlmusic.c') diff --git a/src/i_sdlmusic.c b/src/i_sdlmusic.c index c612c6c9..2549b39e 100644 --- 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 @@ char *timidity_cfg_path = ""; 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 SDLIsInitialized(void) 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 @@ static boolean I_SDL_InitMusic(void) } #endif + //! + // @arg + // + // 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 @@ static boolean I_SDL_InitMusic(void) Mix_SetMusicCMD(snd_musiccmd); } + // If we're in GENMIDI mode, try to load sound packs. + if (snd_musicdevice == SNDDEVICE_GENMIDI) + { + LoadSubstituteConfigs(); + } + return music_initialized; } @@ -386,6 +749,26 @@ static void *I_SDL_RegisterSong(void *data, int len) 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" // Reject anything which doesnt have this signature -- cgit v1.2.3