/* * 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 * * 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 #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 looping; convar extmusic{"bgm_extmusic", "1", {.archive = true}}; namespace { snd_stream_t *stream = 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 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 \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) looping = false; else if (q_strcasecmp(command::argv(1)->c_str(), "1") == 0 || q_strcasecmp(command::argv(1)->c_str(), "on") == 0) looping = true; else if (q_strcasecmp(command::argv(1)->c_str(), "toggle") == 0) looping = !looping; if (stream) stream->loop = looping; } if (looping) 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 \n"); } else if (stream) { S_CodecJumpToOrder(stream, static_cast(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: stream = S_CodecOpenStreamType(tmp, handler.type, looping); if (stream) 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 (stream->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 * stream->info.rate / shm->speed; if (!fileSamples) return; /* our max buffer size */ fileBytes = fileSamples * (stream->info.width * stream->info.channels); if (fileBytes > (int) sizeof(raw)) { fileBytes = (int) sizeof(raw); fileSamples = fileBytes / (stream->info.width * stream->info.channels); } /* Read */ res = S_CodecReadStream(stream, fileBytes, raw); if (res < fileBytes) { fileBytes = res; fileSamples = res / (stream->info.width * stream->info.channels); } if (res > 0) /* data: add to raw buffer */ { S_RawSamples(fileSamples, stream->info.rate, stream->info.width, stream->info.channels, raw, bgmvolume.value); did_rewind = false; } else if (res == 0) /* EOF */ { if (looping) { if (did_rewind) { console::info("Stream keeps returning EOF.\n"); stop(); return; } res = S_CodecRewindStream(stream); 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() { 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; looping = 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: stream = S_CodecOpenStreamType(tmp, chosen->type, looping); if (stream) return; /* success */ break; case player_kind::none: default: break; } console::info("Couldn't handle music file %s\n", filename.c_str()); } void stop() { if (stream) { stream->status = STREAM_NONE; S_CodecCloseStream(stream); stream = 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 (stream) update_stream(); } void pause() { if (stream) { if (stream->status == STREAM_PLAY) stream->status = STREAM_PAUSE; } } void resume() { if (stream) { if (stream->status == STREAM_PAUSE) stream->status = STREAM_PLAY; } } void play_cd_track(const byte track, const bool should_loop) { /* 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, should_loop) == 0) { return; /* success */ } if (active_handlers.empty()) return; // We obviously can't play anything without handlers. if (no_extmusic || 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(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(track)); else { q_snprintf(tmp, sizeof(tmp), "%s/track%02d.%s", MUSIC_DIRNAME, static_cast(track), ext); stream = S_CodecOpenStreamType(tmp, type, should_loop); if (!stream) console::info("Couldn't handle music file %s\n", tmp); } } }