446 lines
15 KiB
C++
446 lines
15 KiB
C++
/*
|
|
* Background music handling for Quakespasm (adapted from uHexen2)
|
|
* Handles streaming music as raw sound samples and runs the midi driver
|
|
*
|
|
* Copyright (C) 1999-2005 Id Software, Inc.
|
|
* Copyright (C) 2010-2018 O.Sezer <[email protected]>
|
|
*
|
|
* 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.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*
|
|
*/
|
|
|
|
#include "quakedef.hpp"
|
|
#include "snd_codec.hpp"
|
|
#include "bgmusic.hpp"
|
|
|
|
#include <vector>
|
|
|
|
#define MUSIC_DIRNAME "music"
|
|
|
|
|
|
#define CDRIP_TYPES (CODECTYPE_VORBIS | CODECTYPE_MP3 | CODECTYPE_FLAC | CODECTYPE_WAV | CODECTYPE_OPUS)
|
|
#define CDRIPTYPE(x) (((x) & CDRIP_TYPES) != 0)
|
|
|
|
namespace music {
|
|
bool bgmloop;
|
|
convar bgm_extmusic{"bgm_extmusic", "1", {.archive = true}};
|
|
|
|
namespace {
|
|
snd_stream_t *bgmstream = nullptr;
|
|
bool no_extmusic = false;
|
|
float old_volume = -1.0f;
|
|
|
|
constexpr unsigned int ANY_CODECTYPE = 0xFFFFFFFF;
|
|
|
|
|
|
enum class player_kind {
|
|
none = -1,
|
|
midi_drv = 1,
|
|
streamer
|
|
};
|
|
|
|
struct handler {
|
|
unsigned int type; /* 1U << n (see snd_codec.h) */
|
|
player_kind player; /* Enumerated bgm player type */
|
|
int is_available; /* -1 means not present */
|
|
const char *ext; /* Expected file extension */
|
|
const char *dir; /* Where to look for music file */
|
|
};
|
|
|
|
std::vector<handler> active_handlers{};
|
|
|
|
std::array wanted_handlers =
|
|
{
|
|
handler{
|
|
.type = CODECTYPE_VORBIS, .player = player_kind::streamer, .is_available = -1, .ext = "ogg",
|
|
.dir = MUSIC_DIRNAME
|
|
},
|
|
handler{
|
|
.type = CODECTYPE_OPUS, .player = player_kind::streamer, .is_available = -1, .ext = "opus",
|
|
.dir = MUSIC_DIRNAME
|
|
},
|
|
handler{
|
|
.type = CODECTYPE_MP3, .player = player_kind::streamer, .is_available = -1, .ext = "mp3",
|
|
.dir = MUSIC_DIRNAME
|
|
},
|
|
handler{
|
|
.type = CODECTYPE_FLAC, .player = player_kind::streamer, .is_available = -1, .ext = "flac",
|
|
.dir = MUSIC_DIRNAME
|
|
},
|
|
handler{
|
|
.type = CODECTYPE_WAV, .player = player_kind::streamer, .is_available = -1, .ext = "wav",
|
|
.dir = MUSIC_DIRNAME
|
|
},
|
|
handler{
|
|
.type = CODECTYPE_MOD, .player = player_kind::streamer, .is_available = -1, .ext = "it", .dir = MUSIC_DIRNAME
|
|
},
|
|
handler{
|
|
.type = CODECTYPE_MOD, .player = player_kind::streamer, .is_available = -1, .ext = "s3m",
|
|
.dir = MUSIC_DIRNAME
|
|
},
|
|
handler{
|
|
.type = CODECTYPE_MOD, .player = player_kind::streamer, .is_available = -1, .ext = "xm", .dir = MUSIC_DIRNAME
|
|
},
|
|
handler{
|
|
.type = CODECTYPE_MOD, .player = player_kind::streamer, .is_available = -1, .ext = "mod",
|
|
.dir = MUSIC_DIRNAME
|
|
},
|
|
handler{
|
|
.type = CODECTYPE_UMX, .player = player_kind::streamer, .is_available = -1, .ext = "umx",
|
|
.dir = MUSIC_DIRNAME
|
|
},
|
|
handler{.type = CODECTYPE_NONE, .player = player_kind::none, .is_available = -1, .ext = nullptr, .dir = nullptr}
|
|
};
|
|
|
|
void _play() {
|
|
if (command::argc() == 2) {
|
|
play(*command::argv(1));
|
|
} else {
|
|
console::info("music <musicfile>\n");
|
|
}
|
|
}
|
|
|
|
void _pause() {
|
|
pause();
|
|
}
|
|
|
|
void _resume() {
|
|
resume();
|
|
}
|
|
|
|
void _loop() {
|
|
if (command::argc() == 2) {
|
|
if (q_strcasecmp(command::argv(1)->c_str(), "0") == 0 ||
|
|
q_strcasecmp(command::argv(1)->c_str(), "off") == 0)
|
|
bgmloop = false;
|
|
else if (q_strcasecmp(command::argv(1)->c_str(), "1") == 0 ||
|
|
q_strcasecmp(command::argv(1)->c_str(), "on") == 0)
|
|
bgmloop = true;
|
|
else if (q_strcasecmp(command::argv(1)->c_str(), "toggle") == 0)
|
|
bgmloop = !bgmloop;
|
|
|
|
if (bgmstream) bgmstream->loop = bgmloop;
|
|
}
|
|
|
|
if (bgmloop)
|
|
console::info("Music will be looped\n");
|
|
else
|
|
console::info("Music will not be looped\n");
|
|
}
|
|
|
|
void _stop() {
|
|
stop();
|
|
}
|
|
|
|
void _jump() {
|
|
if (command::argc() != 2) {
|
|
console::info("music_jump <ordernum>\n");
|
|
} else if (bgmstream) {
|
|
S_CodecJumpToOrder(bgmstream, static_cast<int>(std::strtol(command::argv(1)->c_str(), nullptr, 10)));
|
|
}
|
|
}
|
|
|
|
void play_noext(const std::string &filename, const unsigned int allowed_types) {
|
|
char tmp[MAX_QPATH];
|
|
|
|
for (auto &handler: active_handlers) {
|
|
if (!(handler.type & allowed_types)) {
|
|
continue;
|
|
}
|
|
if (!handler.is_available) {
|
|
continue;
|
|
}
|
|
q_snprintf(tmp, sizeof(tmp), "%s/%s.%s",
|
|
handler.dir, filename.c_str(), handler.ext);
|
|
switch (handler.player) {
|
|
case player_kind::midi_drv:
|
|
/* not supported in quake */
|
|
break;
|
|
case player_kind::streamer:
|
|
bgmstream = S_CodecOpenStreamType(tmp, handler.type, bgmloop);
|
|
if (bgmstream)
|
|
return; /* success */
|
|
break;
|
|
case player_kind::none:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
console::info("Couldn't handle music file %s\n", filename.c_str());
|
|
}
|
|
|
|
void update_stream() {
|
|
bool did_rewind = false;
|
|
int res; /* Number of bytes read. */
|
|
int bufferSamples;
|
|
int fileSamples;
|
|
int fileBytes;
|
|
byte raw[16384];
|
|
|
|
if (bgmstream->status != STREAM_PLAY) {
|
|
return;
|
|
}
|
|
|
|
/* don't bother playing anything if musicvolume is 0 */
|
|
if (bgmvolume.value <= 0) {
|
|
return;
|
|
}
|
|
|
|
/* see how many samples should be copied into the raw buffer */
|
|
if (s_rawend < paintedtime) {
|
|
s_rawend = paintedtime;
|
|
}
|
|
|
|
while (s_rawend < paintedtime + MAX_RAW_SAMPLES) {
|
|
bufferSamples = MAX_RAW_SAMPLES - (s_rawend - paintedtime);
|
|
|
|
/* decide how much data needs to be read from the file */
|
|
fileSamples = bufferSamples * bgmstream->info.rate / shm->speed;
|
|
if (!fileSamples)
|
|
return;
|
|
|
|
/* our max buffer size */
|
|
fileBytes = fileSamples * (bgmstream->info.width * bgmstream->info.channels);
|
|
if (fileBytes > (int) sizeof(raw)) {
|
|
fileBytes = (int) sizeof(raw);
|
|
fileSamples = fileBytes /
|
|
(bgmstream->info.width * bgmstream->info.channels);
|
|
}
|
|
|
|
/* Read */
|
|
res = S_CodecReadStream(bgmstream, fileBytes, raw);
|
|
if (res < fileBytes) {
|
|
fileBytes = res;
|
|
fileSamples = res / (bgmstream->info.width * bgmstream->info.channels);
|
|
}
|
|
|
|
if (res > 0) /* data: add to raw buffer */
|
|
{
|
|
S_RawSamples(fileSamples, bgmstream->info.rate,
|
|
bgmstream->info.width,
|
|
bgmstream->info.channels,
|
|
raw, bgmvolume.value);
|
|
did_rewind = false;
|
|
} else if (res == 0) /* EOF */
|
|
{
|
|
if (bgmloop) {
|
|
if (did_rewind) {
|
|
console::info("Stream keeps returning EOF.\n");
|
|
stop();
|
|
return;
|
|
}
|
|
|
|
res = S_CodecRewindStream(bgmstream);
|
|
if (res != 0) {
|
|
console::info("Stream seek error (%i), stopping.\n", res);
|
|
stop();
|
|
return;
|
|
}
|
|
did_rewind = true;
|
|
} else {
|
|
stop();
|
|
return;
|
|
}
|
|
} else /* res < 0: some read error */
|
|
{
|
|
console::info("Stream read error (%i), stopping.\n", res);
|
|
stop();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void init() {
|
|
bgm_extmusic.inscribe();
|
|
command::add("music", _play);
|
|
command::add("music_pause", _pause);
|
|
command::add("music_resume", _resume);
|
|
command::add("music_loop", _loop);
|
|
command::add("music_stop", _stop);
|
|
command::add("music_jump", _jump);
|
|
|
|
if (common::check_param("-noextmusic").has_value())
|
|
no_extmusic = true;
|
|
|
|
bgmloop = true;
|
|
|
|
for (auto i = 0; wanted_handlers[i].type != CODECTYPE_NONE; i++) {
|
|
switch (wanted_handlers[i].player) {
|
|
case player_kind::midi_drv:
|
|
/* not supported in quake */
|
|
break;
|
|
case player_kind::streamer:
|
|
wanted_handlers[i].is_available =
|
|
S_CodecIsAvailable(wanted_handlers[i].type);
|
|
break;
|
|
case player_kind::none:
|
|
default:
|
|
break;
|
|
}
|
|
if (wanted_handlers[i].is_available != -1) {
|
|
active_handlers.push_back(wanted_handlers[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void shutdown() {
|
|
stop();
|
|
active_handlers.clear();
|
|
}
|
|
|
|
void play(const std::string &filename) {
|
|
char tmp[MAX_QPATH];
|
|
|
|
stop();
|
|
|
|
if (active_handlers.empty())
|
|
return;
|
|
|
|
if (filename.empty()) {
|
|
console::debug("null music file name\n");
|
|
return;
|
|
}
|
|
|
|
const char *ext = COM_FileGetExtension(filename.c_str());
|
|
if (*ext == '\0') /* try all things */
|
|
{
|
|
play_noext(filename, ANY_CODECTYPE);
|
|
return;
|
|
}
|
|
|
|
const handler *chosen = nullptr;
|
|
for (const auto &handler: active_handlers) {
|
|
if (handler.is_available &&
|
|
!q_strcasecmp(ext, handler.ext)) {
|
|
chosen = &handler;
|
|
break;
|
|
}
|
|
}
|
|
if (chosen == nullptr) {
|
|
console::info("Unhandled extension for %s\n", filename.c_str());
|
|
return;
|
|
}
|
|
q_snprintf(tmp, sizeof(tmp), "%s/%s", chosen->dir, filename.c_str());
|
|
switch (chosen->player) {
|
|
case player_kind::midi_drv:
|
|
/* not supported in quake */
|
|
break;
|
|
case player_kind::streamer:
|
|
bgmstream = S_CodecOpenStreamType(tmp, chosen->type, bgmloop);
|
|
if (bgmstream)
|
|
return; /* success */
|
|
break;
|
|
case player_kind::none:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
console::info("Couldn't handle music file %s\n", filename.c_str());
|
|
}
|
|
|
|
void stop() {
|
|
if (bgmstream) {
|
|
bgmstream->status = STREAM_NONE;
|
|
S_CodecCloseStream(bgmstream);
|
|
bgmstream = nullptr;
|
|
s_rawend = 0;
|
|
}
|
|
}
|
|
|
|
void update() {
|
|
if (old_volume != bgmvolume.value) {
|
|
if (bgmvolume.value < 0)
|
|
bgmvolume.set("0");
|
|
else if (bgmvolume.value > 1)
|
|
bgmvolume.set("1");
|
|
old_volume = bgmvolume.value;
|
|
}
|
|
if (bgmstream)
|
|
update_stream();
|
|
}
|
|
|
|
void pause() {
|
|
if (bgmstream) {
|
|
if (bgmstream->status == STREAM_PLAY)
|
|
bgmstream->status = STREAM_PAUSE;
|
|
}
|
|
}
|
|
|
|
void resume() {
|
|
if (bgmstream) {
|
|
if (bgmstream->status == STREAM_PAUSE)
|
|
bgmstream->status = STREAM_PLAY;
|
|
}
|
|
}
|
|
|
|
void play_cd_track(const byte track, const bool looping) {
|
|
/* instead of searching by the order of music_handlers, do so by
|
|
* the order of searchpath priority: the file from the searchpath
|
|
* with the highest path_id is most likely from our own gamedir
|
|
* itself. This way, if a mod has track02 as a *.mp3 file, which
|
|
* is below *.ogg in the music_handler order, the mp3 will still
|
|
* have priority over track02.ogg from, say, id1.
|
|
*/
|
|
char tmp[MAX_QPATH];
|
|
unsigned int path_id;
|
|
|
|
stop();
|
|
if (CDAudio_Play(track, looping) == 0) {
|
|
return; /* success */
|
|
}
|
|
|
|
if (active_handlers.empty())
|
|
return; // We obviously can't play anything without handlers.
|
|
|
|
if (no_extmusic || bgm_extmusic.value == 0.0)
|
|
return;
|
|
|
|
unsigned int prev_id = 0;
|
|
unsigned int type = 0;
|
|
const char *ext = nullptr;
|
|
for (const auto handler: active_handlers) {
|
|
if (!handler.is_available) {
|
|
continue;
|
|
}
|
|
if (!CDRIPTYPE(handler.type)) {
|
|
continue;
|
|
}
|
|
q_snprintf(tmp, sizeof(tmp), "%s/track%02d.%s",
|
|
MUSIC_DIRNAME, static_cast<int>(track), handler.ext);
|
|
if (!COM_FileExists(tmp, &path_id)) {
|
|
continue;
|
|
}
|
|
if (path_id > prev_id) {
|
|
prev_id = path_id;
|
|
type = handler.type;
|
|
ext = handler.ext;
|
|
}
|
|
}
|
|
|
|
if (ext == nullptr)
|
|
console::info("Couldn't find a cdrip for track %d\n", static_cast<int>(track));
|
|
else {
|
|
q_snprintf(tmp, sizeof(tmp), "%s/track%02d.%s",
|
|
MUSIC_DIRNAME, static_cast<int>(track), ext);
|
|
bgmstream = S_CodecOpenStreamType(tmp, type, bgmloop);
|
|
if (!bgmstream)
|
|
console::info("Couldn't handle music file %s\n", tmp);
|
|
}
|
|
}
|
|
}
|