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)
// Enables sound output
-#undef FEATURE_SOUND
+//#undef FEATURE_SOUND
#endif /* #ifndef DOOM_FEATURES_H */
* public functions *
*---------------------------------------------------------------------*/
-void I_InitTimidityConfig(void)
-{
-}
-
/*---------------------------------------------------------------------*
* eof *
*---------------------------------------------------------------------*/
#include <stdio.h>
#ifdef ORIGCODE
-#include "SDL.h"
-#include "SDL_cdrom.h"
+#include "SDL2/SDL.h"
+#include "SDL2/SDL_cdrom.h"
#endif
#include "doomtype.h"
--- /dev/null
+//
+// 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 "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 <output filename>
+ //
+ // Read all MIDI files from loaded WAD files, dump an example substitution
+ // music config file to the specified filename and quit.
+ //
+
+ i = M_CheckParmWithArgs("-dumpsubstconfig", 1);
+
+ if (i > 0)
+ {
+ DumpSubstituteConfig(myargv[i + 1]);
+ }
+
+ // If SDL_mixer is not initialized, we have to initialize it
+ // and have the responsibility to shut it down later on.
+
+ 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,
+};
+
--- /dev/null
+//
+// 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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <ctype.h>
+#include "SDL2/SDL.h"
+#include "SDL2/SDL_mixer.h"
+
+#ifdef HAVE_LIBSAMPLERATE
+#include <samplerate.h>
+#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; i<length; ++i)
+ {
+ // Unclear whether 128 should be interpreted as "zero" or whether a
+ // symmetrical range should be assumed. The following assumes a
+ // symmetrical range.
+ src_data.data_in[i] = data[i] / 127.5 - 1;
+ }
+
+ // Do the sound conversion
+
+ retn = src_simple(&src_data, SRC_ConversionMode(), 1);
+ assert(retn == 0);
+
+ // Allocate the new chunk.
+
+ alen = src_data.output_frames_gen * 4;
+
+ chunk = AllocateSound(sfxinfo, src_data.output_frames_gen * 4);
+
+ if (chunk == NULL)
+ {
+ return false;
+ }
+
+ expanded = (int16_t *) chunk->abuf;
+
+ // Convert the result back into 16-bit integers.
+
+ for (i=0; i<src_data.output_frames_gen; ++i)
+ {
+ // libsamplerate does not limit itself to the -1.0 .. 1.0 range on
+ // output, so a multiplier less than INT16_MAX (32767) is required
+ // to avoid overflows or clipping. However, the smaller the
+ // multiplier, the quieter the sound effects get, and the more you
+ // have to turn down the music to keep it in balance.
+
+ // 22265 is the largest multiplier that can be used to resample all
+ // of the Vanilla DOOM sound effects to 48 kHz without clipping
+ // using SRC_SINC_BEST_QUALITY. It is close enough (only slightly
+ // too conservative) for SRC_SINC_MEDIUM_QUALITY and
+ // SRC_SINC_FASTEST. PWADs with interestingly different sound
+ // effects or target rates other than 48 kHz might still result in
+ // clipping--I don't know if there's a limit to it.
+
+ // As the number of clipped samples increases, the signal is
+ // gradually overtaken by noise, with the loudest parts going first.
+ // However, a moderate amount of clipping is often tolerated in the
+ // quest for the loudest possible sound overall. The results of
+ // using INT16_MAX as the multiplier are not all that bad, but
+ // artifacts are noticeable during the loudest parts.
+
+ float cvtval_f =
+ src_data.data_out[i] * libsamplerate_scale * INT16_MAX;
+ int32_t cvtval_i = cvtval_f + (cvtval_f < 0 ? -0.5 : 0.5);
+
+ // Asymmetrical sound worries me, so we won't use -32768.
+ if (cvtval_i < -INT16_MAX)
+ {
+ cvtval_i = -INT16_MAX;
+ ++clipped;
+ }
+ else if (cvtval_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<expanded_length; ++i)
+ {
+ Sint16 sample;
+ int src;
+
+ src = (i * expand_ratio) >> 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; i<expanded_length * 2; ++i)
+ {
+ expanded[i] = (Sint16) (alpha * expanded[i]
+ + (1 - alpha) * expanded[i-2]);
+ }
+ }
+#endif /* #ifdef LOW_PASS_FILTER */
+
+ return true;
+}
+
+// Load and convert a sound effect
+// Returns true if successful
+
+static boolean CacheSFX(sfxinfo_t *sfxinfo)
+{
+ int lumpnum;
+ unsigned int lumplen;
+ int samplerate;
+ unsigned int length;
+ byte *data;
+
+ // need to load the sound
+
+ lumpnum = sfxinfo->lumpnum;
+ 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; i<num_sounds; ++i)
+ {
+ if ((i % 6) == 0)
+ {
+ printf(".");
+ fflush(stdout);
+ }
+
+ GetSfxLumpName(&sounds[i], namebuf, sizeof(namebuf));
+
+ sounds[i].lumpnum = W_CheckNumForName(namebuf);
+
+ if (sounds[i].lumpnum != -1)
+ {
+ CacheSFX(&sounds[i]);
+ }
+ }
+
+ printf("\n");
+}
+
+#else
+
+static void I_SDL_PrecacheSounds(sfxinfo_t *sounds, int num_sounds)
+{
+ // no-op
+}
+
+#endif
+
+// Load a SFX chunk into memory and ensure that it is locked.
+
+static boolean LockSound(sfxinfo_t *sfxinfo)
+{
+ // If the sound isn't loaded, load it now
+
+ if (sfxinfo->driver_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<NUM_CHANNELS; ++i)
+ {
+ if (channels_playing[i] && !I_SDL_SoundIsPlaying(i))
+ {
+ // Sound has finished playing on this channel,
+ // but sound data has not been released to cache
+
+ ReleaseSoundOnChannel(i);
+ }
+ }
+}
+
+static void I_SDL_ShutdownSound(void)
+{
+ if (!sound_initialized)
+ {
+ return;
+ }
+
+ Mix_CloseAudio();
+ SDL_QuitSubSystem(SDL_INIT_AUDIO);
+
+ sound_initialized = false;
+}
+
+// Calculate slice size, based on snd_maxslicetime_ms.
+// The result must be a power of two.
+
+static int GetSliceSize(void)
+{
+ int limit;
+ int n;
+
+ limit = (snd_samplerate * snd_maxslicetime_ms) / 1000;
+
+ // Try all powers of two, not exceeding the limit.
+
+ for (n=0;; ++n)
+ {
+ // 2^n <= limit < 2^n+1 ?
+
+ if ((1 << (n + 1)) > 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; i<NUM_CHANNELS; ++i)
+ {
+ channels_playing[i] = NULL;
+ }
+
+ if (SDL_Init(SDL_INIT_AUDIO) < 0)
+ {
+ fprintf(stderr, "Unable to set up sound.\n");
+ return false;
+ }
+
+ if (Mix_OpenAudio(snd_samplerate, AUDIO_S16SYS, 2, GetSliceSize()) < 0)
+ {
+ fprintf(stderr, "Error initialising SDL_mixer: %s\n", Mix_GetError());
+ return false;
+ }
+
+ ExpandSoundData = ExpandSoundData_SDL;
+
+ Mix_QuerySpec(&mixer_freq, &mixer_format, &mixer_channels);
+
+#ifdef HAVE_LIBSAMPLERATE
+ if (use_libsamplerate != 0)
+ {
+ if (SRC_ConversionMode() < 0)
+ {
+ I_Error("I_SDL_InitSound: Invalid value for use_libsamplerate: %i",
+ use_libsamplerate);
+ }
+
+ ExpandSoundData = ExpandSoundData_SRC;
+ }
+#else
+ if (use_libsamplerate != 0)
+ {
+ fprintf(stderr, "I_SDL_InitSound: use_libsamplerate=%i, but "
+ "libsamplerate support not compiled in.\n",
+ use_libsamplerate);
+ }
+#endif
+
+ // SDL_mixer version 1.2.8 and earlier has a bug in the Mix_SetPanning
+ // function that can cause the game to lock up. If we're using an old
+ // version, we need to apply a workaround. But the workaround has its
+ // own drawbacks ...
+
+ {
+ const SDL_version *mixer_version;
+ int v;
+
+ mixer_version = Mix_Linked_Version();
+ v = SDL_VERSIONNUM(mixer_version->major,
+ 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,
+};
+
#include <stdio.h>
#include <stdlib.h>
-#ifdef ORIGCODE
-#include "SDL_mixer.h"
+#ifdef FEATURE_SOUND
+#include "SDL2/SDL_mixer.h"
#endif
#include "config.h"
// 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,
};
static music_module_t *music_modules[] =
{
-#ifdef FEATURE_SOUND
+ #ifdef FEATURE_SOUND
&music_sdl_module,
- &music_opl_module,
-#endif
+ #endif
NULL,
};
{
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?
return;
}
}
- }
+ }*/
}
//
InitMusicModule();
}
}
+
}
void I_ShutdownSound(void)
void I_InitMusic(void)
{
+ if(music_module != NULL)
+ {
+ music_module->Init();
+ }
}
void I_ShutdownMusic(void)
{
return false;
}
+
}
void I_BindSoundVariables(void)
{
-#ifdef ORIGCODE
extern int use_libsamplerate;
extern float libsamplerate_scale;
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);
// 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
}
#ifndef __I_SWAP__
#define __I_SWAP__
-#ifdef ORIGCODE
-#include "SDL_endian.h"
+#include <SDL2/SDL_endian.h>
// Endianess handling.
// WAD files are stored little endian.
#define SYS_BIG_ENDIAN
#endif
-#else
-
-#define SHORT(x) ((signed short) (x))
-#define LONG(x) ((signed int) (x))
+// cosmito from lsdldoom
+#define doom_swap_s(x) \
+ ((short int)((((unsigned short int)(x) & 0x00ff) << 8) | \
+ (((unsigned short int)(x) & 0xff00) >> 8)))
-#define SYS_LITTLE_ENDIAN
+#if ( SDL_BYTEORDER == SDL_BIG_ENDIAN )
+#define doom_wtohs(x) doom_swap_s(x)
+#else
+#define doom_wtohs(x) (short int)(x)
#endif
+
#endif
#include "doomgeneric.h"
#include <stdarg.h>
+#include <stdio.h>
+#include <SDL2/SDL.h>
//#include <sys/time.h>
//#include <unistd.h>
ticks -= basetime;
- return (ticks * TICRATE) / 1000;
+ return (ticks * TICRATE) / 1000;
}
{
// 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);
+ }
}
--- /dev/null
+//
+// 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 <stdio.h>
+
+#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<NUM_CHANNELS; ++i)
+ {
+ if (channel_map[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<NUM_CHANNELS; ++channel)
+ {
+ channel_map[channel] = -1;
+ }
+
+ // Grab the header
+
+ if (!ReadMusHeader(musinput, &musfileheader))
+ {
+ return true;
+ }
+
+// [crispy] enable MUS format header check
+#define CHECK_MUS_HEADER
+#ifdef CHECK_MUS_HEADER
+ // Check MUS header
+ if (musfileheader.id[0] != 'M'
+ || musfileheader.id[1] != 'U'
+ || musfileheader.id[2] != 'S'
+ || musfileheader.id[3] != 0x1A)
+ {
+ return true;
+ }
+#endif
+
+ // Seek to where the data is held
+ if (mem_fseek(musinput, (long)musfileheader.scorestart,
+ MEM_SEEK_SET) != 0)
+ {
+ return true;
+ }
+
+ // So, we can assume the MUS file is faintly legit. Let's start
+ // writing MIDI data...
+
+ mem_fwrite(midiheader, 1, sizeof(midiheader), midioutput);
+ tracksize = 0;
+
+ // Now, process the MUS file:
+ while (!hitscoreend)
+ {
+ // Handle a block of events:
+
+ while (!hitscoreend)
+ {
+ // Fetch channel number and event code:
+
+ if (mem_fread(&eventdescriptor, 1, 1, musinput) != 1)
+ {
+ return true;
+ }
+
+ channel = GetMIDIChannel(eventdescriptor & 0x0F, midioutput);
+ event = eventdescriptor & 0x70;
+
+ switch (event)
+ {
+ case mus_releasekey:
+ if (mem_fread(&key, 1, 1, musinput) != 1)
+ {
+ return true;
+ }
+
+ if (WriteReleaseKey(channel, key, midioutput))
+ {
+ return true;
+ }
+
+ break;
+
+ case mus_presskey:
+ if (mem_fread(&key, 1, 1, musinput) != 1)
+ {
+ return true;
+ }
+
+ if (key & 0x80)
+ {
+ if (mem_fread(&channelvelocities[channel], 1, 1, musinput) != 1)
+ {
+ return true;
+ }
+
+ channelvelocities[channel] &= 0x7F;
+ }
+
+ if (WritePressKey(channel, key,
+ channelvelocities[channel], midioutput))
+ {
+ return true;
+ }
+
+ break;
+
+ case mus_pitchwheel:
+ if (mem_fread(&key, 1, 1, musinput) != 1)
+ {
+ break;
+ }
+ if (WritePitchWheel(channel, (short)(key * 64), midioutput))
+ {
+ return true;
+ }
+
+ break;
+
+ case mus_systemevent:
+ if (mem_fread(&controllernumber, 1, 1, musinput) != 1)
+ {
+ return true;
+ }
+ if (controllernumber < 10 || controllernumber > 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 <musfile> <midfile>\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
+
--- /dev/null
+#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