shithub: choc

ref: 350fb1e60c6761d7171df663035846bacc2b8fde
dir: /src/i_musicpack.c/

View raw version
//
// Copyright(C) 1993-1996 Id Software, Inc.
// Copyright(C) 2005-2014 Simon Howard
//
// 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:
//	System interface for music.
//


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#include "SDL.h"
#include "SDL_mixer.h"

#include "i_glob.h"

#include "config.h"
#include "doomtype.h"
#include "memio.h"
#include "mus2mid.h"

#include "deh_str.h"
#include "gusconf.h"
#include "i_sound.h"
#include "i_system.h"
#include "i_swap.h"
#include "m_argv.h"
#include "m_config.h"
#include "m_misc.h"
#include "sha1.h"
#include "w_wad.h"
#include "z_zone.h"


char *music_pack_path = "";


#ifndef DISABLE_SDL2MIXER


#define MID_HEADER_MAGIC "MThd"
#define MUS_HEADER_MAGIC "MUS\x1a"

// Starting with 2.6.0, SDL_mixer supports OGG and FLAC looping natively.
// TODO: Once SDL_mixer 2.6.0+ is a requirement, delete the old looping code.
#if !defined(USE_SDL_MIXER_LOOPING)
#if defined(SDL_MIXER_VERSION_ATLEAST)
#if SDL_MIXER_VERSION_ATLEAST(2, 6, 0)
#define USE_SDL_MIXER_LOOPING 1
#else
#define USE_SDL_MIXER_LOOPING 0
#endif // SDL_MIXER_VERSION_ATLEAST(2, 6, 0)
#else
#define USE_SDL_MIXER_LOOPING 0
#endif // defined(SDL_MIXER_VERSION_ATLEAST)
#endif // !defined(USE_SDL_MIXER_LOOPING)

#if !USE_SDL_MIXER_LOOPING
#define FLAC_HEADER "fLaC"
#define OGG_HEADER "OggS"

// Looping Vorbis metadata tag names. These have been defined by ZDoom
// for specifying the start and end positions for looping music tracks
// in .ogg and .flac files.
// More information is here: http://zdoom.org/wiki/Audio_loop
#define LOOP_START_TAG "LOOP_START"
#define LOOP_END_TAG   "LOOP_END"

// FLAC metadata headers that we care about.
#define FLAC_STREAMINFO      0
#define FLAC_VORBIS_COMMENT  4

// Ogg metadata headers that we care about.
#define OGG_ID_HEADER        1
#define OGG_COMMENT_HEADER   3
#endif // !USE_SDL_MIXER_LOOPING

// 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
{
    const char *hash_prefix;
    const char *filename;
} subst_music_t;

#if !USE_SDL_MIXER_LOOPING
// Structure containing parsed metadata read from a digital music track:
typedef struct
{
    boolean valid;
    unsigned int samplerate_hz;
    int start_time, end_time;
} file_metadata_t;
#endif // !USE_SDL_MIXER_LOOPING

static subst_music_t *subst_music = NULL;
static unsigned int subst_music_len = 0;

static boolean music_initialized = false;

// If this is true, this module initialized SDL sound and has the 
// responsibility to shut it down

static boolean sdl_was_initialized = false;

#if !USE_SDL_MIXER_LOOPING
// If true, we are playing a substitute digital track rather than in-WAD
// MIDI/MUS track, and file_metadata contains loop metadata.
static file_metadata_t file_metadata;

// Position (in samples) that we have reached in the current track.
// This is updated by the TrackPositionCallback function.
static unsigned int current_track_pos;
#endif // !USE_SDL_MIXER_LOOPING

// Currently playing music track.
static Mix_Music *current_track_music = NULL;

// If true, the currently playing track is being played on loop.
static boolean current_track_loop;

// Table of known hashes and filenames to look up for them. This allows
// users to drop in a set of files without having to also provide a
// configuration file.
static const subst_music_t known_filenames[] = {
    // Doom 1 music files.
    {"b2e05b4e8dff8d76f8f4", "d_inter.{ext}"},
    {"0c0acce45130bab935d2", "d_intro.{ext}"},
    {"fca4086939a68ae4ed84", "d_victor.{ext}"},
    {"5971e5e20554f47ca065", "d_intro.{ext}"},
    {"99767e32769229897f77", "d_e1m1.{ext}"},
    {"b5e7dfb4efe9e688bf2a", "d_e1m2.{ext}"},
    {"fda8fa73e4d30a6b961c", "d_e1m3.{ext}"},
    {"3805f9bf3f1702f7e7f5", "d_e1m4.{ext}"},
    {"f546ed823b234fe39165", "d_e1m5.{ext}"},
    {"4450811b5a6748cfd83e", "d_e1m6.{ext}"},
    {"73edb50d96b0ac03be34", "d_e1m7.{ext}"},
    {"47d711a6fd32f5047879", "d_e1m8.{ext}"},
    {"62c631c2fdaa5ecd9a8d", "d_e1m9.{ext}"},
    {"7702a6449585428e7185", "d_e2m1.{ext}"},
    {"1cb1810989cbfae2b29b", "d_e2m2.{ext}"},
    {"7d740f3c881a22945e47", "d_e2m4.{ext}"},
    {"ae9c3dc2f9aeea002327", "d_e2m6.{ext}"},
    {"b26aad3caa420e9a2c76", "d_e2m7.{ext}"},
    {"90f06251a2a90bfaefd4", "d_e2m8.{ext}"},
    {"b2fb439f23c08c8e2577", "d_e3m1.{ext}"},
    {"b6c07bb249526b864208", "d_e3m2.{ext}"},
    {"ce3587ee503ffe707b2d", "d_e3m3.{ext}"},
    {"d746ea2aa16b3237422c", "d_e3m8.{ext}"},
    {"3da3b1335560a92912e6", "d_bunny.{ext}"},

    // Duplicates that don't have identical hashes:
    {"4a5badc4f10a7d4ed021", "d_inter.{ext}"},  // E2M3
    {"36b14bf165b3fdd3958e", "d_e1m7.{ext}"},   // E3M5
    {"e77c3d42f2ea87f04607", "d_e1m6.{ext}"},   // E3M6
    {"3d85ec9c10b5ea465568", "d_e2m7.{ext}"},   // E3M7
    {"4d42e2ce1c1ff192500e", "d_e1m9.{ext}"},   // E3M9

    // These tracks are reused in Alien Vendetta, but are MIDs:
    {"a05e45f67e1b64733fe3", "d_e2m1.{ext}"},   // MAP02
    {"8024ae1616ddd97ce330", "d_e1m4.{ext}"},   // MAP03
    {"3af8d79ddba49edaf9eb", "d_victor.{ext}"}, // MAP05
    {"a55352c96c025b6bd08a", "d_inter.{ext}"},  // MAP07
    {"76d1fc25ab7b1b4a58d6", "d_e1m8.{ext}"},   // MAP11
    {"497777f0863eca7cea87", "d_e1m2.{ext}"},   // MAP12
    {"0228fd87f8762f112fb6", "d_e2m2.{ext}"},   // MAP13
    {"db94e8e1d7c02092eab5", "d_e1m6.{ext}"},   // MAP14
    {"5a8d7a307eebc952795c", "d_e2m7.{ext}"},   // MAP16
    {"1a36b692bf26d94a72cc", "d_e1m7.{ext}"},   // MAP23
    {"37c6cefa351b06995152", "d_e1m5.{ext}"},   // MAP27
    {"36b97b87fe98348d44b6", "d_e2m6.{ext}"},   // MAP28

    // Doom II music files.
    {"79080e9681a2d7bec3fb", "d_runnin.{ext}"},  // MAP01,15
    {"868b3aae73c7b12e92c0", "d_stalks.{ext}"},  // MAP02,11,17
    {"19237754d2eb85f41d84", "d_countd.{ext}"},  // MAP03,21
    {"00abff3b61b25a6855d2", "d_betwee.{ext}"},  // MAP04
    {"954636c7ee09edf5d98f", "d_doom.{ext}"},    // MAP05,13
    {"8d32b2b7aa3b806474c1", "d_the_da.{ext}"},  // MAP06,12,24
    {"41efc3c84bb321af2b6b", "d_shawn.{ext}"},   // MAP07,19,29
    // Assuming single D_DDTBLU: http://doomwiki.org/wiki/Doom_II_music#Trivia
    {"51c0872fec9f43259318", "d_ddtblu.{ext}"},  // MAP08
    {"acb7ad85494d18235df8", "d_ddtblu.{ext}"},  // MAP14,22
    {"4b7ceccbf47e78e2fa0b", "d_in_cit.{ext}"},  // MAP09
    {"1d1f4a9edba174584e11", "d_dead.{ext}"},    // MAP10,16
    {"1736c81aac77f9bffd3d", "d_romero.{ext}"},  // MAP18,27
    {"a55d400570ad255a576b", "d_messag.{ext}"},  // MAP20,26
    {"29d30c3fbd712016f2e5", "d_ampie.{ext}"},   // MAP23
    {"bcfe9786afdcfb704afa", "d_adrian.{ext}"},  // MAP25
    {"e05c10389e71836834ae", "d_tense.{ext}"},   // MAP28
    {"b779022b1d0f0010b8f0", "d_openin.{ext}"},  // MAP30
    {"a9a5f7b0ab3be0f4fc24", "d_evil.{ext}"},    // MAP31
    {"4503d155aafec0296689", "d_ultima.{ext}"},  // MAP32
    {"56f2363f01df38908c77", "d_dm2ttl.{ext}"},
    {"71e58baf9e9dea4dd24a", "d_dm2int.{ext}"},
    {"e632318629869811f7dc", "d_read_m.{ext}"},

    // Duplicate filenames: the above filenames are the "canonical" files
    // for the given SHA1 hashes, but we can also look for these filenames
    // corresponding to the duplicated music tracks too.
    {"868b3aae73c7b12e92c0", "d_stlks2.{ext}"},
    {"868b3aae73c7b12e92c0", "d_stlks3.{ext}"},
    {"8d32b2b7aa3b806474c1", "d_theda2.{ext}"},
    {"8d32b2b7aa3b806474c1", "d_theda3.{ext}"},
    {"954636c7ee09edf5d98f", "d_doom2.{ext}"},
    {"acb7ad85494d18235df8", "d_ddtbl2.{ext}"},
    {"acb7ad85494d18235df8", "d_ddtbl3.{ext}"},
    {"79080e9681a2d7bec3fb", "d_runni2.{ext}"},
    {"1d1f4a9edba174584e11", "d_dead2.{ext}"},
    {"41efc3c84bb321af2b6b", "d_shawn2.{ext}"},
    {"41efc3c84bb321af2b6b", "d_shawn3.{ext}"},
    {"19237754d2eb85f41d84", "d_count2.{ext}"},
    {"a55d400570ad255a576b", "d_messg2.{ext}"},
    {"1736c81aac77f9bffd3d", "d_romer2.{ext}"},

    // These tracks are reused in Alien Vendetta, but are MIDs:
    {"9433604c098b7b1119a4", "d_in_cit.{ext}"},  // MAP26

    // Heretic tracks.
    {"12818ca0d3c957e7d57e", "mus_titl.{ext}"},
    {"5cb988538ce1b1857349", "mus_intr.{ext}"},
    {"6f126abe35a78b61b930", "mus_cptd.{ext}"},
    {"62557250f0427c067dc9", "mus_e1m1.{ext}"},
    {"1e8d5fd814490b9ae166", "mus_e1m2.{ext}"},
    {"f0f31e8834e85035d434", "mus_e1m3.{ext}"},
    {"054d6997405cc5a32b46", "mus_e1m4.{ext}"},
    {"31950ab062cc1e5ca49d", "mus_e1m5.{ext}"},
    {"7389024fbab0dff47211", "mus_e1m6.{ext}"},
    {"f2aa312dddd0a294a095", "mus_e1m7.{ext}"},
    {"cd6856731d1ae1f3aa4e", "mus_e1m8.{ext}"},
    {"d7fe793f266733d92e61", "mus_e1m9.{ext}"},
    {"933545b48fad8c66f042", "mus_e2m1.{ext}"},
    {"bf88ecd4ae1621222592", "mus_e2m2.{ext}"},
    {"4f619f87a828c2ca4801", "mus_e2m3.{ext}"},
    {"13033a83c49424b2f2ab", "mus_e2m4.{ext}"},
    {"b3851f9351ae411d9de3", "mus_e2m6.{ext}"},
    {"82539791159fbbc02a23", "mus_e2m7.{ext}"},
    {"fd9e53a49cfa62c463a0", "mus_e2m8.{ext}"},
    {"29503959324d2ca67958", "mus_e2m9.{ext}"},
    {"3aa632257c5be375b97b", "mus_e3m2.{ext}"},
    {"69ba0dce7913d53b67a8", "mus_e3m3.{ext}"},

    // These Heretic tracks are reused in Alien Vendetta, but are MIDs:
    {"51344131e8d260753ce7", "mus_e2m3.{ext}"},  // MAP15
    {"78b570b2397570440aff", "mus_e1m1.{ext}"},  // MAP19
    {"ee21ba9fad4de3dfaef0", "mus_e1m4.{ext}"},  // MAP29
    {"d2bb643a60696ccbca03", "mus_e1m9.{ext}"},  // MAP32

    // Hexen tracks:
    {"fbf55fc1ee26bd01266b", "winnowr.{ext}"},
    {"71776e2da2b7ba607d81", "jachr.{ext}"},
    {"c5c8630608b8132b33cd", "simonr.{ext}"},
    {"43683b3f55a031de88d4", "wutzitr.{ext}"},
    {"a6062883f29436ef73db", "falconr.{ext}"},
    {"512cb6cc9b558d5f0fef", "levelr.{ext}"},
    {"d31226ae75fce6a24208", "chartr.{ext}"},
    {"bf1f1e561bbdba4e699f", "swampr.{ext}"},
    {"b303193f756ca0e2de0f", "deepr.{ext}"},
    {"f0635f0386d883b00186", "fubasr.{ext}"},
    {"18f2a01f83df6e3abedc", "grover.{ext}"},
    {"b2527eb0522f08b2cf5f", "fortr.{ext}"},
    {"343addba8ba53a20a160", "foojar.{ext}"},
    {"c13109045b06b5a63386", "sixater.{ext}"},
    {"693525aaf69eac5429ab", "wobabyr.{ext}"},
    {"8f884223811c2bb8311d", "cryptr.{ext}"},
    {"de540e6826e62b32c01c", "fantar.{ext}"},
    {"efdff548df918934f71f", "blechr.{ext}"},
    {"de91f150f6a127e72e35", "voidr.{ext}"},
    {"e0497fe27289fe18515b", "chap_1r.{ext}"},
    {"f2ef1abdc3f672a3519a", "chap_2r.{ext}"},
    {"78cd9882f61cc441bef4", "chap_3r.{ext}"},
    {"97b2b575d9d096c1f89f", "chap_4r.{ext}"},
    {"ad0197a0f6c52ac30915", "chippyr.{ext}"},
    {"30506c62e9f0989ffe09", "percr.{ext}"},
    {"3542803beaa43bf1de1a", "secretr.{ext}"},
    {"81067721f40c611d09fb", "bonesr.{ext}"},
    {"4822af2e1a2eb7faf660", "octor.{ext}"},
    {"26bb3cec902ed8008fc2", "rithmr.{ext}"},
    {"94ab641c7aa93caac77a", "stalkr.{ext}"},
    {"d0a3f337c54b0703b4d3", "borkr.{ext}"},
    {"79e7781ec7eb9b9434b5", "crucibr.{ext}"},
    {"c2786e5581a7f8801969", "hexen.{ext}"},
    {"97fae9a084c0efda5151", "hub.{ext}"},
    {"c5da52d5c2ec4803ef8f", "hall.{ext}"},
    {"1e71bc0e2feafb06214e", "orb.{ext}"},
    {"bc9dcfa6632e847e03af", "chess.{ext}"},

    // Hexen CD tracks: alternate filenames for a ripped copy of
    // the CD soundtrack.
    {"71776e2da2b7ba607d81", "hexen02.{ext}"},   // level  2 (jachr)
    {"efdff548df918934f71f", "hexen03.{ext}"},   // level 26 (blechr)
    {"c2786e5581a7f8801969", "hexen04.{ext}"},   // (hexen)
    {"1e71bc0e2feafb06214e", "hexen05.{ext}"},   // (orb)
    {"f0635f0386d883b00186", "hexen06.{ext}"},   // level 10 (fubasr)
    {"bc9dcfa6632e847e03af", "hexen07.{ext}"},   // (chess)
    {"8f884223811c2bb8311d", "hexen08.{ext}"},   // level 24 (cryptr)
    {"a6062883f29436ef73db", "hexen09.{ext}"},   // level  5 (falconr)
    {"4822af2e1a2eb7faf660", "hexen10.{ext}"},   // level 36 (octor)
    {"26bb3cec902ed8008fc2", "hexen11.{ext}"},   // level 37 (rithmr)
    {"c13109045b06b5a63386", "hexen12.{ext}"},   // level 22 (sixater)
    {"fbf55fc1ee26bd01266b", "hexen13.{ext}"},   // level  1 (winnowr)
    {"bf1f1e561bbdba4e699f", "hexen14.{ext}"},   // level  8 (swampr)
    {"43683b3f55a031de88d4", "hexen15.{ext}"},   // level  4 (wutzitr)
    {"81067721f40c611d09fb", "hexen16.{ext}"},   // level 35 (bonesr)
    {"e0497fe27289fe18515b", "hexen17.{ext}"},   // level 28 (chap_1r)
    {"97b2b575d9d096c1f89f", "hexen18.{ext}"},   // level 31 (chap_4r)
    {"de540e6826e62b32c01c", "hexen19.{ext}"},   // level 25 (fantar)
    {"343addba8ba53a20a160", "hexen20.{ext}"},   // level 21 (foojar)
    {"512cb6cc9b558d5f0fef", "hexen21.{ext}"},   // level  6 (levelr)
    {"c5c8630608b8132b33cd", "hexen22.{ext}"},   // level  3 (simonr)

    // Strife:
    {"8ac2b2b47707f0fdf8f6", "d_logo.{ext}"},   // Title
    {"62e1c58054a1f1bc39b2", "d_action.{ext}"}, // 1,15,28
    {"12fa000f3fa1edac5c4f", "d_tavern.{ext}"}, // 2
    {"695e56ab3251792d20e5", "d_danger.{ext}"}, // 3,11
    {"96fe30e8712217b60dd7", "d_fast.{ext}"},   // 4
    {"61345598a3de04aad508", "d_darker.{ext}"}, // 6,14
    {"52353e9a435b7b1cb268", "d_strike.{ext}"}, // 7,19
    {"061164504907bffc9c22", "d_slide.{ext}"},  // 8,18,22
    {"3dbb4b703ce69aafcdd5", "d_tribal.{ext}"}, // 9
    {"393773688eba050c3548", "d_march.{ext}"},  // 10
    {"3cba3c627de065a667dd", "d_mood.{ext}"},   // 12
    {"b1f65a333e5c70255784", "d_castle.{ext}"}, // 13
    {"e1455a83a04c9ac4a09f", "d_fight.{ext}"},  // 16,31
    {"17f822b7374b1f069b89", "d_spense.{ext}"}, // 17
    {"e66c5a1a7d05f021f4ae", "d_dark.{ext}"},   // 20
    {"1c92bd0625026af30dad", "d_tech.{ext}"},   // 21,27
    {"7ae280713d078de7933a", "d_drone.{ext}"},  // 23,30
    {"4a664afd0d7eae79c97a", "d_panthr.{ext}"}, // 24
    {"4a7d62beeac5601ccf21", "d_sad.{ext}"},    // 25
    {"e60e109779400f2855d7", "d_instry.{ext}"}, // 26,29
    {"b7d36878faeb291d6df5", "d_happy.{ext}"},  // Better ending
    {"ff4a342c8c5ec51b06c3", "d_end.{ext}"},    // Worse ending
    // This conflicts with Doom's d_intro:
    //{"ec8fa484c4e85adbf700", "d_intro.{ext}"},  // 5
};

#if !USE_SDL_MIXER_LOOPING
// Given a time string (for LOOP_START/LOOP_END), parse it and return
// the time (in # samples since start of track) it represents.
static unsigned int ParseVorbisTime(unsigned int samplerate_hz, char *value)
{
    char *num_start, *p;
    unsigned int result = 0;
    char c;

    if (strchr(value, ':') == NULL)
    {
	return atoi(value);
    }

    result = 0;
    num_start = value;

    for (p = value; *p != '\0'; ++p)
    {
        if (*p == '.' || *p == ':')
        {
            c = *p; *p = '\0';
            result = result * 60 + atoi(num_start);
            num_start = p + 1;
            *p = c;
        }

        if (*p == '.')
        {
            return result * samplerate_hz
	         + (unsigned int) (atof(p) * samplerate_hz);
        }
    }

    return (result * 60 + atoi(num_start)) * samplerate_hz;
}

// Given a vorbis comment string (eg. "LOOP_START=12345"), set fields
// in the metadata structure as appropriate.
static void ParseVorbisComment(file_metadata_t *metadata, char *comment)
{
    char *eq, *key, *value;

    eq = strchr(comment, '=');

    if (eq == NULL)
    {
        return;
    }

    key = comment;
    *eq = '\0';
    value = eq + 1;

    if (!strcmp(key, LOOP_START_TAG))
    {
        metadata->start_time = ParseVorbisTime(metadata->samplerate_hz, value);
    }
    else if (!strcmp(key, LOOP_END_TAG))
    {
        metadata->end_time = ParseVorbisTime(metadata->samplerate_hz, value);
    }
}

// Parse a vorbis comments structure, reading from the given file.
static void ParseVorbisComments(file_metadata_t *metadata, FILE *fs)
{
    uint32_t buf;
    unsigned int num_comments, i, comment_len;
    char *comment;

    // We must have read the sample rate already from an earlier header.
    if (metadata->samplerate_hz == 0)
    {
	return;
    }

    // Skip the starting part we don't care about.
    if (fread(&buf, 4, 1, fs) < 1)
    {
        return;
    }
    if (fseek(fs, LONG(buf), SEEK_CUR) != 0)
    {
	return;
    }

    // Read count field for number of comments.
    if (fread(&buf, 4, 1, fs) < 1)
    {
        return;
    }
    num_comments = LONG(buf);

    // Read each individual comment.
    for (i = 0; i < num_comments; ++i)
    {
        // Read length of comment.
        if (fread(&buf, 4, 1, fs) < 1)
	{
            return;
	}

        comment_len = LONG(buf);

        // Read actual comment data into string buffer.
        comment = calloc(1, comment_len + 1);
        if (comment == NULL
         || fread(comment, 1, comment_len, fs) < comment_len)
        {
            free(comment);
            break;
        }

        // Parse comment string.
        ParseVorbisComment(metadata, comment);
        free(comment);
    }
}

static void ParseFlacStreaminfo(file_metadata_t *metadata, FILE *fs)
{
    byte buf[34];

    // Read block data.
    if (fread(buf, sizeof(buf), 1, fs) < 1)
    {
        return;
    }

    // We only care about sample rate and song length.
    metadata->samplerate_hz = (buf[10] << 12) | (buf[11] << 4)
                            | (buf[12] >> 4);
    // Song length is actually a 36 bit field, but 32 bits should be
    // enough for everybody.
    //metadata->song_length = (buf[14] << 24) | (buf[15] << 16)
    //                      | (buf[16] << 8) | buf[17];
}

static void ParseFlacFile(file_metadata_t *metadata, FILE *fs)
{
    byte header[4];
    unsigned int block_type;
    size_t block_len;
    boolean last_block;

    for (;;)
    {
        long pos = -1;

        // Read METADATA_BLOCK_HEADER:
        if (fread(header, 4, 1, fs) < 1)
        {
            return;
        }

        block_type = header[0] & ~0x80;
        last_block = (header[0] & 0x80) != 0;
        block_len = (header[1] << 16) | (header[2] << 8) | header[3];

        pos = ftell(fs);
        if (pos < 0)
        {
            return;
        }

        if (block_type == FLAC_STREAMINFO)
        {
            ParseFlacStreaminfo(metadata, fs);
        }
        else if (block_type == FLAC_VORBIS_COMMENT)
        {
            ParseVorbisComments(metadata, fs);
        }

        if (last_block)
        {
            break;
        }

        // Seek to start of next block.
        if (fseek(fs, pos + block_len, SEEK_SET) != 0)
        {
            return;
        }
    }
}

static void ParseOggIdHeader(file_metadata_t *metadata, FILE *fs)
{
    byte buf[21];

    if (fread(buf, sizeof(buf), 1, fs) < 1)
    {
        return;
    }

    metadata->samplerate_hz = (buf[8] << 24) | (buf[7] << 16)
                            | (buf[6] << 8) | buf[5];
}

static void ParseOggFile(file_metadata_t *metadata, FILE *fs)
{
    byte buf[7];
    unsigned int offset;

    // Scan through the start of the file looking for headers. They
    // begin '[byte]vorbis' where the byte value indicates header type.
    memset(buf, 0, sizeof(buf));

    for (offset = 0; offset < 100 * 1024; ++offset)
    {
	// buf[] is used as a sliding window. Each iteration, we
	// move the buffer one byte to the left and read an extra
	// byte onto the end.
        memmove(buf, buf + 1, sizeof(buf) - 1);

        if (fread(&buf[6], 1, 1, fs) < 1)
        {
            return;
        }

        if (!memcmp(buf + 1, "vorbis", 6))
        {
            switch (buf[0])
            {
                case OGG_ID_HEADER:
                    ParseOggIdHeader(metadata, fs);
                    break;
                case OGG_COMMENT_HEADER:
		    ParseVorbisComments(metadata, fs);
                    break;
                default:
                    break;
            }
        }
    }
}

static void ReadLoopPoints(const char *filename, file_metadata_t *metadata)
{
    FILE *fs;
    char header[4];

    metadata->valid = false;
    metadata->samplerate_hz = 0;
    metadata->start_time = 0;
    metadata->end_time = -1;

    fs = M_fopen(filename, "rb");

    if (fs == NULL)
    {
        return;
    }

    // Check for a recognized file format; use the first four bytes
    // of the file.

    if (fread(header, 4, 1, fs) < 1)
    {
        fclose(fs);
        return;
    }

    if (memcmp(header, FLAC_HEADER, 4) == 0)
    {
        ParseFlacFile(metadata, fs);
    }
    else if (memcmp(header, OGG_HEADER, 4) == 0)
    {
        ParseOggFile(metadata, fs);
    }

    fclose(fs);

    // Only valid if at the very least we read the sample rate.
    metadata->valid = metadata->samplerate_hz > 0;

    // If start and end time are both zero, ignore the loop tags.
    // This is consistent with other source ports.
    if (metadata->start_time == 0 && metadata->end_time == 0)
    {
        metadata->valid = false;
    }
}
#endif // !USE_SDL_MIXER_LOOPING

// Given a MUS lump, look up a substitute MUS file to play instead
// (or NULL to just use normal MIDI playback).

static const char *GetSubstituteMusicFile(void *data, size_t data_len)
{
    sha1_context_t context;
    sha1_digest_t hash;
    const char *filename;
    char hash_str[sizeof(sha1_digest_t) * 2 + 1];
    unsigned 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);

    // Build a string representation of the hash.
    for (i = 0; i < sizeof(sha1_digest_t); ++i)
    {
        M_snprintf(hash_str + i * 2, sizeof(hash_str) - i * 2,
                   "%02x", hash[i]);
    }

    // Look for a hash that matches.
    // The substitute mapping list can (intentionally) contain multiple
    // filename mappings for the same hash. This allows us to try
    // different files and fall back if our first choice isn't found.

    filename = NULL;

    for (i = 0; i < subst_music_len; ++i)
    {
        if (M_StringStartsWith(hash_str, subst_music[i].hash_prefix))
        {
            filename = subst_music[i].filename;

            // If the file exists, then use this file in preference to
            // any fallbacks. But we always return a filename if it's
            // in the list, even if it's just so we can print an error
            // message to the user saying it doesn't exist.
            if (M_FileExists(filename))
            {
                break;
            }
        }
    }

    return filename;
}

static char *GetFullPath(const char *musicdir, const char *path)
{
    char *result;
    char *systemized_path;

    // Starting with directory separator means we have an absolute path,
    // so just return it.
    if (path[0] == DIR_SEPARATOR)
    {
        return M_StringDuplicate(path);
    }

#ifdef _WIN32
    // d:\path\...
    if (isalpha(path[0]) && path[1] == ':' && path[2] == DIR_SEPARATOR)
    {
        return M_StringDuplicate(path);
    }
#endif

    // Paths in the substitute filenames can contain Unix-style /
    // path separators, but we should convert this to the separator
    // for the native platform.
    systemized_path = M_StringReplace(path, "/", DIR_SEPARATOR_S);

    // Copy config filename and cut off the filename to just get the
    // parent dir.
    result = M_StringJoin(musicdir, systemized_path, NULL);
    free(systemized_path);

    return result;
}

// If filename ends with .{ext}, check if a .ogg, .flac or .mp3 exists with
// that name, returning it if found. If none exist, NULL is returned. If the
// filename doesn't end with .{ext} then it just acts as a wrapper around
// GetFullPath().
static char *ExpandFileExtension(const char *musicdir, const char *filename)
{
    static const char *extns[] = {".flac", ".ogg", ".mp3"};
    char *replaced, *result;
    int i;

    if (!M_StringEndsWith(filename, ".{ext}"))
    {
        return GetFullPath(musicdir, filename);
    }

    for (i = 0; i < arrlen(extns); ++i)
    {
        replaced = M_StringReplace(filename, ".{ext}", extns[i]);
        result = GetFullPath(musicdir, replaced);
        free(replaced);
        if (M_FileExists(result))
        {
            return result;
        }
        free(result);
    }

    return NULL;
}

// Add a substitute music file to the lookup list.
static void AddSubstituteMusic(const char *musicdir, const char *hash_prefix,
                               const char *filename)
{
    subst_music_t *s;
    char *path;

    path = ExpandFileExtension(musicdir, filename);
    if (path == NULL)
    {
        return;
    }

    ++subst_music_len;
    subst_music =
        I_Realloc(subst_music, sizeof(subst_music_t) * subst_music_len);
    s = &subst_music[subst_music_len - 1];
    s->hash_prefix = hash_prefix;
    s->filename = path;
}

static const char *ReadHashPrefix(char *line)
{
    char *result;
    char *p;
    int i, len;

    for (p = line; *p != '\0' && !isspace(*p) && *p != '='; ++p)
    {
        if (!isxdigit(*p))
        {
            return NULL;
        }
    }

    len = p - line;
    if (len == 0 || len > sizeof(sha1_digest_t) * 2)
    {
        return NULL;
    }

    result = malloc(len + 1);
    if (result == NULL)
    {
        return NULL;
    }

    for (i = 0; i < len; ++i)
    {
        result[i] = tolower(line[i]);
    }
    result[len] = '\0';

    return result;
}

// Parse a line from substitute music configuration file; returns error
// message or NULL for no error.

static const char *ParseSubstituteLine(char *musicdir, char *line)
{
    const char *hash_prefix;
    char *filename;
    char *p;

    // Strip out comments if present.
    p = strchr(line, '#');
    if (p != NULL)
    {
        while (p > line && isspace(*(p - 1)))
        {
            --p;
        }
        *p = '\0';
    }

    // Skip leading spaces.
    for (p = line; *p != '\0' && isspace(*p); ++p);

    // Empty line? This includes comment lines now that comments have
    // been stripped.
    if (*p == '\0')
    {
        return NULL;
    }

    hash_prefix = ReadHashPrefix(p);
    if (hash_prefix == NULL)
    {
        return "Invalid hash prefix";
    }

    p += strlen(hash_prefix);

    // Skip spaces.
    for (; *p != '\0' && isspace(*p); ++p);

    if (*p != '=')
    {
        return "Expected '='";
    }

    ++p;

    // Skip spaces.
    for (; *p != '\0' && isspace(*p); ++p);

    filename = 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.
    AddSubstituteMusic(musicdir, hash_prefix, filename);

    return NULL;
}

// Read a substitute music configuration file.

static boolean ReadSubstituteConfig(char *musicdir, const char *filename)
{
    char *buffer;
    char *line;
    int linenum = 1;

    // This unnecessarily opens the file twice...
    if (!M_FileExists(filename))
    {
        return false;
    }

    M_ReadFile(filename, (byte **) &buffer);

    line = buffer;

    while (line != NULL)
    {
        const char *error;
        char *next;

        // find end of line
        char *eol = strchr(line, '\n');
        if (eol != NULL)
        {
            // change the newline into NUL
            *eol = '\0';
            next = eol + 1;
        }
        else
        {
            // end of buffer
            next = NULL;
        }

        error = ParseSubstituteLine(musicdir, line);

        if (error != NULL)
        {
            fprintf(stderr, "%s:%i: Error: %s\n", filename, linenum, error);
        }

        ++linenum;
        line = next;
    }

    Z_Free(buffer);

    return true;
}

// Find substitute configs and try to load them.

static void LoadSubstituteConfigs(void)
{
    glob_t *glob;
    char *musicdir;
    const char *path;
    unsigned int old_music_len;
    unsigned int i;

    // We can configure the path to music packs using the music_pack_path
    // configuration variable. Otherwise we use the current directory, or
    // $configdir/music to look for .cfg files.
    if (strcmp(music_pack_path, "") != 0)
    {
        musicdir = M_StringJoin(music_pack_path, DIR_SEPARATOR_S, NULL);
    }
    else if (!strcmp(configdir, exedir))
    {
        musicdir = M_StringDuplicate("");
    }
    else
    {
        musicdir = M_StringJoin(configdir, "music", DIR_SEPARATOR_S, NULL);
    }

    // Load all music packs, by searching for .cfg files.
    glob = I_StartGlob(musicdir, "*.cfg", GLOB_FLAG_SORTED|GLOB_FLAG_NOCASE);
    for (;;)
    {
        path = I_NextGlob(glob);
        if (path == NULL)
        {
            break;
        }
        ReadSubstituteConfig(musicdir, path);
    }
    I_EndGlob(glob);

    if (subst_music_len > 0)
    {
        printf("Loaded %u music substitutions from config files.\n",
               subst_music_len);
    }

    old_music_len = subst_music_len;

    // Add entries from known filenames list. We add this after those from the
    // configuration files, so that the entries here can be overridden.
    for (i = 0; i < arrlen(known_filenames); ++i)
    {
        AddSubstituteMusic(musicdir, known_filenames[i].hash_prefix,
                           known_filenames[i].filename);
    }

    if (subst_music_len > old_music_len)
    {
        printf("Configured %u music substitutions based on filename.\n",
               subst_music_len - old_music_len);
    }

    free(musicdir);
}

// Returns true if the given lump number is a music lump that should
// be included in substitute configs.
// Identifying music lumps by name is not feasible; some games (eg.
// Heretic, Hexen) don't have a common naming pattern for music lumps.

static boolean IsMusicLump(int lumpnum)
{
    byte *data;
    boolean result;

    if (W_LumpLength(lumpnum) < 4)
    {
        return false;
    }

    data = W_CacheLumpNum(lumpnum, PU_STATIC);

    result = memcmp(data, MUS_HEADER_MAGIC, 4) == 0
          || memcmp(data, MID_HEADER_MAGIC, 4) == 0;

    W_ReleaseLumpNum(lumpnum);

    return result;
}

// Dump an example config file containing checksums for all MIDI music
// found in the WAD directory.

static void DumpSubstituteConfig(const char *filename)
{
    sha1_context_t context;
    sha1_digest_t digest;
    char name[9];
    byte *data;
    FILE *fs;
    unsigned int lumpnum;
    size_t h;

    fs = M_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);
    fprintf(fs, "# SHA1 hash                              = filename\n");

    for (lumpnum = 0; lumpnum < numlumps; ++lumpnum)
    {
        strncpy(name, lumpinfo[lumpnum]->name, 8);
        name[8] = '\0';

        if (!IsMusicLump(lumpnum))
        {
            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.ogg\n", name);
    }

    fprintf(fs, "\n");
    fclose(fs);

    printf("Substitute MIDI config file written to %s.\n", filename);
    I_Quit();
}

// Shutdown music

static void I_MP_ShutdownMusic(void)
{
    if (music_initialized)
    {
        Mix_HaltMusic();
        music_initialized = false;

        if (sdl_was_initialized)
        {
            Mix_CloseAudio();
            SDL_QuitSubSystem(SDL_INIT_AUDIO);
            sdl_was_initialized = false;
        }
    }
}

static boolean SDLIsInitialized(void)
{
    int freq, channels;
    Uint16 format;

    return Mix_QuerySpec(&freq, &format, &channels) != 0;
}

#if !USE_SDL_MIXER_LOOPING
// Callback function that is invoked to track current track position.
void TrackPositionCallback(int chan, void *stream, int len, void *udata)
{
    // Position is doubled up twice: for 16-bit samples and for stereo.
    current_track_pos += len / 4;
}
#endif // !USE_SDL_MIXER_LOOPING

// Initialize music subsystem
static boolean I_MP_InitMusic(void)
{
    int i;

    //!
    // @category obscure
    // @arg <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 we're in GENMIDI mode, try to load sound packs.
    LoadSubstituteConfigs();

    // We can't initialize if we don't have any substitute files to work with.
    // If so, don't bother with SDL initialization etc.
    if (subst_music_len == 0)
    {
        return false;
    }

    // If SDL_mixer is not initialized, we have to initialize it
    // and have the responsibility to shut it down later on.
    if (SDLIsInitialized())
    {
        music_initialized = true;
    }
    else if (SDL_Init(SDL_INIT_AUDIO) < 0)
    {
        fprintf(stderr, "Unable to set up sound.\n");
    }
    else if (Mix_OpenAudioDevice(snd_samplerate, AUDIO_S16SYS, 2, 1024, NULL, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE) < 0)
    {
        fprintf(stderr, "Error initializing SDL_mixer: %s\n",
                Mix_GetError());
        SDL_QuitSubSystem(SDL_INIT_AUDIO);
    }
    else
    {
        SDL_PauseAudio(0);

        sdl_was_initialized = true;
        music_initialized = true;
    }

    // Initialize SDL_Mixer for digital music playback
    Mix_Init(MIX_INIT_FLAC | MIX_INIT_OGG | MIX_INIT_MP3);

#if !USE_SDL_MIXER_LOOPING
    // Register an effect function to track the music position.
    Mix_RegisterEffect(MIX_CHANNEL_POST, TrackPositionCallback, NULL, NULL);
#endif // !USE_SDL_MIXER_LOOPING

    return music_initialized;
}

// Set music volume (0 - 127)

static void I_MP_SetMusicVolume(int volume)
{
    Mix_VolumeMusic((volume * MIX_MAX_VOLUME) / 127);
}

// Start playing a mid

static void I_MP_PlaySong(void *handle, boolean looping)
{
    int loops;

    if (!music_initialized)
    {
        return;
    }

    if (handle == NULL)
    {
        return;
    }

    current_track_music = (Mix_Music *) handle;
    current_track_loop = looping;

    if (looping)
    {
        loops = -1;
    }
    else
    {
        loops = 1;
    }

#if !USE_SDL_MIXER_LOOPING
    // Don't loop when playing substitute music, as we do it
    // ourselves instead.
    if (file_metadata.valid)
    {
        loops = 1;
        SDL_LockAudio();
        current_track_pos = 0;  // start of track
        SDL_UnlockAudio();
    }
#endif // !USE_SDL_MIXER_LOOPING

    if (Mix_PlayMusic(current_track_music, loops) == -1)
    {
        fprintf(stderr, "I_MP_PlaySong: Error starting track: %s\n",
                Mix_GetError());
    }
}

static void I_MP_PauseSong(void)
{
    if (!music_initialized)
    {
        return;
    }

    Mix_PauseMusic();
}

static void I_MP_ResumeSong(void)
{
    if (!music_initialized)
    {
        return;
    }

    Mix_ResumeMusic();
}

static void I_MP_StopSong(void)
{
    if (!music_initialized)
    {
        return;
    }

    Mix_HaltMusic();
    current_track_music = NULL;
}

static void I_MP_UnRegisterSong(void *handle)
{
    Mix_Music *music = (Mix_Music *) handle;

    if (!music_initialized)
    {
        return;
    }

    if (handle == NULL)
    {
        return;
    }

    Mix_FreeMusic(music);
}

static void *I_MP_RegisterSong(void *data, int len)
{
    const char *filename;
    Mix_Music *music;

    if (!music_initialized)
    {
        return NULL;
    }

    // See if we're substituting this MUS for a high-quality replacement.
    filename = GetSubstituteMusicFile(data, len);
    if (filename == NULL)
    {
        return 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());
        return NULL;
    }

#if !USE_SDL_MIXER_LOOPING
    // Read loop point metadata from the file so that we know where
    // to loop the music.
    ReadLoopPoints(filename, &file_metadata);
#endif // !USE_SDL_MIXER_LOOPING
    return music;
}

// Is the song playing?
static boolean I_MP_MusicIsPlaying(void)
{
    if (!music_initialized)
    {
        return false;
    }

    return Mix_PlayingMusic();
}

#if !USE_SDL_MIXER_LOOPING
// Get position in substitute music track, in seconds since start of track.
static double GetMusicPosition(void)
{
    unsigned int music_pos;
    int freq;

    Mix_QuerySpec(&freq, NULL, NULL);

    SDL_LockAudio();
    music_pos = current_track_pos;
    SDL_UnlockAudio();

    return (double) music_pos / freq;
}

static void RestartCurrentTrack(void)
{
    double start = (double) file_metadata.start_time
                 / file_metadata.samplerate_hz;

    // If the track finished we need to restart it.
    if (current_track_music != NULL)
    {
        Mix_PlayMusic(current_track_music, 1);
    }

    Mix_SetMusicPosition(start);
    SDL_LockAudio();
    current_track_pos = file_metadata.start_time;
    SDL_UnlockAudio();
}
#endif // !USE_SDL_MIXER_LOOPING

// Poll music position; if we have passed the loop point end position
// then we need to go back.
static void I_MP_PollMusic(void)
{
#if !USE_SDL_MIXER_LOOPING
    // When playing substitute tracks, loop tags only apply if we're playing
    // a looping track. Tracks like the title screen music have the loop
    // tags ignored.
    if (current_track_loop && file_metadata.valid)
    {
        double end = (double) file_metadata.end_time
                   / file_metadata.samplerate_hz;

        // If we have reached the loop end point then we have to take action.
        if (file_metadata.end_time >= 0 && GetMusicPosition() >= end)
        {
            RestartCurrentTrack();
        }

        // Have we reached the actual end of track (not loop end)?
        if (!Mix_PlayingMusic())
        {
            RestartCurrentTrack();
        }
    }
#endif // !USE_SDL_MIXER_LOOPING
}

music_module_t music_pack_module =
{
    NULL,
    0,
    I_MP_InitMusic,
    I_MP_ShutdownMusic,
    I_MP_SetMusicVolume,
    I_MP_PauseSong,
    I_MP_ResumeSong,
    I_MP_RegisterSong,
    I_MP_UnRegisterSong,
    I_MP_PlaySong,
    I_MP_StopSong,
    I_MP_MusicIsPlaying,
    I_MP_PollMusic,
};


#else // DISABLE_SDL2MIXER


static boolean I_NULL_InitMusic(void)
{
    return false;
}


static void I_NULL_ShutdownMusic(void)
{
}


static void I_NULL_SetMusicVolume(int volume)
{
}


static void I_NULL_PauseSong(void)
{
}


static void I_NULL_ResumeSong(void)
{
}


static void *I_NULL_RegisterSong(void *data, int len)
{
    return NULL;
}


static void I_NULL_UnRegisterSong(void *handle)
{
}


static void I_NULL_PlaySong(void *handle, boolean looping)
{
}


static void I_NULL_StopSong(void)
{
}


static boolean I_NULL_MusicIsPlaying(void)
{
    return false;
}


static void I_NULL_PollMusic(void)
{
}

music_module_t music_pack_module =
{
    NULL,
    0,
    I_NULL_InitMusic,
    I_NULL_ShutdownMusic,
    I_NULL_SetMusicVolume,
    I_NULL_PauseSong,
    I_NULL_ResumeSong,
    I_NULL_RegisterSong,
    I_NULL_UnRegisterSong,
    I_NULL_PlaySong,
    I_NULL_StopSong,
    I_NULL_MusicIsPlaying,
    I_NULL_PollMusic,
};


#endif // DISABLE_SDL2MIXER