diff options
author | Simon Howard | 2014-04-27 00:58:59 -0400 |
---|---|---|
committer | Simon Howard | 2014-04-27 00:58:59 -0400 |
commit | 797a9a563b2e90849bc6eb79169a9381896eeb15 (patch) | |
tree | 4d23ec4eec1ceb392b482d268358fa8a219f0bca | |
parent | 90f7206384d54cee5cee40f3e7c64b7a7c43cb26 (diff) | |
download | chocolate-doom-797a9a563b2e90849bc6eb79169a9381896eeb15.tar.gz chocolate-doom-797a9a563b2e90849bc6eb79169a9381896eeb15.tar.bz2 chocolate-doom-797a9a563b2e90849bc6eb79169a9381896eeb15.zip |
music: Add loop point Ogg/Flac metadata support.
ZDoom has defined a format for Vorbis metadata comments named
LOOP_START and LOOP_END that allow the start and end points to be
defined in .ogg and .flac files for looping music. Add support for
these (they are used in Brandon Blume's SC-55 recordings).
-rw-r--r-- | src/doom/s_sound.c | 2 | ||||
-rw-r--r-- | src/heretic/s_sound.c | 2 | ||||
-rw-r--r-- | src/hexen/s_sound.c | 2 | ||||
-rw-r--r-- | src/i_sdlmusic.c | 422 | ||||
-rw-r--r-- | src/i_sound.c | 5 | ||||
-rw-r--r-- | src/i_sound.h | 4 | ||||
-rw-r--r-- | src/i_swap.h | 2 |
7 files changed, 434 insertions, 5 deletions
diff --git a/src/doom/s_sound.c b/src/doom/s_sound.c index dc0473a3..a97c34dc 100644 --- a/src/doom/s_sound.c +++ b/src/doom/s_sound.c @@ -518,6 +518,8 @@ void S_UpdateSounds(mobj_t *listener) sfxinfo_t* sfx; channel_t* c; + I_UpdateSound(); + for (cnum=0; cnum<snd_channels; cnum++) { c = &channels[cnum]; diff --git a/src/heretic/s_sound.c b/src/heretic/s_sound.c index 79e2d353..060e68bf 100644 --- a/src/heretic/s_sound.c +++ b/src/heretic/s_sound.c @@ -452,6 +452,8 @@ void S_UpdateSounds(mobj_t * listener) int absx; int absy; + I_UpdateSound(); + listener = GetSoundListener(); if (snd_MaxVolume == 0) { diff --git a/src/hexen/s_sound.c b/src/hexen/s_sound.c index 71944f11..400357f8 100644 --- a/src/hexen/s_sound.c +++ b/src/hexen/s_sound.c @@ -706,6 +706,8 @@ void S_UpdateSounds(mobj_t * listener) int absx; int absy; + I_UpdateSound(); + // If we are looping a CD track, we need to check if it has // finished playing and needs to restart. if (cdmusic && ShouldRestartCDTrack()) diff --git a/src/i_sdlmusic.c b/src/i_sdlmusic.c index 4f540e8d..143f8eb2 100644 --- a/src/i_sdlmusic.c +++ b/src/i_sdlmusic.c @@ -40,6 +40,7 @@ #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" @@ -51,6 +52,24 @@ #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 @@ -67,6 +86,14 @@ typedef struct 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; @@ -94,6 +121,298 @@ 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 (;;) + { + // 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]; + + long 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). @@ -532,8 +851,14 @@ static boolean SDLIsInitialized(void) return Mix_QuerySpec(&freq, &format, &channels) != 0; } -// Initialize music subsystem +// 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; @@ -614,6 +939,9 @@ static boolean I_SDL_InitMusic(void) 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) { @@ -658,7 +986,6 @@ static void I_SDL_SetMusicVolume(int volume) static void I_SDL_PlaySong(void *handle, boolean looping) { - Mix_Music *music = (Mix_Music *) handle; int loops; if (!music_initialized) @@ -671,6 +998,9 @@ static void I_SDL_PlaySong(void *handle, boolean looping) return; } + current_track_music = (Mix_Music *) handle; + current_track_loop = looping; + if (looping) { loops = -1; @@ -680,7 +1010,17 @@ static void I_SDL_PlaySong(void *handle, boolean looping) loops = 1; } - Mix_PlayMusic(music, loops); + // 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) @@ -715,6 +1055,8 @@ static void I_SDL_StopSong(void) } Mix_HaltMusic(); + playing_substitute = false; + current_track_music = NULL; } static void I_SDL_UnRegisterSong(void *handle) @@ -777,6 +1119,8 @@ static void *I_SDL_RegisterSong(void *data, int len) return NULL; } + playing_substitute = false; + // See if we're substituting this MUS for a high-quality replacement. filename = GetSubstituteMusicFile(data, len); @@ -793,6 +1137,10 @@ static void *I_SDL_RegisterSong(void *data, int len) } 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; } } @@ -852,6 +1200,72 @@ static boolean I_SDL_MusicIsPlaying(void) 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, @@ -876,6 +1290,6 @@ music_module_t music_sdl_module = I_SDL_PlaySong, I_SDL_StopSong, I_SDL_MusicIsPlaying, + I_SDL_PollMusic, }; - diff --git a/src/i_sound.c b/src/i_sound.c index d71dacd7..1b61f06f 100644 --- a/src/i_sound.c +++ b/src/i_sound.c @@ -278,6 +278,11 @@ void I_UpdateSound(void) { sound_module->Update(); } + + if (music_module != NULL) + { + music_module->Poll(); + } } static void CheckVolumeSeparation(int *vol, int *sep) diff --git a/src/i_sound.h b/src/i_sound.h index 8990e9c1..e1c6998d 100644 --- a/src/i_sound.h +++ b/src/i_sound.h @@ -216,6 +216,10 @@ typedef struct // Query if music is playing. boolean (*MusicIsPlaying)(void); + + // Invoked periodically to poll. + + void (*Poll)(void); } music_module_t; void I_InitMusic(void); diff --git a/src/i_swap.h b/src/i_swap.h index 065037a4..c1b11040 100644 --- a/src/i_swap.h +++ b/src/i_swap.h @@ -39,7 +39,7 @@ // of the macros in the original source and some code relies on it. #define SHORT(x) ((signed short) SDL_SwapLE16(x)) -#define LONG(x) ((signed long) SDL_SwapLE32(x)) +#define LONG(x) ((signed int) SDL_SwapLE32(x)) // Defines for checking the endianness of the system. |