diff --git a/doomgeneric/Makefile.sdl b/doomgeneric/Makefile.sdl index a2605a4..a7c24a5 100644 --- a/doomgeneric/Makefile.sdl +++ b/doomgeneric/Makefile.sdl @@ -19,14 +19,14 @@ SDL_LIBS = CC=clang # gcc or g++ CFLAGS+=-ggdb3 -Os $(INCLUDES) $(SDL_CFLAGS) LDFLAGS+=-Wl,-dead_strip -CFLAGS+=-ggdb3 -Wall -DNORMALUNIX -DLINUX -DSNDSERV # -DUSEASM -LIBS+=-lm -lc -lSDL2 +CFLAGS+=-ggdb3 -Wall -DNORMALUNIX -DLINUX -DSNDSERV -DFEATURE_SOUND # -DUSEASM +LIBS+=-lm -lc -lSDL2 -lSDL2_mixer `sdl2-config --cflags --libs` # subdirectory for objects OBJDIR=build OUTPUT=doomgeneric -SRC_DOOM = i_main.o dummy.o am_map.o doomdef.o doomstat.o dstrings.o d_event.o d_items.o d_iwad.o d_loop.o d_main.o d_mode.o d_net.o f_finale.o f_wipe.o g_game.o hu_lib.o hu_stuff.o info.o i_cdmus.o i_endoom.o i_joystick.o i_scale.o i_sound.o i_system.o i_timer.o memio.o m_argv.o m_bbox.o m_cheat.o m_config.o m_controls.o m_fixed.o m_menu.o m_misc.o m_random.o p_ceilng.o p_doors.o p_enemy.o p_floor.o p_inter.o p_lights.o p_map.o p_maputl.o p_mobj.o p_plats.o p_pspr.o p_saveg.o p_setup.o p_sight.o p_spec.o p_switch.o p_telept.o p_tick.o p_user.o r_bsp.o r_data.o r_draw.o r_main.o r_plane.o r_segs.o r_sky.o r_things.o sha1.o sounds.o statdump.o st_lib.o st_stuff.o s_sound.o tables.o v_video.o wi_stuff.o w_checksum.o w_file.o w_main.o w_wad.o z_zone.o w_file_stdc.o i_input.o i_video.o doomgeneric.o doomgeneric_sdl.o +SRC_DOOM = i_main.o dummy.o am_map.o doomdef.o doomstat.o dstrings.o d_event.o d_items.o d_iwad.o d_loop.o d_main.o d_mode.o d_net.o f_finale.o f_wipe.o g_game.o hu_lib.o hu_stuff.o info.o i_cdmus.o i_endoom.o i_joystick.o i_scale.o i_sound.o i_system.o i_timer.o memio.o m_argv.o m_bbox.o m_cheat.o m_config.o m_controls.o m_fixed.o m_menu.o m_misc.o m_random.o p_ceilng.o p_doors.o p_enemy.o p_floor.o p_inter.o p_lights.o p_map.o p_maputl.o p_mobj.o p_plats.o p_pspr.o p_saveg.o p_setup.o p_sight.o p_spec.o p_switch.o p_telept.o p_tick.o p_user.o r_bsp.o r_data.o r_draw.o r_main.o r_plane.o r_segs.o r_sky.o r_things.o sha1.o sounds.o statdump.o st_lib.o st_stuff.o s_sound.o tables.o v_video.o wi_stuff.o w_checksum.o w_file.o w_main.o w_wad.o z_zone.o w_file_stdc.o i_input.o i_video.o doomgeneric.o doomgeneric_sdl.o mus2mid.o i_sdlmusic.o i_sdlsound.o OBJS += $(addprefix $(OBJDIR)/, $(SRC_DOOM)) all: $(OUTPUT) diff --git a/doomgeneric/doomfeatures.h b/doomgeneric/doomfeatures.h index f7b15f7..dff6936 100644 --- a/doomgeneric/doomfeatures.h +++ b/doomgeneric/doomfeatures.h @@ -33,7 +33,7 @@ // Enables sound output -#undef FEATURE_SOUND +//#undef FEATURE_SOUND #endif /* #ifndef DOOM_FEATURES_H */ diff --git a/doomgeneric/dummy.c b/doomgeneric/dummy.c index 0068d03..693901c 100644 --- a/doomgeneric/dummy.c +++ b/doomgeneric/dummy.c @@ -40,10 +40,6 @@ boolean drone = false; * public functions * *---------------------------------------------------------------------*/ -void I_InitTimidityConfig(void) -{ -} - /*---------------------------------------------------------------------* * eof * *---------------------------------------------------------------------*/ diff --git a/doomgeneric/i_cdmus.c b/doomgeneric/i_cdmus.c index 6a9a9a6..12a815c 100644 --- a/doomgeneric/i_cdmus.c +++ b/doomgeneric/i_cdmus.c @@ -19,8 +19,8 @@ #include #ifdef ORIGCODE -#include "SDL.h" -#include "SDL_cdrom.h" +#include "SDL2/SDL.h" +#include "SDL2/SDL_cdrom.h" #endif #include "doomtype.h" diff --git a/doomgeneric/i_sdlmusic.c b/doomgeneric/i_sdlmusic.c new file mode 100644 index 0000000..c2101b7 --- /dev/null +++ b/doomgeneric/i_sdlmusic.c @@ -0,0 +1,1316 @@ +// +// 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 +#include +#include +#include +#include "SDL2/SDL.h" +#include "SDL2/SDL_mixer.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" + +#define MAXMIDLENGTH (96 * 1024) +#define MID_HEADER_MAGIC "MThd" +#define MUS_HEADER_MAGIC "MUS\x1a" + +#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 + +// 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; + +// 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; + +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 +// responsibility to shut it down + +static boolean sdl_was_initialized = false; + +static boolean musicpaused = false; +static int current_music_volume; + +char *timidity_cfg_path = ""; + +static char *temp_timidity_cfg = NULL; + +// If true, we are playing a substitute digital track rather than in-WAD +// MIDI/MUS track, and file_metadata contains loop metadata. +static boolean playing_substitute = false; +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; + +// 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; + +// 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(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 = fopen(filename, "r"); + + 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; +} + +// 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; + char *filename; + 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. + // 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 (memcmp(hash, subst_music[i].hash, sizeof(hash)) == 0) + { + 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; +} + +// 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 + + // Paths in the substitute filenames can contain Unix-style / + // path separators, but we should convert this to the separator + // for the native platform. + path = M_StringReplace(path, "/", DIR_SEPARATOR_S); + + // 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); + free(path); + + 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; + + // 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; + } + + // 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 = strdup(""); + } + else + { + musicdir = M_StringJoin(configdir, "music", DIR_SEPARATOR_S, 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); + } +} + +// 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(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); + 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(); +} + +// 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 +// relative to the actual config file. + +static boolean WriteWrapperTimidityConfig(char *write_path) +{ + char *p, *path; + FILE *fstream; + + if (!strcmp(timidity_cfg_path, "")) + { + return false; + } + + fstream = fopen(write_path, "w"); + + if (fstream == NULL) + { + return false; + } + + p = strrchr(timidity_cfg_path, DIR_SEPARATOR); + if (p != NULL) + { + path = strdup(timidity_cfg_path); + path[p - timidity_cfg_path] = '\0'; + fprintf(fstream, "dir %s\n", path); + free(path); + } + + fprintf(fstream, "source %s\n", timidity_cfg_path); + fclose(fstream); + + return true; +} + +void I_InitTimidityConfig(void) +{ + char *env_string; + boolean success; + + temp_timidity_cfg = M_TempFile("timidity.cfg"); + + success = WriteWrapperTimidityConfig(temp_timidity_cfg); + + // Set the TIMIDITY_CFG environment variable to point to the temporary + // config file. + + if (success) + { + env_string = M_StringJoin("TIMIDITY_CFG=", temp_timidity_cfg, NULL); + putenv(env_string); + } + else + { + free(temp_timidity_cfg); + temp_timidity_cfg = NULL; + } +} + +// Remove the temporary config file generated by I_InitTimidityConfig(). + +static void RemoveTimidityConfig(void) +{ + if (temp_timidity_cfg != NULL) + { + remove(temp_timidity_cfg); + free(temp_timidity_cfg); + } +} + +// Shutdown music + +static void I_SDL_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; +} + +// 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; +} + +// Initialize music subsystem +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. + +#ifdef __MACOSX__ + { + const SDL_version *v = Mix_Linked_Version(); + + if (SDL_VERSIONNUM(v->major, v->minor, v->patch) + < SDL_VERSIONNUM(1, 2, 11)) + { + printf("\n" + " *** WARNING ***\n" + " You are using an old version of SDL_mixer.\n" + " Music playback on this version may cause crashes\n" + " under OS X and is disabled by default.\n" + "\n"); + } + } +#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. + + if (SDLIsInitialized()) + { + music_initialized = true; + } + else + { + if (SDL_Init(SDL_INIT_AUDIO) < 0) + { + fprintf(stderr, "Unable to set up sound.\n"); + } + else if (Mix_OpenAudio(snd_samplerate, AUDIO_S16SYS, 2, 1024) < 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; + } + } + + // Once initialization is complete, the temporary Timidity config + // file can be removed. + + RemoveTimidityConfig(); + + // If snd_musiccmd is set, we need to call Mix_SetMusicCMD to + // configure an external music playback program. + + if (strlen(snd_musiccmd) > 0) + { + Mix_SetMusicCMD(snd_musiccmd); + } + + // Register an effect function to track the music position. + Mix_RegisterEffect(MIX_CHANNEL_POST, TrackPositionCallback, NULL, NULL); + + // If we're in GENMIDI mode, try to load sound packs. + if (snd_musicdevice == SNDDEVICE_GENMIDI) + { + LoadSubstituteConfigs(); + } + + return music_initialized; +} + +// +// SDL_mixer's native MIDI music playing does not pause properly. +// As a workaround, set the volume to 0 when paused. +// + +static void UpdateMusicVolume(void) +{ + int vol; + + if (musicpaused) + { + vol = 0; + } + else + { + vol = (current_music_volume * MIX_MAX_VOLUME) / 127; + } + + Mix_VolumeMusic(vol); +} + +// Set music volume (0 - 127) + +static void I_SDL_SetMusicVolume(int volume) +{ + // Internal state variable. + current_music_volume = volume; + + UpdateMusicVolume(); +} + +// Start playing a mid + +static void I_SDL_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; + } + + // Don't loop when playing substitute music, as we do it + // ourselves instead. + if (playing_substitute && file_metadata.valid) + { + loops = 1; + SDL_LockAudio(); + current_track_pos = 0; // start of track + SDL_UnlockAudio(); + } + + Mix_PlayMusic(current_track_music, loops); +} + +static void I_SDL_PauseSong(void) +{ + if (!music_initialized) + { + return; + } + + musicpaused = true; + + UpdateMusicVolume(); +} + +static void I_SDL_ResumeSong(void) +{ + if (!music_initialized) + { + return; + } + + musicpaused = false; + + UpdateMusicVolume(); +} + +static void I_SDL_StopSong(void) +{ + if (!music_initialized) + { + return; + } + + Mix_HaltMusic(); + playing_substitute = false; + current_track_music = NULL; +} + +static void I_SDL_UnRegisterSong(void *handle) +{ + Mix_Music *music = (Mix_Music *) handle; + + if (!music_initialized) + { + return; + } + + if (handle == NULL) + { + return; + } + + Mix_FreeMusic(music); +} + +// Determine whether memory block is a .mid file + +static boolean IsMid(byte *mem, int len) +{ + return len > 4 && !memcmp(mem, "MThd", 4); +} + +static boolean ConvertMus(byte *musdata, int len, char *filename) +{ + MEMFILE *instream; + MEMFILE *outstream; + void *outbuf; + size_t outbuf_len; + int result; + + instream = mem_fopen_read(musdata, len); + outstream = mem_fopen_write(); + + result = mus2mid(instream, outstream); + + if (result == 0) + { + mem_get_buf(outstream, &outbuf, &outbuf_len); + + M_WriteFile(filename, outbuf, outbuf_len); + } + + mem_fclose(instream); + mem_fclose(outstream); + + return result; +} + +static void *I_SDL_RegisterSong(void *data, int len) +{ + char *filename; + Mix_Music *music; + + if (!music_initialized) + { + return NULL; + } + + playing_substitute = false; + + // 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 + { + // Read loop point metadata from the file so that we know where + // to loop the music. + playing_substitute = true; + ReadLoopPoints(filename, &file_metadata); + return music; + } + } + + // MUS files begin with "MUS" + // Reject anything which doesnt have this signature + + filename = M_TempFile("doom.mid"); + + if (IsMid(data, len) && len < MAXMIDLENGTH) + { + M_WriteFile(filename, data, len); + } + else + { + // Assume a MUS file and try to convert + + ConvertMus(data, len, filename); + } + + // Load the MIDI. In an ideal world we'd be using Mix_LoadMUS_RW() + // by now, but Mix_SetMusicCMD() only works with Mix_LoadMUS(), so + // we have to generate a temporary file. + + music = Mix_LoadMUS(filename); + + if (music == NULL) + { + // Failed to load + + fprintf(stderr, "Error loading midi: %s\n", Mix_GetError()); + } + + // Remove the temporary MIDI file; however, when using an external + // MIDI program we can't delete the file. Otherwise, the program + // won't find the file to play. This means we leave a mess on + // disk :( + + if (strlen(snd_musiccmd) == 0) + { + remove(filename); + } + + free(filename); + + return music; +} + +// Is the song playing? +static boolean I_SDL_MusicIsPlaying(void) +{ + if (!music_initialized) + { + return false; + } + + return Mix_PlayingMusic(); +} + +// 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 is playing on loop then reset to the start point. + // Otherwise we need to stop the track. + if (current_track_loop) + { + // 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(); + } + else + { + Mix_HaltMusic(); + current_track_music = NULL; + playing_substitute = false; + } +} + +// Poll music position; if we have passed the loop point end position +// then we need to go back. +static void I_SDL_PollMusic(void) +{ + if (playing_substitute && 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() && current_track_loop) + { + RestartCurrentTrack(); + } + } +} + +static snddevice_t music_sdl_devices[] = +{ + SNDDEVICE_PAS, + SNDDEVICE_GUS, + SNDDEVICE_WAVEBLASTER, + SNDDEVICE_SOUNDCANVAS, + SNDDEVICE_GENMIDI, + SNDDEVICE_AWE32, +}; + +music_module_t music_sdl_module = +{ + music_sdl_devices, + arrlen(music_sdl_devices), + I_SDL_InitMusic, + I_SDL_ShutdownMusic, + I_SDL_SetMusicVolume, + I_SDL_PauseSong, + I_SDL_ResumeSong, + I_SDL_RegisterSong, + I_SDL_UnRegisterSong, + I_SDL_PlaySong, + I_SDL_StopSong, + I_SDL_MusicIsPlaying, + I_SDL_PollMusic, +}; + diff --git a/doomgeneric/i_sdlsound.c b/doomgeneric/i_sdlsound.c new file mode 100644 index 0000000..e85e2fc --- /dev/null +++ b/doomgeneric/i_sdlsound.c @@ -0,0 +1,1076 @@ +// +// Copyright(C) 1993-1996 Id Software, Inc. +// Copyright(C) 2005-2014 Simon Howard +// Copyright(C) 2008 David Flater +// +// 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 sound. +// + +#include "config.h" + +#include +#include +#include +#include +#include +#include "SDL2/SDL.h" +#include "SDL2/SDL_mixer.h" + +#ifdef HAVE_LIBSAMPLERATE +#include +#endif + +#include "deh_str.h" +#include "i_sound.h" +#include "i_system.h" +#include "i_swap.h" +#include "m_argv.h" +#include "m_misc.h" +#include "w_wad.h" +#include "z_zone.h" + +#include "doomtype.h" + +#define LOW_PASS_FILTER 1 +//#define DEBUG_DUMP_WAVS +#define NUM_CHANNELS 16 + +typedef struct allocated_sound_s allocated_sound_t; + +struct allocated_sound_s +{ + sfxinfo_t *sfxinfo; + Mix_Chunk chunk; + int use_count; + allocated_sound_t *prev, *next; +}; + +static boolean setpanning_workaround = false; + +static boolean sound_initialized = false; + +static sfxinfo_t *channels_playing[NUM_CHANNELS]; + +static int mixer_freq; +static Uint16 mixer_format; +static int mixer_channels; +static boolean use_sfx_prefix; +static boolean (*ExpandSoundData)(sfxinfo_t *sfxinfo, + byte *data, + int samplerate, + int length) = NULL; + +// Doubly-linked list of allocated sounds. +// When a sound is played, it is moved to the head, so that the oldest +// sounds not used recently are at the tail. + +static allocated_sound_t *allocated_sounds_head = NULL; +static allocated_sound_t *allocated_sounds_tail = NULL; +static int allocated_sounds_size = 0; + +int use_libsamplerate = 0; + +// Scale factor used when converting libsamplerate floating point numbers +// to integers. Too high means the sounds can clip; too low means they +// will be too quiet. This is an amount that should avoid clipping most +// of the time: with all the Doom IWAD sound effects, at least. If a PWAD +// is used, clipping might occur. + +float libsamplerate_scale = 0.65f; + +// Hook a sound into the linked list at the head. + +static void AllocatedSoundLink(allocated_sound_t *snd) +{ + snd->prev = NULL; + + snd->next = allocated_sounds_head; + allocated_sounds_head = snd; + + if (allocated_sounds_tail == NULL) + { + allocated_sounds_tail = snd; + } + else + { + snd->next->prev = snd; + } +} + +// Unlink a sound from the linked list. + +static void AllocatedSoundUnlink(allocated_sound_t *snd) +{ + if (snd->prev == NULL) + { + allocated_sounds_head = snd->next; + } + else + { + snd->prev->next = snd->next; + } + + if (snd->next == NULL) + { + allocated_sounds_tail = snd->prev; + } + else + { + snd->next->prev = snd->prev; + } +} + +static void FreeAllocatedSound(allocated_sound_t *snd) +{ + // Unlink from linked list. + + AllocatedSoundUnlink(snd); + + // Unlink from higher-level code. + + snd->sfxinfo->driver_data = NULL; + + // Keep track of the amount of allocated sound data: + + allocated_sounds_size -= snd->chunk.alen; + + free(snd); +} + +// Search from the tail backwards along the allocated sounds list, find +// and free a sound that is not in use, to free up memory. Return true +// for success. + +static boolean FindAndFreeSound(void) +{ + allocated_sound_t *snd; + + snd = allocated_sounds_tail; + + while (snd != NULL) + { + if (snd->use_count == 0) + { + FreeAllocatedSound(snd); + return true; + } + + snd = snd->prev; + } + + // No available sounds to free... + + return false; +} + +// Enforce SFX cache size limit. We are just about to allocate "len" +// bytes on the heap for a new sound effect, so free up some space +// so that we keep allocated_sounds_size < snd_cachesize + +static void ReserveCacheSpace(size_t len) +{ + if (snd_cachesize <= 0) + { + return; + } + + // Keep freeing sound effects that aren't currently being played, + // until there is enough space for the new sound. + + while (allocated_sounds_size + len > snd_cachesize) + { + // Free a sound. If there is nothing more to free, stop. + + if (!FindAndFreeSound()) + { + break; + } + } +} + +// Allocate a block for a new sound effect. + +static Mix_Chunk *AllocateSound(sfxinfo_t *sfxinfo, size_t len) +{ + allocated_sound_t *snd; + + // Keep allocated sounds within the cache size. + + ReserveCacheSpace(len); + + // Allocate the sound structure and data. The data will immediately + // follow the structure, which acts as a header. + + do + { + snd = malloc(sizeof(allocated_sound_t) + len); + + // Out of memory? Try to free an old sound, then loop round + // and try again. + + if (snd == NULL && !FindAndFreeSound()) + { + return NULL; + } + + } while (snd == NULL); + + // Skip past the chunk structure for the audio buffer + + snd->chunk.abuf = (byte *) (snd + 1); + snd->chunk.alen = len; + snd->chunk.allocated = 1; + snd->chunk.volume = MIX_MAX_VOLUME; + + snd->sfxinfo = sfxinfo; + snd->use_count = 0; + + // driver_data pointer points to the allocated_sound structure. + + sfxinfo->driver_data = snd; + + // Keep track of how much memory all these cached sounds are using... + + allocated_sounds_size += len; + + AllocatedSoundLink(snd); + + return &snd->chunk; +} + +// Lock a sound, to indicate that it may not be freed. + +static void LockAllocatedSound(allocated_sound_t *snd) +{ + // Increase use count, to stop the sound being freed. + + ++snd->use_count; + + //printf("++ %s: Use count=%i\n", snd->sfxinfo->name, snd->use_count); + + // When we use a sound, re-link it into the list at the head, so + // that the oldest sounds fall to the end of the list for freeing. + + AllocatedSoundUnlink(snd); + AllocatedSoundLink(snd); +} + +// Unlock a sound to indicate that it may now be freed. + +static void UnlockAllocatedSound(allocated_sound_t *snd) +{ + if (snd->use_count <= 0) + { + I_Error("Sound effect released more times than it was locked..."); + } + + --snd->use_count; + + //printf("-- %s: Use count=%i\n", snd->sfxinfo->name, snd->use_count); +} + +// When a sound stops, check if it is still playing. If it is not, +// we can mark the sound data as CACHE to be freed back for other +// means. + +static void ReleaseSoundOnChannel(int channel) +{ + sfxinfo_t *sfxinfo = channels_playing[channel]; + + if (sfxinfo == NULL) + { + return; + } + + channels_playing[channel] = NULL; + + UnlockAllocatedSound(sfxinfo->driver_data); +} + +#ifdef HAVE_LIBSAMPLERATE + +// Returns the conversion mode for libsamplerate to use. + +static int SRC_ConversionMode(void) +{ + switch (use_libsamplerate) + { + // 0 = disabled + + default: + case 0: + return -1; + + // Ascending numbers give higher quality + + case 1: + return SRC_LINEAR; + case 2: + return SRC_ZERO_ORDER_HOLD; + case 3: + return SRC_SINC_FASTEST; + case 4: + return SRC_SINC_MEDIUM_QUALITY; + case 5: + return SRC_SINC_BEST_QUALITY; + } +} + +// libsamplerate-based generic sound expansion function for any sample rate +// unsigned 8 bits --> signed 16 bits +// mono --> stereo +// samplerate --> mixer_freq +// Returns number of clipped samples. +// DWF 2008-02-10 with cleanups by Simon Howard. + +static boolean ExpandSoundData_SRC(sfxinfo_t *sfxinfo, + byte *data, + int samplerate, + int length) +{ + SRC_DATA src_data; + uint32_t i, abuf_index=0, clipped=0; + uint32_t alen; + int retn; + int16_t *expanded; + Mix_Chunk *chunk; + + src_data.input_frames = length; + src_data.data_in = malloc(length * sizeof(float)); + src_data.src_ratio = (double)mixer_freq / samplerate; + + // We include some extra space here in case of rounding-up. + src_data.output_frames = src_data.src_ratio * length + (mixer_freq / 4); + src_data.data_out = malloc(src_data.output_frames * sizeof(float)); + + assert(src_data.data_in != NULL && src_data.data_out != NULL); + + // Convert input data to floats + + for (i=0; iabuf; + + // Convert the result back into 16-bit integers. + + for (i=0; i INT16_MAX) + { + cvtval_i = INT16_MAX; + ++clipped; + } + + // Left and right channels + + expanded[abuf_index++] = cvtval_i; + expanded[abuf_index++] = cvtval_i; + } + + free(src_data.data_in); + free(src_data.data_out); + + if (clipped > 0) + { + fprintf(stderr, "Sound '%s': clipped %u samples (%0.2f %%)\n", + sfxinfo->name, clipped, + 400.0 * clipped / chunk->alen); + } + + return true; +} + +#endif + +static boolean ConvertibleRatio(int freq1, int freq2) +{ + int ratio; + + if (freq1 > freq2) + { + return ConvertibleRatio(freq2, freq1); + } + else if ((freq2 % freq1) != 0) + { + // Not in a direct ratio + + return false; + } + else + { + // Check the ratio is a power of 2 + + ratio = freq2 / freq1; + + while ((ratio & 1) == 0) + { + ratio = ratio >> 1; + } + + return ratio == 1; + } +} + +#ifdef DEBUG_DUMP_WAVS + +// Debug code to dump resampled sound effects to WAV files for analysis. + +static void WriteWAV(char *filename, byte *data, + uint32_t length, int samplerate) +{ + FILE *wav; + unsigned int i; + unsigned short s; + + wav = fopen(filename, "wb"); + + // Header + + fwrite("RIFF", 1, 4, wav); + i = LONG(36 + samplerate); + fwrite(&i, 4, 1, wav); + fwrite("WAVE", 1, 4, wav); + + // Subchunk 1 + + fwrite("fmt ", 1, 4, wav); + i = LONG(16); + fwrite(&i, 4, 1, wav); // Length + s = SHORT(1); + fwrite(&s, 2, 1, wav); // Format (PCM) + s = SHORT(2); + fwrite(&s, 2, 1, wav); // Channels (2=stereo) + i = LONG(samplerate); + fwrite(&i, 4, 1, wav); // Sample rate + i = LONG(samplerate * 2 * 2); + fwrite(&i, 4, 1, wav); // Byte rate (samplerate * stereo * 16 bit) + s = SHORT(2 * 2); + fwrite(&s, 2, 1, wav); // Block align (stereo * 16 bit) + s = SHORT(16); + fwrite(&s, 2, 1, wav); // Bits per sample (16 bit) + + // Data subchunk + + fwrite("data", 1, 4, wav); + i = LONG(length); + fwrite(&i, 4, 1, wav); // Data length + fwrite(data, 1, length, wav); // Data + + fclose(wav); +} + +#endif + +// Generic sound expansion function for any sample rate. +// Returns number of clipped samples (always 0). + +static boolean ExpandSoundData_SDL(sfxinfo_t *sfxinfo, + byte *data, + int samplerate, + int length) +{ + SDL_AudioCVT convertor; + Mix_Chunk *chunk; + uint32_t expanded_length; + + // Calculate the length of the expanded version of the sample. + + expanded_length = (uint32_t) ((((uint64_t) length) * mixer_freq) / samplerate); + + // Double up twice: 8 -> 16 bit and mono -> stereo + + expanded_length *= 4; + + // Allocate a chunk in which to expand the sound + + chunk = AllocateSound(sfxinfo, expanded_length); + + if (chunk == NULL) + { + return false; + } + + // If we can, use the standard / optimized SDL conversion routines. + Sint16 *expanded = (Sint16 *) chunk->abuf; + int expand_ratio; + int i; + + // Generic expansion if conversion does not work: + // + // SDL's audio conversion only works for rate conversions that are + // powers of 2; if the two formats are not in a direct power of 2 + // ratio, do this naive conversion instead. + + // number of samples in the converted sound + + expanded_length = ((uint64_t) length * mixer_freq) / samplerate; + expand_ratio = (length << 8) / expanded_length; + + for (i=0; i> 8; + + sample = data[src] | (data[src] << 8); + sample -= 32768; + + // expand 8->16 bits, mono->stereo + + expanded[i * 2] = expanded[i * 2 + 1] = sample; + } + +#ifdef LOW_PASS_FILTER + // Perform a low-pass filter on the upscaled sound to filter + // out high-frequency noise from the conversion process. + + { + float rc, dt, alpha; + + // Low-pass filter for cutoff frequency f: + // + // For sampling rate r, dt = 1 / r + // rc = 1 / 2*pi*f + // alpha = dt / (rc + dt) + + // Filter to the half sample rate of the original sound effect + // (maximum frequency, by nyquist) + + dt = 1.0f / mixer_freq; + rc = 1.0f / (3.14f * samplerate); + alpha = dt / (rc + dt); + + // Both channels are processed in parallel, hence [i-2]: + + for (i=2; ilumpnum; + data = W_CacheLumpNum(lumpnum, PU_STATIC); + lumplen = W_LumpLength(lumpnum); + + // Check the header, and ensure this is a valid sound + + if (lumplen < 8 + || data[0] != 0x03 || data[1] != 0x00) + { + // Invalid sound + + return false; + } + + // 16 bit sample rate field, 32 bit length field + + samplerate = (data[3] << 8) | data[2]; + length = (data[7] << 24) | (data[6] << 16) | (data[5] << 8) | data[4]; + + // If the header specifies that the length of the sound is greater than + // the length of the lump itself, this is an invalid sound lump + + // We also discard sound lumps that are less than 49 samples long, + // as this is how DMX behaves - although the actual cut-off length + // seems to vary slightly depending on the sample rate. This needs + // further investigation to better understand the correct + // behavior. + + if (length > lumplen - 8 || length <= 48) + { + return false; + } + + // The DMX sound library seems to skip the first 16 and last 16 + // bytes of the lump - reason unknown. + + data += 16; + length -= 32; + + // Sample rate conversion + + if (!ExpandSoundData(sfxinfo, data + 8, samplerate, length)) + { + return false; + } + +#ifdef DEBUG_DUMP_WAVS + { + char filename[16]; + + M_snprintf(filename, sizeof(filename), "%s.wav", + DEH_String(S_sfx[sound].name)); + WriteWAV(filename, sound_chunks[sound].abuf, + sound_chunks[sound].alen, mixer_freq); + } +#endif + + // don't need the original lump any more + + W_ReleaseLumpNum(lumpnum); + + return true; +} + +static void GetSfxLumpName(sfxinfo_t *sfx, char *buf, size_t buf_len) +{ + // Linked sfx lumps? Get the lump number for the sound linked to. + + if (sfx->link != NULL) + { + sfx = sfx->link; + } + + // Doom adds a DS* prefix to sound lumps; Heretic and Hexen don't + // do this. + + if (use_sfx_prefix) + { + M_snprintf(buf, buf_len, "ds%s", DEH_String(sfx->name)); + } + else + { + M_StringCopy(buf, DEH_String(sfx->name), buf_len); + } +} + +#ifdef HAVE_LIBSAMPLERATE + +// Preload all the sound effects - stops nasty ingame freezes + +static void I_SDL_PrecacheSounds(sfxinfo_t *sounds, int num_sounds) +{ + char namebuf[9]; + int i; + + // Don't need to precache the sounds unless we are using libsamplerate. + + if (use_libsamplerate == 0) + { + return; + } + + printf("I_SDL_PrecacheSounds: Precaching all sound effects.."); + + for (i=0; idriver_data == NULL) + { + if (!CacheSFX(sfxinfo)) + { + return false; + } + } + + LockAllocatedSound(sfxinfo->driver_data); + + return true; +} + +// +// Retrieve the raw data lump index +// for a given SFX name. +// + +static int I_SDL_GetSfxLumpNum(sfxinfo_t *sfx) +{ + char namebuf[9]; + + GetSfxLumpName(sfx, namebuf, sizeof(namebuf)); + + return W_GetNumForName(namebuf); +} + +static void I_SDL_UpdateSoundParams(int handle, int vol, int sep) +{ + int left, right; + + if (!sound_initialized || handle < 0 || handle >= NUM_CHANNELS) + { + return; + } + + left = ((254 - sep) * vol) / 127; + right = ((sep) * vol) / 127; + + if (left < 0) left = 0; + else if ( left > 255) left = 255; + if (right < 0) right = 0; + else if (right > 255) right = 255; + + // SDL_mixer version 1.2.8 and earlier has a bug in the Mix_SetPanning + // function. A workaround is to call Mix_UnregisterAllEffects for + // the channel before calling it. This is undesirable as it may lead + // to the channel volumes resetting briefly. + + if (setpanning_workaround) + { + Mix_UnregisterAllEffects(handle); + } + + Mix_SetPanning(handle, left, right); +} + +// +// Starting a sound means adding it +// to the current list of active sounds +// in the internal channels. +// As the SFX info struct contains +// e.g. a pointer to the raw data, +// it is ignored. +// As our sound handling does not handle +// priority, it is ignored. +// Pitching (that is, increased speed of playback) +// is set, but currently not used by mixing. +// + +static int I_SDL_StartSound(sfxinfo_t *sfxinfo, int channel, int vol, int sep) +{ + allocated_sound_t *snd; + + if (!sound_initialized || channel < 0 || channel >= NUM_CHANNELS) + { + return -1; + } + + // Release a sound effect if there is already one playing + // on this channel + + ReleaseSoundOnChannel(channel); + + // Get the sound data + + if (!LockSound(sfxinfo)) + { + return -1; + } + + snd = sfxinfo->driver_data; + + // play sound + + Mix_PlayChannelTimed(channel, &snd->chunk, 0, -1); + + channels_playing[channel] = sfxinfo; + + // set separation, etc. + + I_SDL_UpdateSoundParams(channel, vol, sep); + + return channel; +} + +static void I_SDL_StopSound(int handle) +{ + if (!sound_initialized || handle < 0 || handle >= NUM_CHANNELS) + { + return; + } + + Mix_HaltChannel(handle); + + // Sound data is no longer needed; release the + // sound data being used for this channel + + ReleaseSoundOnChannel(handle); +} + + +static boolean I_SDL_SoundIsPlaying(int handle) +{ + if (!sound_initialized || handle < 0 || handle >= NUM_CHANNELS) + { + return false; + } + + return Mix_Playing(handle); +} + +// +// Periodically called to update the sound system +// + +static void I_SDL_UpdateSound(void) +{ + int i; + + // Check all channels to see if a sound has finished + + for (i=0; i limit) + { + return (1 << n); + } + } + + // Should never happen? + + return 1024; +} + +static boolean I_SDL_InitSound(boolean _use_sfx_prefix) +{ + int i; + + use_sfx_prefix = _use_sfx_prefix; + + // No sounds yet + + for (i=0; imajor, + mixer_version->minor, + mixer_version->patch); + + if (v <= SDL_VERSIONNUM(1, 2, 8)) + { + setpanning_workaround = true; + fprintf(stderr, "\n" + "ATTENTION: You are using an old version of SDL_mixer!\n" + " This version has a bug that may cause " + "your sound to stutter.\n" + " Please upgrade to a newer version!\n" + "\n"); + } + } + + Mix_AllocateChannels(NUM_CHANNELS); + + SDL_PauseAudio(0); + + sound_initialized = true; + + return true; +} + +static snddevice_t sound_sdl_devices[] = +{ + SNDDEVICE_SB, + SNDDEVICE_PAS, + SNDDEVICE_GUS, + SNDDEVICE_WAVEBLASTER, + SNDDEVICE_SOUNDCANVAS, + SNDDEVICE_AWE32, +}; + +sound_module_t sound_sdl_module = +{ + sound_sdl_devices, + arrlen(sound_sdl_devices), + I_SDL_InitSound, + I_SDL_ShutdownSound, + I_SDL_GetSfxLumpNum, + I_SDL_UpdateSound, + I_SDL_UpdateSoundParams, + I_SDL_StartSound, + I_SDL_StopSound, + I_SDL_SoundIsPlaying, + I_SDL_PrecacheSounds, +}; + diff --git a/doomgeneric/i_sound.c b/doomgeneric/i_sound.c index 71947d2..ade161d 100644 --- a/doomgeneric/i_sound.c +++ b/doomgeneric/i_sound.c @@ -18,8 +18,8 @@ #include #include -#ifdef ORIGCODE -#include "SDL_mixer.h" +#ifdef FEATURE_SOUND +#include "SDL2/SDL_mixer.h" #endif #include "config.h" @@ -80,21 +80,18 @@ extern char *timidity_cfg_path; // so that the config file can be shared between chocolate // doom and doom.exe -#if ORIGCODE static int snd_sbport = 0; static int snd_sbirq = 0; static int snd_sbdma = 0; static int snd_mport = 0; -#endif // Compiled-in sound modules: static sound_module_t *sound_modules[] = { -#ifdef FEATURE_SOUND + #ifdef FEATURE_SOUND &sound_sdl_module, - &sound_pcsound_module, -#endif + #endif NULL, }; @@ -102,10 +99,9 @@ static sound_module_t *sound_modules[] = static music_module_t *music_modules[] = { -#ifdef FEATURE_SOUND + #ifdef FEATURE_SOUND &music_sdl_module, - &music_opl_module, -#endif + #endif NULL, }; @@ -162,9 +158,9 @@ static void InitMusicModule(void) { int i; - music_module = NULL; + music_module = &music_sdl_module; return; - for (i=0; music_modules[i] != NULL; ++i) + /*for (i=0; music_modules[i] != NULL; ++i) { // Is the music device in the list of devices supported // by this module? @@ -181,7 +177,7 @@ static void InitMusicModule(void) return; } } - } + }*/ } // @@ -243,6 +239,7 @@ void I_InitSound(boolean use_sfx_prefix) InitMusicModule(); } } + } void I_ShutdownSound(void) @@ -356,6 +353,10 @@ void I_PrecacheSounds(sfxinfo_t *sounds, int num_sounds) void I_InitMusic(void) { + if(music_module != NULL) + { + music_module->Init(); + } } void I_ShutdownMusic(void) @@ -433,11 +434,11 @@ boolean I_MusicIsPlaying(void) { return false; } + } void I_BindSoundVariables(void) { -#ifdef ORIGCODE extern int use_libsamplerate; extern float libsamplerate_scale; @@ -451,11 +452,6 @@ void I_BindSoundVariables(void) M_BindVariable("snd_musiccmd", &snd_musiccmd); M_BindVariable("snd_samplerate", &snd_samplerate); M_BindVariable("snd_cachesize", &snd_cachesize); - M_BindVariable("opl_io_port", &opl_io_port); - - M_BindVariable("timidity_cfg_path", &timidity_cfg_path); - M_BindVariable("gus_patch_path", &gus_patch_path); - M_BindVariable("gus_ram_kb", &gus_ram_kb); #ifdef FEATURE_SOUND M_BindVariable("use_libsamplerate", &use_libsamplerate); @@ -465,18 +461,5 @@ void I_BindSoundVariables(void) // Before SDL_mixer version 1.2.11, MIDI music caused the game // to crash when it looped. If this is an old SDL_mixer version, // disable MIDI. - -#ifdef __MACOSX__ - { - const SDL_version *v = Mix_Linked_Version(); - - if (SDL_VERSIONNUM(v->major, v->minor, v->patch) - < SDL_VERSIONNUM(1, 2, 11)) - { - snd_musicdevice = SNDDEVICE_NONE; - } - } -#endif -#endif } diff --git a/doomgeneric/i_swap.h b/doomgeneric/i_swap.h index d1fd754..0dbcdb5 100644 --- a/doomgeneric/i_swap.h +++ b/doomgeneric/i_swap.h @@ -20,8 +20,7 @@ #ifndef __I_SWAP__ #define __I_SWAP__ -#ifdef ORIGCODE -#include "SDL_endian.h" +#include // Endianess handling. // WAD files are stored little endian. @@ -42,13 +41,17 @@ #define SYS_BIG_ENDIAN #endif +// cosmito from lsdldoom +#define doom_swap_s(x) \ + ((short int)((((unsigned short int)(x) & 0x00ff) << 8) | \ + (((unsigned short int)(x) & 0xff00) >> 8))) + + +#if ( SDL_BYTEORDER == SDL_BIG_ENDIAN ) +#define doom_wtohs(x) doom_swap_s(x) #else - -#define SHORT(x) ((signed short) (x)) -#define LONG(x) ((signed int) (x)) - -#define SYS_LITTLE_ENDIAN - -#endif +#define doom_wtohs(x) (short int)(x) +#endif + #endif diff --git a/doomgeneric/i_timer.c b/doomgeneric/i_timer.c index 2d6a7cf..27beaaf 100644 --- a/doomgeneric/i_timer.c +++ b/doomgeneric/i_timer.c @@ -22,6 +22,8 @@ #include "doomgeneric.h" #include +#include +#include //#include //#include @@ -50,7 +52,7 @@ int I_GetTime (void) ticks -= basetime; - return (ticks * TICRATE) / 1000; + return (ticks * TICRATE) / 1000; } @@ -90,6 +92,12 @@ void I_InitTimer(void) { // initialize timer - //SDL_Init(SDL_INIT_TIMER); + printf("I_InitTimer: Setting up timer.\n"); + if (SDL_Init(SDL_INIT_TIMER) < 0) + { + printf("SDL_Init failed: %s\n", SDL_GetError()); + atexit(SDL_Quit); + exit(1); + } } diff --git a/doomgeneric/mus2mid.c b/doomgeneric/mus2mid.c new file mode 100644 index 0000000..bcbeb00 --- /dev/null +++ b/doomgeneric/mus2mid.c @@ -0,0 +1,737 @@ +// +// Copyright(C) 1993-1996 Id Software, Inc. +// Copyright(C) 2005-2014 Simon Howard +// Copyright(C) 2006 Ben Ryves 2006 +// +// 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. +// +// mus2mid.c - Ben Ryves 2006 - http://benryves.com - benryves@benryves.com +// Use to convert a MUS file into a single track, type 0 MIDI file. + +#include + +#include "doomtype.h" +#include "i_swap.h" + +#include "memio.h" +#include "mus2mid.h" + +#define NUM_CHANNELS 16 + +#define MIDI_PERCUSSION_CHAN 9 +#define MUS_PERCUSSION_CHAN 15 + +// MUS event codes +typedef enum +{ + mus_releasekey = 0x00, + mus_presskey = 0x10, + mus_pitchwheel = 0x20, + mus_systemevent = 0x30, + mus_changecontroller = 0x40, + mus_scoreend = 0x60 +} musevent; + +// MIDI event codes +typedef enum +{ + midi_releasekey = 0x80, + midi_presskey = 0x90, + midi_aftertouchkey = 0xA0, + midi_changecontroller = 0xB0, + midi_changepatch = 0xC0, + midi_aftertouchchannel = 0xD0, + midi_pitchwheel = 0xE0 +} midievent; + +// Structure to hold MUS file header +typedef struct +{ + byte id[4]; + unsigned short scorelength; + unsigned short scorestart; + unsigned short primarychannels; + unsigned short secondarychannels; + unsigned short instrumentcount; +} musheader; + +// Standard MIDI type 0 header + track header +static const byte midiheader[] = +{ + 'M', 'T', 'h', 'd', // Main header + 0x00, 0x00, 0x00, 0x06, // Header size + 0x00, 0x00, // MIDI type (0) + 0x00, 0x01, // Number of tracks + 0x00, 0x46, // Resolution + 'M', 'T', 'r', 'k', // Start of track + 0x00, 0x00, 0x00, 0x00 // Placeholder for track length +}; + +// Cached channel velocities +static byte channelvelocities[] = +{ + 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127 +}; + +// Timestamps between sequences of MUS events + +static unsigned int queuedtime = 0; + +// Counter for the length of the track + +static unsigned int tracksize; + +static const byte controller_map[] = +{ + 0x00, 0x20, 0x01, 0x07, 0x0A, 0x0B, 0x5B, 0x5D, + 0x40, 0x43, 0x78, 0x7B, 0x7E, 0x7F, 0x79 +}; + +static int channel_map[NUM_CHANNELS]; + +// Write timestamp to a MIDI file. + +static boolean WriteTime(unsigned int time, MEMFILE *midioutput) +{ + unsigned int buffer = time & 0x7F; + byte writeval; + + while ((time >>= 7) != 0) + { + buffer <<= 8; + buffer |= ((time & 0x7F) | 0x80); + } + + for (;;) + { + writeval = (byte)(buffer & 0xFF); + + if (mem_fwrite(&writeval, 1, 1, midioutput) != 1) + { + return true; + } + + ++tracksize; + + if ((buffer & 0x80) != 0) + { + buffer >>= 8; + } + else + { + queuedtime = 0; + return false; + } + } +} + + +// Write the end of track marker +static boolean WriteEndTrack(MEMFILE *midioutput) +{ + byte endtrack[] = {0xFF, 0x2F, 0x00}; + + if (WriteTime(queuedtime, midioutput)) + { + return true; + } + + if (mem_fwrite(endtrack, 1, 3, midioutput) != 3) + { + return true; + } + + tracksize += 3; + return false; +} + +// Write a key press event +static boolean WritePressKey(byte channel, byte key, + byte velocity, MEMFILE *midioutput) +{ + byte working = midi_presskey | channel; + + if (WriteTime(queuedtime, midioutput)) + { + return true; + } + + if (mem_fwrite(&working, 1, 1, midioutput) != 1) + { + return true; + } + + working = key & 0x7F; + + if (mem_fwrite(&working, 1, 1, midioutput) != 1) + { + return true; + } + + working = velocity & 0x7F; + + if (mem_fwrite(&working, 1, 1, midioutput) != 1) + { + return true; + } + + tracksize += 3; + + return false; +} + +// Write a key release event +static boolean WriteReleaseKey(byte channel, byte key, + MEMFILE *midioutput) +{ + byte working = midi_releasekey | channel; + + if (WriteTime(queuedtime, midioutput)) + { + return true; + } + + if (mem_fwrite(&working, 1, 1, midioutput) != 1) + { + return true; + } + + working = key & 0x7F; + + if (mem_fwrite(&working, 1, 1, midioutput) != 1) + { + return true; + } + + working = 0; + + if (mem_fwrite(&working, 1, 1, midioutput) != 1) + { + return true; + } + + tracksize += 3; + + return false; +} + +// Write a pitch wheel/bend event +static boolean WritePitchWheel(byte channel, short wheel, + MEMFILE *midioutput) +{ + byte working = midi_pitchwheel | channel; + + if (WriteTime(queuedtime, midioutput)) + { + return true; + } + + if (mem_fwrite(&working, 1, 1, midioutput) != 1) + { + return true; + } + + working = wheel & 0x7F; + + if (mem_fwrite(&working, 1, 1, midioutput) != 1) + { + return true; + } + + working = (wheel >> 7) & 0x7F; + + if (mem_fwrite(&working, 1, 1, midioutput) != 1) + { + return true; + } + + tracksize += 3; + return false; +} + +// Write a patch change event +static boolean WriteChangePatch(byte channel, byte patch, + MEMFILE *midioutput) +{ + byte working = midi_changepatch | channel; + + if (WriteTime(queuedtime, midioutput)) + { + return true; + } + + if (mem_fwrite(&working, 1, 1, midioutput) != 1) + { + return true; + } + + working = patch & 0x7F; + + if (mem_fwrite(&working, 1, 1, midioutput) != 1) + { + return true; + } + + tracksize += 2; + + return false; +} + +// Write a valued controller change event + +static boolean WriteChangeController_Valued(byte channel, + byte control, + byte value, + MEMFILE *midioutput) +{ + byte working = midi_changecontroller | channel; + + if (WriteTime(queuedtime, midioutput)) + { + return true; + } + + if (mem_fwrite(&working, 1, 1, midioutput) != 1) + { + return true; + } + + working = control & 0x7F; + + if (mem_fwrite(&working, 1, 1, midioutput) != 1) + { + return true; + } + + // Quirk in vanilla DOOM? MUS controller values should be + // 7-bit, not 8-bit. + + working = value;// & 0x7F; + + // Fix on said quirk to stop MIDI players from complaining that + // the value is out of range: + + if (working & 0x80) + { + working = 0x7F; + } + + if (mem_fwrite(&working, 1, 1, midioutput) != 1) + { + return true; + } + + tracksize += 3; + + return false; +} + +// Write a valueless controller change event +static boolean WriteChangeController_Valueless(byte channel, + byte control, + MEMFILE *midioutput) +{ + return WriteChangeController_Valued(channel, control, 0, + midioutput); +} + +// Allocate a free MIDI channel. + +static int AllocateMIDIChannel(void) +{ + int result; + int max; + int i; + + // Find the current highest-allocated channel. + + max = -1; + + for (i=0; i max) + { + max = channel_map[i]; + } + } + + // max is now equal to the highest-allocated MIDI channel. We can + // now allocate the next available channel. This also works if + // no channels are currently allocated (max=-1) + + result = max + 1; + + // Don't allocate the MIDI percussion channel! + + if (result == MIDI_PERCUSSION_CHAN) + { + ++result; + } + + return result; +} + +// Given a MUS channel number, get the MIDI channel number to use +// in the outputted file. + +static int GetMIDIChannel(int mus_channel, MEMFILE *midioutput) +{ + // Find the MIDI channel to use for this MUS channel. + // MUS channel 15 is the percusssion channel. + + if (mus_channel == MUS_PERCUSSION_CHAN) + { + return MIDI_PERCUSSION_CHAN; + } + else + { + // If a MIDI channel hasn't been allocated for this MUS channel + // yet, allocate the next free MIDI channel. + + if (channel_map[mus_channel] == -1) + { + channel_map[mus_channel] = AllocateMIDIChannel(); + + // First time using the channel, send an "all notes off" + // event. This fixes "The D_DDTBLU disease" described here: + // https://www.doomworld.com/vb/source-ports/66802-the + WriteChangeController_Valueless(channel_map[mus_channel], 0x7b, + midioutput); + } + + return channel_map[mus_channel]; + } +} + +static boolean ReadMusHeader(MEMFILE *file, musheader *header) +{ + boolean result; + + result = mem_fread(&header->id, sizeof(byte), 4, file) == 4 + && mem_fread(&header->scorelength, sizeof(short), 1, file) == 1 + && mem_fread(&header->scorestart, sizeof(short), 1, file) == 1 + && mem_fread(&header->primarychannels, sizeof(short), 1, file) == 1 + && mem_fread(&header->secondarychannels, sizeof(short), 1, file) == 1 + && mem_fread(&header->instrumentcount, sizeof(short), 1, file) == 1; + + if (result) + { + header->scorelength = SHORT(header->scorelength); + header->scorestart = SHORT(header->scorestart); + header->primarychannels = SHORT(header->primarychannels); + header->secondarychannels = SHORT(header->secondarychannels); + header->instrumentcount = SHORT(header->instrumentcount); + } + + return result; +} + + +// Read a MUS file from a stream (musinput) and output a MIDI file to +// a stream (midioutput). +// +// Returns 0 on success or 1 on failure. + +boolean mus2mid(MEMFILE *musinput, MEMFILE *midioutput) +{ + // Header for the MUS file + musheader musfileheader; + + // Descriptor for the current MUS event + byte eventdescriptor; + int channel; // Channel number + musevent event; + + + // Bunch of vars read from MUS lump + byte key; + byte controllernumber; + byte controllervalue; + + // Buffer used for MIDI track size record + byte tracksizebuffer[4]; + + // Flag for when the score end marker is hit. + int hitscoreend = 0; + + // Temp working byte + byte working; + // Used in building up time delays + unsigned int timedelay; + + // Initialise channel map to mark all channels as unused. + + for (channel=0; channel 14) + { + return true; + } + + if (WriteChangeController_Valueless(channel, + controller_map[controllernumber], + midioutput)) + { + return true; + } + + break; + + case mus_changecontroller: + if (mem_fread(&controllernumber, 1, 1, musinput) != 1) + { + return true; + } + + if (mem_fread(&controllervalue, 1, 1, musinput) != 1) + { + return true; + } + + if (controllernumber == 0) + { + if (WriteChangePatch(channel, controllervalue, + midioutput)) + { + return true; + } + } + else + { + if (controllernumber < 1 || controllernumber > 9) + { + return true; + } + + if (WriteChangeController_Valued(channel, + controller_map[controllernumber], + controllervalue, + midioutput)) + { + return true; + } + } + + break; + + case mus_scoreend: + hitscoreend = 1; + break; + + default: + return true; + break; + } + + if (eventdescriptor & 0x80) + { + break; + } + } + // Now we need to read the time code: + if (!hitscoreend) + { + timedelay = 0; + for (;;) + { + if (mem_fread(&working, 1, 1, musinput) != 1) + { + return true; + } + + timedelay = timedelay * 128 + (working & 0x7F); + if ((working & 0x80) == 0) + { + break; + } + } + queuedtime += timedelay; + } + } + + // End of track + if (WriteEndTrack(midioutput)) + { + return true; + } + + // Write the track size into the stream + if (mem_fseek(midioutput, 18, MEM_SEEK_SET)) + { + return true; + } + + tracksizebuffer[0] = (tracksize >> 24) & 0xff; + tracksizebuffer[1] = (tracksize >> 16) & 0xff; + tracksizebuffer[2] = (tracksize >> 8) & 0xff; + tracksizebuffer[3] = tracksize & 0xff; + + if (mem_fwrite(tracksizebuffer, 1, 4, midioutput) != 4) + { + return true; + } + + return false; +} + +#ifdef STANDALONE + +#include "m_misc.h" +#include "z_zone.h" + +int main(int argc, char *argv[]) +{ + MEMFILE *src, *dst; + byte *infile; + long infile_len; + void *outfile; + size_t outfile_len; + + if (argc != 3) + { + printf("Usage: %s \n", argv[0]); + exit(-1); + } + + Z_Init(); + + infile_len = M_ReadFile(argv[1], &infile); + + src = mem_fopen_read(infile, infile_len); + dst = mem_fopen_write(); + + if (mus2mid(src, dst)) + { + fprintf(stderr, "mus2mid() failed\n"); + exit(-1); + } + + // Write result to output file: + + mem_get_buf(dst, &outfile, &outfile_len); + + M_WriteFile(argv[2], outfile, outfile_len); + + return 0; +} + +#endif + diff --git a/doomgeneric/mus2mid.h b/doomgeneric/mus2mid.h new file mode 100644 index 0000000..d21516c --- /dev/null +++ b/doomgeneric/mus2mid.h @@ -0,0 +1,9 @@ +#ifndef MUS2MID_H +#define MUS2MID_H + +#include "doomtype.h" +#include "memio.h" + +boolean mus2mid(MEMFILE *musinput, MEMFILE *midioutput); + +#endif /* #ifndef MUS2MID_H */ \ No newline at end of file