shithub: choc

Download patch

ref: 797a9a563b2e90849bc6eb79169a9381896eeb15
parent: 90f7206384d54cee5cee40f3e7c64b7a7c43cb26
author: Simon Howard <[email protected]>
date: Sat Apr 26 20:58:59 EDT 2014

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).

--- a/src/doom/s_sound.c
+++ b/src/doom/s_sound.c
@@ -518,6 +518,8 @@
     sfxinfo_t*        sfx;
     channel_t*        c;
 
+    I_UpdateSound();
+
     for (cnum=0; cnum<snd_channels; cnum++)
     {
         c = &channels[cnum];
--- a/src/heretic/s_sound.c
+++ b/src/heretic/s_sound.c
@@ -452,6 +452,8 @@
     int absx;
     int absy;
 
+    I_UpdateSound();
+
     listener = GetSoundListener();
     if (snd_MaxVolume == 0)
     {
--- a/src/hexen/s_sound.c
+++ b/src/hexen/s_sound.c
@@ -706,6 +706,8 @@
     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())
--- 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 @@
     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 @@
 
 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 @@
     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 @@
         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_PlaySong(void *handle, boolean looping)
 {
-    Mix_Music *music = (Mix_Music *) handle;
     int loops;
 
     if (!music_initialized)
@@ -671,6 +998,9 @@
         return;
     }
 
+    current_track_music = (Mix_Music *) handle;
+    current_track_loop = looping;
+
     if (looping)
     {
         loops = -1;
@@ -680,7 +1010,17 @@
         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 @@
     }
 
     Mix_HaltMusic();
+    playing_substitute = false;
+    current_track_music = NULL;
 }
 
 static void I_SDL_UnRegisterSong(void *handle)
@@ -777,6 +1119,8 @@
         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 @@
         }
         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 @@
     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 @@
     I_SDL_PlaySong,
     I_SDL_StopSong,
     I_SDL_MusicIsPlaying,
+    I_SDL_PollMusic,
 };
-
 
--- a/src/i_sound.c
+++ b/src/i_sound.c
@@ -278,6 +278,11 @@
     {
         sound_module->Update();
     }
+
+    if (music_module != NULL)
+    {
+        music_module->Poll();
+    }
 }
 
 static void CheckVolumeSeparation(int *vol, int *sep)
--- a/src/i_sound.h
+++ b/src/i_sound.h
@@ -216,6 +216,10 @@
     // Query if music is playing.
 
     boolean (*MusicIsPlaying)(void);
+
+    // Invoked periodically to poll.
+
+    void (*Poll)(void);
 } music_module_t;
 
 void I_InitMusic(void);
--- 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.