yquake2remaster/src/client/sound/ogg.c
Yamagi 2ab4e5553a Prevent music from playing even if ogg_enabled is 0.
ogg_enabled doesn't prevent music from playing, it just toggles if the
ogg backend should be enabled or not. If the user does something like
`ogg_enable=0; snd_restart` everything is okay. If they just set
`ogg_enable=0` strange things happen because the backend stays
initialized and will play tracks as requested. Work around the by
shutting the backend down if `ogg_enable == 0 && ogg_started == true`.

Closes #583.
2020-06-30 14:46:48 +02:00

712 lines
16 KiB
C

/*
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
* USA.
*
* =======================================================================
*
* This file implements an interface to libvorbis for decoding
* OGG/Vorbis files. Strongly spoken this file isn't part of the sound
* system but part of the main client. It justs converts Vorbis streams
* into normal, raw Wave stream which are injected into the backends as
* if they were normal "raw" samples. At this moment only background
* music playback and in theory .cin movie file playback is supported.
*
* =======================================================================
*/
#ifndef _WIN32
#include <sys/time.h>
#endif
#include <errno.h>
#include "../header/client.h"
#include "header/local.h"
#include "header/vorbis.h"
#define STB_VORBIS_NO_PUSHDATA_API
#include "header/stb_vorbis.h"
static cvar_t *ogg_enabled; /* Backend is enabled */
static cvar_t *ogg_shuffle; /* Shuffle playback */
static cvar_t *ogg_ignoretrack0; /* Toggle track 0 playing */
static cvar_t *ogg_volume; /* Music volume. */
static int ogg_curfile; /* Index of currently played file. */
static int ogg_numbufs; /* Number of buffers for OpenAL */
static int ogg_numsamples; /* Number of sambles read from the current file */
static ogg_status_t ogg_status; /* Status indicator. */
static stb_vorbis *ogg_file; /* Ogg Vorbis file. */
static qboolean ogg_started; /* Initialization flag. */
enum { MAX_NUM_OGGTRACKS = 32 };
static char* ogg_tracks[MAX_NUM_OGGTRACKS];
static int ogg_maxfileindex;
enum GameType {
other, // incl. baseq2
xatrix,
rogue
};
struct {
qboolean saved;
int curfile;
int numsamples;
} ogg_saved_state;
// --------
/*
* The GOG version of Quake2 has the music tracks in music/TrackXX.ogg
* That music/ dir is next to baseq2/ (not in it) and contains Track02.ogg to Track21.ogg
* There
* - Track02 to Track11 correspond to Quake2 (baseq2) CD tracks 2-11
* - Track12 to Track21 correspond to the Ground Zero (rogue) addon's CD tracks 2-11
* - The "The Reckoning" (xatrix) addon also had 11 tracks, that were a mix of the ones
* from the main game (baseq2) and the rogue addon.
* See below how the CD track is mapped to GOG track numbers
*/
static int getMappedGOGtrack(int track, enum GameType gameType)
{
if(track <= 0)
return 0;
if(track == 1)
return 0; // 1 is illegal (=> data track on CD), 0 means "no track"
if(gameType == other)
return track;
if(gameType == rogue)
return track + 10;
// apparently it's xatrix => map the track to the corresponding TrackXX.ogg from GOG
switch(track)
{
case 2: return 9; // baseq2 9
case 3: return 13; // rogue 3
case 4: return 14; // rogue 4
case 5: return 7; // baseq2 7
case 6: return 16; // rogue 6
case 7: return 2; // baseq2 2
case 8: return 15; // rogue 5
case 9: return 3; // baseq2 3
case 10: return 4; // baseq2 4
case 11: return 18; // rogue 8
default:
return track;
}
}
/*
* Load list of Ogg Vorbis files in "music/".
*/
void
OGG_InitTrackList(void)
{
for (int i=0; i<MAX_NUM_OGGTRACKS; ++i)
{
if (ogg_tracks[i] != NULL)
{
free(ogg_tracks[i]);
ogg_tracks[i] = NULL;
}
}
ogg_maxfileindex = 0;
const char* potMusicDirs[3] = {0};
char gameMusicDir[MAX_QPATH] = {0}; // e.g. "xatrix/music"
cvar_t* gameCvar = Cvar_Get("game", "", CVAR_LATCH | CVAR_SERVERINFO);
if (gameCvar->string[0] == '\0' || strcmp(BASEDIRNAME, gameCvar->string) == 0)
{
// baseq2 => only 2 dirs in searchPath
potMusicDirs[0] = BASEDIRNAME "/music/"; // baseq2/music/
potMusicDirs[1] = "music/"; // global music dir (GOG)
potMusicDirs[2] = NULL;
}
else
{
// some other mod/addon
snprintf(gameMusicDir, MAX_QPATH, "%s/music/", gameCvar->string);
potMusicDirs[0] = gameMusicDir; // $mod/music/
potMusicDirs[1] = "music/"; // global music dir (GOG)
potMusicDirs[2] = BASEDIRNAME "/music/"; // baseq2/music/
}
enum GameType gameType = other;
if (strcmp("xatrix", gameCvar->string) == 0)
{
gameType = xatrix;
}
else if (strcmp("rogue", gameCvar->string) == 0)
{
gameType = rogue;
}
for (int potMusicDirIdx = 0; potMusicDirIdx < sizeof(potMusicDirs)/sizeof(potMusicDirs[0]); ++potMusicDirIdx)
{
const char* musicDir = potMusicDirs[potMusicDirIdx];
if (musicDir == NULL)
{
break;
}
for (const char* rawPath = FS_GetNextRawPath(NULL); rawPath != NULL; rawPath = FS_GetNextRawPath(rawPath))
{
char fullMusicPath[MAX_OSPATH] = {0};
snprintf(fullMusicPath, MAX_OSPATH, "%s/%s", rawPath, musicDir);
if(!Sys_IsDir(fullMusicPath))
{
continue;
}
char testFileName[MAX_OSPATH];
// the simple case (like before: $mod/music/02.ogg - 11.ogg or whatever)
snprintf(testFileName, MAX_OSPATH, "%s02.ogg", fullMusicPath);
if(Sys_IsFile(testFileName))
{
ogg_tracks[2] = strdup(testFileName);
for(int i=3; i<MAX_NUM_OGGTRACKS; ++i)
{
snprintf(testFileName, MAX_OSPATH, "%s%02i.ogg", fullMusicPath, i);
if(Sys_IsFile(testFileName))
{
ogg_tracks[i] = strdup(testFileName);
ogg_maxfileindex = i;
}
}
return;
}
// the GOG case: music/Track02.ogg to Track21.ogg
int gogTrack = getMappedGOGtrack(8, gameType);
snprintf(testFileName, MAX_OSPATH, "%sTrack%02i.ogg", fullMusicPath, gogTrack);
if(Sys_IsFile(testFileName))
{
for(int i=2; i<MAX_NUM_OGGTRACKS; ++i)
{
int gogTrack = getMappedGOGtrack(i, gameType);
snprintf(testFileName, MAX_OSPATH, "%sTrack%02i.ogg", fullMusicPath, gogTrack);
if(Sys_IsFile(testFileName))
{
ogg_tracks[i] = strdup(testFileName);
ogg_maxfileindex = i;
}
}
return;
}
}
}
// if tracks have been found above, we would've returned there
Com_Printf("No Ogg Vorbis music tracks have been found, so there will be no music.\n");
}
// --------
/*
* Play a portion of the currently opened file.
*/
void
static OGG_Read(void)
{
short samples[4096] = {0};
int read_samples = stb_vorbis_get_samples_short_interleaved(ogg_file, ogg_file->channels, samples,
sizeof(samples) / ogg_file->channels);
if (read_samples > 0)
{
ogg_numsamples += read_samples;
S_RawSamples(read_samples, ogg_file->sample_rate, ogg_file->channels, ogg_file->channels,
(byte *)samples, ogg_volume->value);
}
else
{
// We cannot call OGG_Stop() here. It flushes the OpenAL sample
// queue, thus about 12 seconds of music are lost. Instead we
// just set the OGG state to stop and open a new file. The new
// files content is added to the sample queue after the remaining
// samples from the old file.
stb_vorbis_close(ogg_file);
ogg_status = STOP;
ogg_numbufs = 0;
ogg_numsamples = 0;
OGG_PlayTrack(ogg_curfile);
}
}
/*
* Stream music.
*/
void
OGG_Stream(void)
{
if (!ogg_started)
{
return;
}
/* OGG playback was disabled since the last package frame.
Shutdown the backend to stop all playing tracks and to
prevend OGG_PLayTrack() from starting new ones. */
if (ogg_enabled->value != 1)
{
OGG_Shutdown();
return;
}
if (ogg_status == PLAY)
{
#ifdef USE_OPENAL
if (sound_started == SS_OAL)
{
/* Calculate the number of buffers used
for storing decoded OGG/Vorbis data.
We take the number of active buffers
and add 256. 256 are about 12 seconds
worth of sound, more than enough to
be resilent against underruns. */
if (ogg_numbufs == 0 || active_buffers < ogg_numbufs - 256)
{
ogg_numbufs = active_buffers + 256;
}
/* active_buffers are all active OpenAL buffers,
buffering normal sfx _and_ ogg/vorbis samples. */
while (active_buffers <= ogg_numbufs)
{
OGG_Read();
}
}
else /* using SDL */
#endif
{
if (sound_started == SS_SDL)
{
/* Read that number samples into the buffer, that
were played since the last call to this function.
This keeps the buffer at all times at an "optimal"
fill level. */
while (paintedtime + MAX_RAW_SAMPLES - 2048 > s_rawend)
{
OGG_Read();
}
}
}
}
}
// --------
/*
* play the ogg file that corresponds to the CD track with the given number
*/
void
OGG_PlayTrack(int trackNo)
{
if (sound_started == SS_NOT)
{
return; // sound is not initialized
}
if (ogg_started == false)
{
return;
}
// Track 0 means "stop music".
if(trackNo == 0)
{
if(ogg_ignoretrack0->value == 0)
{
OGG_Stop();
}
// Special case: If ogg_ignoretrack0 is 0 we stopped the music (see above)
// and ogg_curfile is still holding the last track played (track >1). So
// this triggers and we return. If ogg_ignoretrack is 1 we didn't stop the
// music, as soon as the tracks ends OGG_Read() starts it over. Until here
// everything's okay.
// But if ogg_ignoretrack0 is 1, the game was just restarted and a save game
// load send us trackNo 0, we would end up without music. Since we have no
// way to get the last track before trackNo 0 was set just fall through and
// shuffle a random track (see below).
if (ogg_curfile > 0)
{
return;
}
}
// Player has requested shuffle playback.
if((trackNo == 0) || ogg_shuffle->value)
{
if(ogg_maxfileindex >= 0)
{
trackNo = randk() % (ogg_maxfileindex+1);
int retries = 100;
while(ogg_tracks[trackNo] == NULL && retries-- > 0)
{
trackNo = randk() % (ogg_maxfileindex+1);
}
}
}
if(ogg_maxfileindex == -1)
{
return; // no ogg files at all, ignore this silently instead of printing warnings all the time
}
if ((trackNo < 2) || (trackNo > ogg_maxfileindex))
{
Com_Printf("OGG_PlayTrack: %d out of range.\n", trackNo);
return;
}
if(ogg_tracks[trackNo] == NULL)
{
Com_Printf("OGG_PlayTrack: Don't have a .ogg file for track %d\n", trackNo);
}
/* Check running music. */
if (ogg_status == PLAY)
{
if (ogg_curfile == trackNo)
{
return;
}
else
{
OGG_Stop();
}
}
if (ogg_tracks[trackNo] == NULL)
{
Com_Printf("OGG_PlayTrack: I don't have a file for track %d!\n", trackNo);
return;
}
/* Open ogg vorbis file. */
FILE* f = Q_fopen(ogg_tracks[trackNo], "rb");
if (f == NULL)
{
Com_Printf("OGG_PlayTrack: could not open file %s for track %d: %s.\n", ogg_tracks[trackNo], trackNo, strerror(errno));
ogg_tracks[trackNo] = NULL;
return;
}
int res = 0;
ogg_file = stb_vorbis_open_file(f, true, &res, NULL);
if (res != 0)
{
Com_Printf("OGG_PlayTrack: '%s' is not a valid Ogg Vorbis file (error %i).\n", ogg_tracks[trackNo], res);
fclose(f);
return;
}
/* Play file. */
ogg_curfile = trackNo;
ogg_numsamples = 0;
ogg_status = PLAY;
}
// ----
/*
* List Ogg Vorbis files and print current playback state.
*/
static void
OGG_Info(void)
{
Com_Printf("Tracks:\n");
int numFiles = 0;
for (int i = 2; i <= ogg_maxfileindex; i++)
{
if(ogg_tracks[i])
{
Com_Printf(" - %02d %s\n", i, ogg_tracks[i]);
++numFiles;
}
else
{
Com_Printf(" - %02d <none>\n", i);
}
}
Com_Printf("Total: %d Ogg/Vorbis files.\n", ogg_maxfileindex+1);
switch (ogg_status)
{
case PLAY:
Com_Printf("State: Playing file %d (%s) at %i samples.\n",
ogg_curfile, ogg_tracks[ogg_curfile], stb_vorbis_get_sample_offset(ogg_file));
break;
case PAUSE:
Com_Printf("State: Paused file %d (%s) at %i samples.\n",
ogg_curfile, ogg_tracks[ogg_curfile], stb_vorbis_get_sample_offset(ogg_file));
break;
case STOP:
if (ogg_curfile == -1)
{
Com_Printf("State: Stopped.\n");
}
else
{
Com_Printf("State: Stopped file %d (%s).\n", ogg_curfile, ogg_tracks[ogg_curfile]);
}
break;
}
}
/*
* Stop playing the current file.
*/
void
OGG_Stop(void)
{
if (ogg_status == STOP)
{
return;
}
#ifdef USE_OPENAL
if (sound_started == SS_OAL)
{
AL_UnqueueRawSamples();
}
#endif
stb_vorbis_close(ogg_file);
ogg_status = STOP;
ogg_numbufs = 0;
}
/*
* Pause or resume playback.
*/
static void
OGG_TogglePlayback(void)
{
if (ogg_status == PLAY)
{
ogg_status = PAUSE;
ogg_numbufs = 0;
#ifdef USE_OPENAL
if (sound_started == SS_OAL)
{
AL_UnqueueRawSamples();
}
#endif
}
else if (ogg_status == PAUSE)
{
ogg_status = PLAY;
}
}
/*
* Prints a help message for the 'ogg' cmd.
*/
void
OGG_HelpMsg(void)
{
Com_Printf("Unknown sub command %s\n\n", Cmd_Argv(1));
Com_Printf("Commands:\n");
Com_Printf(" - info: Print information about playback state and tracks\n");
Com_Printf(" - play <track>: Play track number <track>\n");
Com_Printf(" - stop: Stop playback\n");
Com_Printf(" - toggle: Toggle pause\n");
}
/*
* The 'ogg' cmd. Gives some control and information about the playback state.
*/
void
OGG_Cmd(void)
{
if (Cmd_Argc() < 2)
{
OGG_HelpMsg();
return;
}
if (Q_stricmp(Cmd_Argv(1), "info") == 0)
{
OGG_Info();
}
else if (Q_stricmp(Cmd_Argv(1), "play") == 0)
{
if (Cmd_Argc() != 3)
{
Com_Printf("ogg play <track> : Play <track>");
return;
}
int track = (int)strtol(Cmd_Argv(2), NULL, 10);
if (track < 2 || track > ogg_maxfileindex)
{
Com_Printf("invalid track %s, must be an number between 2 and %d\n", Cmd_Argv(1), ogg_maxfileindex);
return;
}
else
{
OGG_PlayTrack(track);
}
}
else if (Q_stricmp(Cmd_Argv(1), "stop") == 0)
{
OGG_Stop();
}
else if (Q_stricmp(Cmd_Argv(1), "toggle") == 0)
{
OGG_TogglePlayback();
}
else
{
OGG_HelpMsg();
}
}
/*
* Saves the current state of the subsystem.
*/
void
OGG_SaveState(void)
{
if (ogg_status != PLAY)
{
ogg_saved_state.saved = false;
return;
}
ogg_saved_state.saved = true;
ogg_saved_state.curfile = ogg_curfile;
ogg_saved_state.numsamples = ogg_numsamples;
}
/*
* Recover the previously saved state.
*/
void
OGG_RecoverState(void)
{
if (!ogg_saved_state.saved)
{
return;
}
// Mkay, ultra evil hack to recover the state in case of
// shuffled playback. OGG_PlayTrack() does the shuffeling,
// so switch it of before and enable after state recovery.
int shuffle_state = ogg_shuffle->value;
Cvar_SetValue("ogg_shuffle", 0);
OGG_PlayTrack(ogg_saved_state.curfile);
stb_vorbis_seek_frame(ogg_file, ogg_saved_state.numsamples);
ogg_numsamples = ogg_saved_state.numsamples;
Cvar_SetValue("ogg_shuffle", shuffle_state);
}
// --------
/*
* Initialize the Ogg Vorbis subsystem.
*/
void
OGG_Init(void)
{
// Cvars
ogg_shuffle = Cvar_Get("ogg_shuffle", "0", CVAR_ARCHIVE);
ogg_ignoretrack0 = Cvar_Get("ogg_ignoretrack0", "0", CVAR_ARCHIVE);
ogg_volume = Cvar_Get("ogg_volume", "0.7", CVAR_ARCHIVE);
ogg_enabled = Cvar_Get("ogg_enable", "1", CVAR_ARCHIVE);
if (ogg_enabled->value != 1)
{
return;
}
// Commands
Cmd_AddCommand("ogg", OGG_Cmd);
// Global variables
ogg_curfile = -1;
ogg_numsamples = 0;
ogg_status = STOP;
ogg_started = true;
}
/*
* Shutdown the Ogg Vorbis subsystem.
*/
void
OGG_Shutdown(void)
{
if (!ogg_started)
{
return;
}
// Music must be stopped.
OGG_Stop();
// Free file list.
for(int i=0; i<MAX_NUM_OGGTRACKS; ++i)
{
if(ogg_tracks[i] != NULL)
{
free(ogg_tracks[i]);
ogg_tracks[i] = NULL;
}
}
ogg_maxfileindex = 0;
// Remove console commands
Cmd_RemoveCommand("ogg");
ogg_started = false;
}