diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7293393cb..57fa5993a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -226,6 +226,8 @@ target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_DISCORDRPC -DUSE_STUN) target_sources(SRB2SDL2 PRIVATE discord.c stun.c) target_link_libraries(SRB2SDL2 PRIVATE tcbrindle::span) +target_link_libraries(SRB2SDL2 PRIVATE stb_vorbis) +target_link_libraries(SRB2SDL2 PRIVATE xmp-lite::xmp-lite) set(SRB2_HAVE_THREADS ON) target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_THREADS) @@ -538,6 +540,7 @@ if(SRB2_CONFIG_PROFILEMODE AND "${CMAKE_C_COMPILER_ID}" STREQUAL "GNU") target_link_options(SRB2SDL2 PRIVATE -pg) endif() +add_subdirectory(audio) add_subdirectory(io) add_subdirectory(sdl) add_subdirectory(objects) diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt new file mode 100644 index 000000000..0f35daf91 --- /dev/null +++ b/src/audio/CMakeLists.txt @@ -0,0 +1,37 @@ +target_sources(SRB2SDL2 PRIVATE + chunk_load.cpp + chunk_load.hpp + expand_mono.cpp + expand_mono.hpp + filter.cpp + filter.hpp + gain.cpp + gain.hpp + gme_player.cpp + gme_player.hpp + gme.cpp + gme.hpp + mixer.cpp + mixer.hpp + music_player.cpp + music_player.hpp + ogg_player.cpp + ogg_player.hpp + ogg.cpp + ogg.hpp + resample.cpp + resample.hpp + sample.hpp + sound_chunk.hpp + sound_effect_player.cpp + sound_effect_player.hpp + source.hpp + wav_player.cpp + wav_player.hpp + wav.cpp + wav.hpp + xmp_player.cpp + xmp_player.hpp + xmp.cpp + xmp.hpp +) diff --git a/src/audio/chunk_load.cpp b/src/audio/chunk_load.cpp new file mode 100644 index 000000000..79900ec17 --- /dev/null +++ b/src/audio/chunk_load.cpp @@ -0,0 +1,206 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include "chunk_load.hpp" + +#include + +#include "../cxxutil.hpp" +#include "../io/streams.hpp" +#include "gme.hpp" +#include "gme_player.hpp" +#include "ogg.hpp" +#include "ogg_player.hpp" +#include "resample.hpp" +#include "sound_chunk.hpp" +#include "sound_effect_player.hpp" +#include "wav.hpp" +#include "wav_player.hpp" + +using std::nullopt; +using std::optional; +using std::size_t; + +using namespace srb2::audio; +using namespace srb2; + +namespace { + +// Utility for leveraging Resampler... +class SoundChunkSource : public Source<1> { +public: + explicit SoundChunkSource(std::unique_ptr&& chunk) + : chunk_(std::forward>(chunk)) {} + + virtual size_t generate(tcb::span> buffer) override final { + if (!chunk_) + return 0; + + size_t written = 0; + for (; pos_ < chunk_->samples.size() && written < buffer.size(); pos_++) { + buffer[written] = chunk_->samples[pos_]; + written++; + } + return written; + } + +private: + std::unique_ptr chunk_; + size_t pos_ {0}; +}; + +template +std::vector> generate_to_vec(I& source, std::size_t estimate = 0) { + std::vector> generated; + + size_t total = 0; + size_t read = 0; + generated.reserve(estimate); + do { + generated.resize(total + 4096); + read = source.generate(tcb::span {generated.data() + total, 4096}); + total += read; + } while (read != 0); + generated.resize(total); + return generated; +} + +optional try_load_dmx(tcb::span data) { + io::SpanStream stream {data}; + + if (io::remaining(stream) < 8) + return nullopt; + + uint16_t version = io::read_uint16(stream); + if (version != 3) + return nullopt; + + uint16_t rate = io::read_uint16(stream); + uint32_t length = io::read_uint32(stream) - 32u; + + if (io::remaining(stream) < (length + 32u)) + return nullopt; + + stream.seek(io::SeekFrom::kCurrent, 16); + + std::vector> samples; + for (size_t i = 0; i < length; i++) { + uint8_t doom_sample = io::read_uint8(stream); + float float_sample = audio::sample_to_float(doom_sample); + samples.push_back(Sample<1> {float_sample}); + } + size_t samples_len = samples.size(); + + if (rate == 44100) { + return SoundChunk {samples}; + } + + std::unique_ptr chunk_source = + std::make_unique(std::make_unique(SoundChunk {std::move(samples)})); + Resampler<1> resampler(std::move(chunk_source), rate / static_cast(kSampleRate)); + + std::vector> resampled; + + size_t total = 0; + size_t read = 0; + resampled.reserve(samples_len * (static_cast(kSampleRate) / rate)); + do { + resampled.resize(total + 4096); + read = resampler.generate(tcb::span {resampled.data() + total, 4096}); + total += read; + } while (read != 0); + resampled.resize(total); + + return SoundChunk {std::move(resampled)}; +} + +optional try_load_wav(tcb::span data) { + io::SpanStream stream {data}; + + audio::Wav wav; + std::size_t sample_rate; + + try { + wav = audio::load_wav(stream); + } catch (const std::exception& ex) { + return nullopt; + } + + sample_rate = wav.sample_rate(); + + audio::Resampler<1> resampler(std::make_unique(std::move(wav)), + sample_rate / static_cast(kSampleRate)); + + SoundChunk chunk {generate_to_vec(resampler)}; + return chunk; +} + +optional try_load_ogg(tcb::span data) { + std::shared_ptr> player; + try { + io::SpanStream data_stream {data}; + audio::Ogg ogg = audio::load_ogg(data_stream); + player = std::make_shared>(std::move(ogg)); + } catch (...) { + return nullopt; + } + player->looping(false); + player->playing(true); + player->reset(); + std::size_t sample_rate = player->sample_rate(); + audio::Resampler<1> resampler(player, sample_rate / 44100.); + std::vector> resampled {generate_to_vec(resampler)}; + + SoundChunk chunk {std::move(resampled)}; + return chunk; +} + +optional try_load_gme(tcb::span data) { + std::shared_ptr> player; + try { + if (data[0] == std::byte {0x1F} && data[1] == std::byte {0x8B}) { + io::SpanStream stream {data}; + audio::Gme gme = audio::load_gme(stream); + player = std::make_shared>(std::move(gme)); + } else { + io::ZlibInputStream stream {io::SpanStream(data)}; + audio::Gme gme = audio::load_gme(stream); + player = std::make_shared>(std::move(gme)); + } + } catch (...) { + return nullopt; + } + std::vector> samples {generate_to_vec(*player)}; + SoundChunk chunk {std::move(samples)}; + return chunk; +} + +} // namespace + +optional srb2::audio::try_load_chunk(tcb::span data) { + optional ret; + + ret = try_load_dmx(data); + if (ret) + return ret; + + ret = try_load_wav(data); + if (ret) + return ret; + + ret = try_load_ogg(data); + if (ret) + return ret; + + ret = try_load_gme(data); + if (ret) + return ret; + + return nullopt; +} diff --git a/src/audio/chunk_load.hpp b/src/audio/chunk_load.hpp new file mode 100644 index 000000000..c97d559d2 --- /dev/null +++ b/src/audio/chunk_load.hpp @@ -0,0 +1,27 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_AUDIO_CHUNK_LOAD_HPP__ +#define __SRB2_AUDIO_CHUNK_LOAD_HPP__ + +#include +#include + +#include + +#include "sound_chunk.hpp" + +namespace srb2::audio { + +/// @brief Try to load a chunk from the given byte span. +std::optional try_load_chunk(tcb::span data); + +} // namespace srb2::audio + +#endif // __SRB2_AUDIO_CHUNK_LOAD_HPP__ diff --git a/src/audio/expand_mono.cpp b/src/audio/expand_mono.cpp new file mode 100644 index 000000000..ce7cf0dc3 --- /dev/null +++ b/src/audio/expand_mono.cpp @@ -0,0 +1,26 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include "expand_mono.hpp" + +#include + +using std::size_t; + +using namespace srb2::audio; + +ExpandMono::~ExpandMono() = default; + +size_t ExpandMono::filter(tcb::span> input_buffer, tcb::span> buffer) { + for (size_t i = 0; i < std::min(input_buffer.size(), buffer.size()); i++) { + buffer[i].amplitudes[0] = input_buffer[i].amplitudes[0]; + buffer[i].amplitudes[1] = input_buffer[i].amplitudes[0]; + } + return std::min(input_buffer.size(), buffer.size()); +} diff --git a/src/audio/expand_mono.hpp b/src/audio/expand_mono.hpp new file mode 100644 index 000000000..f3686704e --- /dev/null +++ b/src/audio/expand_mono.hpp @@ -0,0 +1,27 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_AUDIO_EXPAND_MONO_HPP__ +#define __SRB2_AUDIO_EXPAND_MONO_HPP__ + +#include + +#include "filter.hpp" + +namespace srb2::audio { + +class ExpandMono : public Filter<1, 2> { +public: + virtual ~ExpandMono(); + virtual std::size_t filter(tcb::span> input_buffer, tcb::span> buffer) override final; +}; + +} // namespace srb2::audio + +#endif // __SRB2_AUDIO_EXPAND_MONO_HPP__ diff --git a/src/audio/filter.cpp b/src/audio/filter.cpp new file mode 100644 index 000000000..8bb09bdfb --- /dev/null +++ b/src/audio/filter.cpp @@ -0,0 +1,40 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include "filter.hpp" + +using std::shared_ptr; +using std::size_t; + +using srb2::audio::Filter; +using srb2::audio::Sample; +using srb2::audio::Source; + +template +size_t Filter::generate(tcb::span> buffer) { + input_buffer_.clear(); + input_buffer_.resize(buffer.size()); + + input_->generate(input_buffer_); + + return filter(input_buffer_, buffer); +} + +template +void Filter::bind(const shared_ptr>& input) { + input_ = input; +} + +template +Filter::~Filter() = default; + +template class srb2::audio::Filter<1, 1>; +template class srb2::audio::Filter<1, 2>; +template class srb2::audio::Filter<2, 1>; +template class srb2::audio::Filter<2, 2>; diff --git a/src/audio/filter.hpp b/src/audio/filter.hpp new file mode 100644 index 000000000..59dce6e1e --- /dev/null +++ b/src/audio/filter.hpp @@ -0,0 +1,46 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_AUDIO_FILTER_HPP__ +#define __SRB2_AUDIO_FILTER_HPP__ + +#include +#include +#include + +#include + +#include "source.hpp" + +namespace srb2::audio { + +template +class Filter : public Source { +public: + virtual std::size_t generate(tcb::span> buffer) override; + + void bind(const std::shared_ptr>& input); + + virtual std::size_t filter(tcb::span> input_buffer, tcb::span> buffer) = 0; + + virtual ~Filter(); + +private: + std::shared_ptr> input_; + std::vector> input_buffer_; +}; + +extern template class Filter<1, 1>; +extern template class Filter<1, 2>; +extern template class Filter<2, 1>; +extern template class Filter<2, 2>; + +} // namespace srb2::audio + +#endif // __SRB2_AUDIO_FILTER_HPP__ diff --git a/src/audio/gain.cpp b/src/audio/gain.cpp new file mode 100644 index 000000000..59839bb69 --- /dev/null +++ b/src/audio/gain.cpp @@ -0,0 +1,43 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include "gain.hpp" + +#include + +using std::size_t; + +using srb2::audio::Filter; +using srb2::audio::Gain; +using srb2::audio::Sample; + +constexpr const float kGainInterpolationAlpha = 0.8f; + +template +size_t Gain::filter(tcb::span> input_buffer, tcb::span> buffer) { + size_t written = std::min(buffer.size(), input_buffer.size()); + for (size_t i = 0; i < written; i++) { + buffer[i] = input_buffer[i]; + buffer[i] *= gain_; + gain_ += (new_gain_ - gain_) * kGainInterpolationAlpha; + } + + return written; +} + +template +void Gain::gain(float new_gain) { + new_gain_ = std::clamp(new_gain, 0.0f, 1.0f); +} + +template +Gain::~Gain() = default; + +template class srb2::audio::Gain<1>; +template class srb2::audio::Gain<2>; diff --git a/src/audio/gain.hpp b/src/audio/gain.hpp new file mode 100644 index 000000000..ef4dd0d53 --- /dev/null +++ b/src/audio/gain.hpp @@ -0,0 +1,33 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_AUDIO_GAIN_HPP__ +#define __SRB2_AUDIO_GAIN_HPP__ + +#include + +#include "filter.hpp" + +namespace srb2::audio { + +template +class Gain : public Filter { +public: + virtual std::size_t filter(tcb::span> input_buffer, tcb::span> buffer) override final; + void gain(float new_gain); + + virtual ~Gain(); + +private: + float new_gain_ {1.f}; + float gain_ {1.f}; +}; +} // namespace srb2::audio + +#endif // __SRB2_AUDIO_GAIN_HPP__ diff --git a/src/audio/gme.cpp b/src/audio/gme.cpp new file mode 100644 index 000000000..38279dd98 --- /dev/null +++ b/src/audio/gme.cpp @@ -0,0 +1,141 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include "gme.hpp" + +#include +#include + +#include "../cxxutil.hpp" + +using namespace srb2; +using namespace srb2::audio; + +Gme::Gme() : memory_data_(), instance_(nullptr) { +} + +Gme::Gme(Gme&& rhs) noexcept : memory_data_(), instance_(nullptr) { + std::swap(memory_data_, rhs.memory_data_); + std::swap(instance_, rhs.instance_); +} + +Gme::Gme(std::vector&& data) : memory_data_(std::move(data)), instance_(nullptr) { + _init_with_data(); +} + +Gme::Gme(tcb::span data) : memory_data_(data.begin(), data.end()), instance_(nullptr) { + _init_with_data(); +} + +Gme& Gme::operator=(Gme&& rhs) noexcept { + std::swap(memory_data_, rhs.memory_data_); + std::swap(instance_, rhs.instance_); + + return *this; +} + +Gme::~Gme() { + if (instance_) { + gme_delete(instance_); + instance_ = nullptr; + } +} + +std::size_t Gme::get_samples(tcb::span buffer) { + SRB2_ASSERT(instance_ != nullptr); + + gme_err_t err = gme_play(instance_, buffer.size(), buffer.data()); + if (err) + throw GmeException(err); + + return buffer.size(); +} + +void Gme::seek(int sample) { + SRB2_ASSERT(instance_ != nullptr); + + gme_seek_samples(instance_, sample); +} + +std::optional Gme::duration_seconds() const { + SRB2_ASSERT(instance_ != nullptr); + + gme_info_t* info = nullptr; + gme_err_t res = gme_track_info(instance_, &info, 0); + if (res) + throw GmeException(res); + auto info_finally = srb2::finally([&info] { gme_free_info(info); }); + + if (info->length == -1) + return std::nullopt; + + // info lengths are in ms + return static_cast(info->length) / 1000.f; +} + +std::optional Gme::loop_point_seconds() const { + SRB2_ASSERT(instance_ != nullptr); + + gme_info_t* info = nullptr; + gme_err_t res = gme_track_info(instance_, &info, 0); + if (res) + throw GmeException(res); + auto info_finally = srb2::finally([&info] { gme_free_info(info); }); + + int loop_point_ms = info->intro_length; + if (loop_point_ms == -1) + return std::nullopt; + + return loop_point_ms / 44100.f; +} + +float Gme::position_seconds() const { + SRB2_ASSERT(instance_ != nullptr); + + gme_info_t* info = nullptr; + gme_err_t res = gme_track_info(instance_, &info, 0); + if (res) + throw GmeException(res); + auto info_finally = srb2::finally([&info] { gme_free_info(info); }); + + int position = gme_tell(instance_); + + // adjust position, since GME's counter keeps going past loop + if (info->length > 0) + position %= info->length; + else if (info->intro_length + info->loop_length > 0) + position = position >= (info->intro_length + info->loop_length) ? (position % info->loop_length) : position; + else + position %= 150 * 1000; // 2.5 minutes + + return position / 1000.f; +} + +void Gme::_init_with_data() { + if (instance_) { + return; + } + + if (memory_data_.size() >= std::numeric_limits::max()) + throw std::invalid_argument("Buffer is too large for gme"); + if (memory_data_.size() == 0) + throw std::invalid_argument("Insufficient data from stream"); + + gme_err_t result = + gme_open_data(reinterpret_cast(memory_data_.data()), memory_data_.size(), &instance_, 44100); + if (result) + throw GmeException(result); + + // we no longer need the data, so there's no reason to keep the allocation + memory_data_ = std::vector(); + + result = gme_start_track(instance_, 0); + if (result) + throw GmeException(result); +} diff --git a/src/audio/gme.hpp b/src/audio/gme.hpp new file mode 100644 index 000000000..34f2c2769 --- /dev/null +++ b/src/audio/gme.hpp @@ -0,0 +1,74 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_AUDIO_GME_HPP__ +#define __SRB2_AUDIO_GME_HPP__ + +#include +#include +#include +#include +#include +#include +#include + +#include +#undef byte // BLARGG!! NO!! +#undef check // STOP IT!!!! + +#include "../io/streams.hpp" + +namespace srb2::audio { + +class GmeException : public std::exception { + std::string msg_; + +public: + explicit GmeException(gme_err_t msg) : msg_(msg == nullptr ? "" : msg) {} + + virtual const char* what() const noexcept override { return msg_.c_str(); } +}; + +class Gme { + std::vector memory_data_; + Music_Emu* instance_; + +public: + Gme(); + Gme(const Gme&) = delete; + Gme(Gme&& rhs) noexcept; + + Gme& operator=(const Gme&) = delete; + Gme& operator=(Gme&& rhs) noexcept; + + explicit Gme(std::vector&& data); + explicit Gme(tcb::span data); + + std::size_t get_samples(tcb::span buffer); + void seek(int sample); + + std::optional duration_seconds() const; + std::optional loop_point_seconds() const; + float position_seconds() const; + + ~Gme(); + +private: + void _init_with_data(); +}; + +template , int> = 0> +inline Gme load_gme(I& stream) { + std::vector data = srb2::io::read_to_vec(stream); + return Gme {std::move(data)}; +} + +} // namespace srb2::audio + +#endif // __SRB2_AUDIO_GME_HPP__ diff --git a/src/audio/gme_player.cpp b/src/audio/gme_player.cpp new file mode 100644 index 000000000..229c99676 --- /dev/null +++ b/src/audio/gme_player.cpp @@ -0,0 +1,73 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include "gme_player.hpp" + +using namespace srb2; +using namespace srb2::audio; + +template +GmePlayer::GmePlayer(Gme&& gme) : gme_(std::forward(gme)), buf_() { +} + +template +GmePlayer::GmePlayer(GmePlayer&& rhs) noexcept = default; + +template +GmePlayer& GmePlayer::operator=(GmePlayer&& rhs) noexcept = default; + +template +GmePlayer::~GmePlayer() = default; + +template +std::size_t GmePlayer::generate(tcb::span> buffer) { + buf_.clear(); + buf_.resize(buffer.size() * 2); + + std::size_t read = gme_.get_samples(tcb::make_span(buf_)); + buf_.resize(read); + std::size_t new_samples = std::min((read / 2), buffer.size()); + for (std::size_t i = 0; i < new_samples; i++) { + if constexpr (C == 1) { + buffer[i].amplitudes[0] = (buf_[i * 2] / 32768.f + buf_[i * 2 + 1] / 32768.f) / 2.f; + } else if constexpr (C == 2) { + buffer[i].amplitudes[0] = buf_[i * 2] / 32768.f; + buffer[i].amplitudes[1] = buf_[i * 2 + 1] / 32768.f; + } + } + return new_samples; +} + +template +void GmePlayer::seek(float position_seconds) { + gme_.seek(static_cast(position_seconds * 44100.f)); +} + +template +void GmePlayer::reset() { + gme_.seek(0); +} + +template +std::optional GmePlayer::duration_seconds() const { + return gme_.duration_seconds(); +} + +template +std::optional GmePlayer::loop_point_seconds() const { + return gme_.loop_point_seconds(); +} + +template +float GmePlayer::position_seconds() const { + return gme_.position_seconds(); +} + +template class srb2::audio::GmePlayer<1>; +template class srb2::audio::GmePlayer<2>; diff --git a/src/audio/gme_player.hpp b/src/audio/gme_player.hpp new file mode 100644 index 000000000..a2f555c08 --- /dev/null +++ b/src/audio/gme_player.hpp @@ -0,0 +1,51 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_AUDIO_GME_PLAYER_HPP__ +#define __SRB2_AUDIO_GME_PLAYER_HPP__ + +#include + +#include "gme.hpp" +#include "source.hpp" + +namespace srb2::audio { + +template +class GmePlayer : public Source { + Gme gme_; + std::vector buf_; + +public: + GmePlayer(Gme&& gme); + GmePlayer(const GmePlayer&) = delete; + GmePlayer(GmePlayer&& gme) noexcept; + + ~GmePlayer(); + + GmePlayer& operator=(const GmePlayer&) = delete; + GmePlayer& operator=(GmePlayer&& rhs) noexcept; + + virtual std::size_t generate(tcb::span> buffer) override; + + void seek(float position_seconds); + + std::optional duration_seconds() const; + std::optional loop_point_seconds() const; + float position_seconds() const; + + void reset(); +}; + +extern template class GmePlayer<1>; +extern template class GmePlayer<2>; + +} // namespace srb2::audio + +#endif // __SRB2_AUDIO_GME_PLAYER_HPP__ diff --git a/src/audio/mixer.cpp b/src/audio/mixer.cpp new file mode 100644 index 000000000..d4b718ffa --- /dev/null +++ b/src/audio/mixer.cpp @@ -0,0 +1,62 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include "mixer.hpp" + +#include + +using std::shared_ptr; +using std::size_t; + +using srb2::audio::Mixer; +using srb2::audio::Sample; +using srb2::audio::Source; + +namespace { + +template +void default_init_sample_buffer(Sample* buffer, size_t size) { + std::for_each(buffer, buffer + size, [](auto& i) { i = Sample {}; }); +} + +template +void mix_sample_buffers(Sample* dst, size_t size, Sample* src, size_t src_size) { + for (size_t i = 0; i < size && i < src_size; i++) { + dst[i] += src[i]; + } +} + +} // namespace + +template +size_t Mixer::generate(tcb::span> buffer) { + buffer_.resize(buffer.size()); + + default_init_sample_buffer(buffer.data(), buffer.size()); + + for (auto& source : sources_) { + size_t read = source->generate(buffer_); + + mix_sample_buffers(buffer.data(), buffer.size(), buffer_.data(), read); + } + + // because we initialized the out-buffer, we always generate size samples + return buffer.size(); +} + +template +void Mixer::add_source(const shared_ptr>& source) { + sources_.push_back(source); +} + +template +Mixer::~Mixer() = default; + +template class srb2::audio::Mixer<1>; +template class srb2::audio::Mixer<2>; diff --git a/src/audio/mixer.hpp b/src/audio/mixer.hpp new file mode 100644 index 000000000..75c8ba2f9 --- /dev/null +++ b/src/audio/mixer.hpp @@ -0,0 +1,41 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_AUDIO_MIXER_HPP__ +#define __SRB2_AUDIO_MIXER_HPP__ + +#include +#include + +#include + +#include "source.hpp" + +namespace srb2::audio { + +template +class Mixer : public Source { +public: + virtual std::size_t generate(tcb::span> buffer) override final; + + virtual ~Mixer(); + + void add_source(const std::shared_ptr>& source); + +private: + std::vector>> sources_; + std::vector> buffer_; +}; + +extern template class Mixer<1>; +extern template class Mixer<2>; + +} // namespace srb2::audio + +#endif // __SRB2_AUDIO_MIXER_HPP__ diff --git a/src/audio/music_player.cpp b/src/audio/music_player.cpp new file mode 100644 index 000000000..1121518da --- /dev/null +++ b/src/audio/music_player.cpp @@ -0,0 +1,421 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include "music_player.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#undef byte // BLARGG!! NO!! +#undef check // STOP IT!!!! + +#include "../cxxutil.hpp" +#include "../io/streams.hpp" +#include "gme_player.hpp" +#include "ogg_player.hpp" +#include "resample.hpp" +#include "xmp_player.hpp" + +using std::array; +using std::byte; +using std::make_unique; +using std::size_t; +using std::vector; + +using srb2::audio::MusicPlayer; +using srb2::audio::Resampler; +using srb2::audio::Sample; +using srb2::audio::Source; +using namespace srb2; + +class MusicPlayer::Impl { +public: + Impl() = default; + Impl(tcb::span data) : Impl() { _load(data); } + + size_t generate(tcb::span> buffer) { + if (!resampler_) + return 0; + + if (!playing_) + return 0; + + size_t total_written = 0; + + while (total_written < buffer.size()) { + const size_t generated = resampler_->generate(buffer.subspan(total_written)); + + // To avoid a branch preventing optimizations, we're always going to apply + // the fade gain, even if it would clamp anyway. + for (std::size_t i = 0; i < generated; i++) { + const float alpha = 1.0 - (gain_samples_target_ - std::min(gain_samples_ + i, gain_samples_target_)) / + static_cast(gain_samples_target_); + const float fade_gain = (gain_target_ - gain_) * std::clamp(alpha, 0.f, 1.f) + gain_; + buffer[total_written + i] *= fade_gain; + } + + gain_samples_ = std::min(gain_samples_ + generated, gain_samples_target_); + + if (gain_samples_ >= gain_samples_target_) { + fading_ = false; + gain_samples_ = gain_samples_target_; + gain_ = gain_target_; + } + + total_written += generated; + + if (generated == 0) { + playing_ = false; + break; + } + } + + return total_written; + } + + void _load(tcb::span data) { + ogg_inst_ = nullptr; + gme_inst_ = nullptr; + xmp_inst_ = nullptr; + resampler_ = std::nullopt; + + try { + io::SpanStream stream {data}; + audio::Ogg ogg = audio::load_ogg(stream); + ogg_inst_ = std::make_shared>(std::move(ogg)); + ogg_inst_->looping(looping_); + resampler_ = Resampler<2>(ogg_inst_, ogg_inst_->sample_rate() / 44100.f); + } catch (const std::exception& ex) { + // it's probably not ogg + ogg_inst_ = nullptr; + resampler_ = std::nullopt; + } + + if (!resampler_) { + try { + if (data[0] == std::byte {0x1F} && data[1] == std::byte {0x8B}) { + io::ZlibInputStream stream {io::SpanStream(data)}; + audio::Gme gme = audio::load_gme(stream); + gme_inst_ = std::make_shared>(std::move(gme)); + } else { + io::SpanStream stream {data}; + audio::Gme gme = audio::load_gme(stream); + gme_inst_ = std::make_shared>(std::move(gme)); + } + + resampler_ = Resampler<2>(gme_inst_, 1.f); + } catch (const std::exception& ex) { + // it's probably not gme + gme_inst_ = nullptr; + resampler_ = std::nullopt; + } + } + + if (!resampler_) { + try { + io::SpanStream stream {data}; + audio::Xmp<2> xmp = audio::load_xmp<2>(stream); + xmp_inst_ = std::make_shared>(std::move(xmp)); + xmp_inst_->looping(looping_); + + resampler_ = Resampler<2>(xmp_inst_, 1.f); + } catch (const std::exception& ex) { + // it's probably not xmp + xmp_inst_ = nullptr; + resampler_ = std::nullopt; + } + } + + playing_ = false; + + internal_gain(1.f); + } + + void play(bool looping) { + if (ogg_inst_) { + ogg_inst_->looping(looping); + ogg_inst_->playing(true); + playing_ = true; + ogg_inst_->reset(); + } else if (gme_inst_) { + playing_ = true; + gme_inst_->reset(); + } else if (xmp_inst_) { + xmp_inst_->looping(looping); + playing_ = true; + xmp_inst_->reset(); + } + } + + void unpause() { + if (ogg_inst_) { + ogg_inst_->playing(true); + playing_ = true; + } else if (gme_inst_) { + playing_ = true; + } else if (xmp_inst_) { + playing_ = true; + } + } + + void pause() { + if (ogg_inst_) { + ogg_inst_->playing(false); + playing_ = false; + } else if (gme_inst_) { + playing_ = false; + } else if (xmp_inst_) { + playing_ = false; + } + } + + void stop() { + if (ogg_inst_) { + ogg_inst_->reset(); + ogg_inst_->playing(false); + playing_ = false; + } else if (gme_inst_) { + gme_inst_->reset(); + playing_ = false; + } else if (xmp_inst_) { + xmp_inst_->reset(); + playing_ = false; + } + } + + void seek(float position_seconds) { + if (ogg_inst_) { + ogg_inst_->seek(position_seconds); + return; + } + if (gme_inst_) { + gme_inst_->seek(position_seconds); + return; + } + if (xmp_inst_) { + xmp_inst_->seek(position_seconds); + return; + } + } + + bool playing() const { + if (ogg_inst_) + return ogg_inst_->playing(); + else if (gme_inst_) + return playing_; + else if (xmp_inst_) + return playing_; + + return false; + } + + std::optional music_type() const { + if (ogg_inst_) + return audio::MusicType::kOgg; + else if (gme_inst_) + return audio::MusicType::kGme; + else if (xmp_inst_) + return audio::MusicType::kMod; + + return std::nullopt; + } + + std::optional duration_seconds() const { + if (ogg_inst_) + return ogg_inst_->duration_seconds(); + if (gme_inst_) + return gme_inst_->duration_seconds(); + if (xmp_inst_) + return xmp_inst_->duration_seconds(); + + return std::nullopt; + } + + std::optional loop_point_seconds() const { + if (ogg_inst_) + return ogg_inst_->loop_point_seconds(); + if (gme_inst_) + return gme_inst_->loop_point_seconds(); + + return std::nullopt; + } + + std::optional position_seconds() const { + if (ogg_inst_) + return ogg_inst_->position_seconds(); + if (gme_inst_) + return gme_inst_->position_seconds(); + + return std::nullopt; + } + + void fade_to(float gain, float seconds) { fade_from_to(gain_target_, gain, seconds); } + + void fade_from_to(float from, float to, float seconds) { + fading_ = true; + gain_ = from; + gain_target_ = to; + // Gain samples target must always be at least 1 to avoid a div-by-zero. + gain_samples_target_ = std::max(static_cast(seconds * 44100.f), 1ULL); + gain_samples_ = 0; + } + + bool fading() const { return fading_; } + + void stop_fade() { internal_gain(gain_target_); } + + void loop_point_seconds(float loop_point) { + if (ogg_inst_) + ogg_inst_->loop_point_seconds(loop_point); + } + + void internal_gain(float gain) { + fading_ = false; + gain_ = gain; + gain_target_ = gain; + gain_samples_target_ = 1; + gain_samples_ = 0; + } + +private: + std::shared_ptr> ogg_inst_; + std::shared_ptr> gme_inst_; + std::shared_ptr> xmp_inst_; + std::optional> resampler_; + bool playing_ {false}; + bool looping_ {false}; + + // fade control + float gain_target_ {1.f}; + float gain_ {1.f}; + bool fading_ {false}; + uint64_t gain_samples_ {0}; + uint64_t gain_samples_target_ {1}; +}; + +// The special member functions MUST be declared in this unit, where Impl is complete. +MusicPlayer::MusicPlayer() : impl_(make_unique()) { +} +MusicPlayer::MusicPlayer(tcb::span data) : impl_(make_unique(data)) { +} +MusicPlayer::MusicPlayer(MusicPlayer&& rhs) noexcept = default; +MusicPlayer& MusicPlayer::operator=(MusicPlayer&& rhs) noexcept = default; + +MusicPlayer::~MusicPlayer() = default; + +void MusicPlayer::play(bool looping) { + SRB2_ASSERT(impl_ != nullptr); + + return impl_->play(looping); +} + +void MusicPlayer::unpause() { + SRB2_ASSERT(impl_ != nullptr); + + return impl_->unpause(); +} + +void MusicPlayer::pause() { + SRB2_ASSERT(impl_ != nullptr); + + return impl_->pause(); +} + +void MusicPlayer::stop() { + SRB2_ASSERT(impl_ != nullptr); + + return impl_->stop(); +} + +void MusicPlayer::seek(float position_seconds) { + SRB2_ASSERT(impl_ != nullptr); + + return impl_->seek(position_seconds); +} + +bool MusicPlayer::playing() const { + SRB2_ASSERT(impl_ != nullptr); + + return impl_->playing(); +} + +size_t MusicPlayer::generate(tcb::span> buffer) { + SRB2_ASSERT(impl_ != nullptr); + + return impl_->generate(buffer); +} + +std::optional MusicPlayer::music_type() const { + SRB2_ASSERT(impl_ != nullptr); + + return impl_->music_type(); +} + +std::optional MusicPlayer::duration_seconds() const { + SRB2_ASSERT(impl_ != nullptr); + + return impl_->duration_seconds(); +} + +std::optional MusicPlayer::loop_point_seconds() const { + SRB2_ASSERT(impl_ != nullptr); + + return impl_->loop_point_seconds(); +} + +std::optional MusicPlayer::position_seconds() const { + SRB2_ASSERT(impl_ != nullptr); + + return impl_->position_seconds(); +} + +void MusicPlayer::fade_to(float gain, float seconds) { + SRB2_ASSERT(impl_ != nullptr); + + impl_->fade_to(gain, seconds); +} + +void MusicPlayer::fade_from_to(float from, float to, float seconds) { + SRB2_ASSERT(impl_ != nullptr); + + impl_->fade_from_to(from, to, seconds); +} + +void MusicPlayer::internal_gain(float gain) { + SRB2_ASSERT(impl_ != nullptr); + + impl_->internal_gain(gain); +} + +bool MusicPlayer::fading() const { + SRB2_ASSERT(impl_ != nullptr); + + return impl_->fading(); +} + +void MusicPlayer::stop_fade() { + SRB2_ASSERT(impl_ != nullptr); + + impl_->stop_fade(); +} + +void MusicPlayer::loop_point_seconds(float loop_point) { + SRB2_ASSERT(impl_ != nullptr); + + impl_->loop_point_seconds(loop_point); +} diff --git a/src/audio/music_player.hpp b/src/audio/music_player.hpp new file mode 100644 index 000000000..668724fa0 --- /dev/null +++ b/src/audio/music_player.hpp @@ -0,0 +1,69 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_AUDIO_MUSIC_PLAYER_HPP__ +#define __SRB2_AUDIO_MUSIC_PLAYER_HPP__ + +#include +#include + +#include + +#include "source.hpp" + +struct stb_vorbis; + +namespace srb2::audio { + +enum class MusicType { + kOgg, + kGme, + kMod +}; + +class MusicPlayer : public Source<2> { +public: + MusicPlayer(); + MusicPlayer(tcb::span data); + MusicPlayer(const MusicPlayer& rhs) = delete; + MusicPlayer(MusicPlayer&& rhs) noexcept; + + MusicPlayer& operator=(const MusicPlayer& rhs) = delete; + MusicPlayer& operator=(MusicPlayer&& rhs) noexcept; + + virtual std::size_t generate(tcb::span> buffer) override final; + + void play(bool looping); + void unpause(); + void pause(); + void stop(); + void seek(float position_seconds); + void fade_to(float gain, float seconds); + void fade_from_to(float from, float to, float seconds); + void internal_gain(float gain); + void stop_fade(); + void loop_point_seconds(float loop_point); + bool playing() const; + std::optional music_type() const; + std::optional duration_seconds() const; + std::optional loop_point_seconds() const; + std::optional position_seconds() const; + bool fading() const; + + virtual ~MusicPlayer() final; + +private: + class Impl; + + std::unique_ptr impl_; +}; + +} // namespace srb2::audio + +#endif // __SRB2_AUDIO_MUSIC_PLAYER_HPP__ diff --git a/src/audio/ogg.cpp b/src/audio/ogg.cpp new file mode 100644 index 000000000..388fd37fe --- /dev/null +++ b/src/audio/ogg.cpp @@ -0,0 +1,194 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include "ogg.hpp" + +#include + +#include "../cxxutil.hpp" + +using namespace srb2; +using namespace srb2::audio; + +StbVorbisException::StbVorbisException(int code) noexcept : code_(code) { +} + +const char* StbVorbisException::what() const noexcept { + switch (code_) { + case VORBIS__no_error: + return "No error"; + case VORBIS_need_more_data: + return "Need more data"; + case VORBIS_invalid_api_mixing: + return "Invalid API mixing"; + case VORBIS_outofmem: + return "Out of memory"; + case VORBIS_feature_not_supported: + return "Feature not supported"; + case VORBIS_too_many_channels: + return "Too many channels"; + case VORBIS_file_open_failure: + return "File open failure"; + case VORBIS_seek_without_length: + return "Seek without length"; + case VORBIS_unexpected_eof: + return "Unexpected EOF"; + case VORBIS_seek_invalid: + return "Seek invalid"; + case VORBIS_invalid_setup: + return "Invalid setup"; + case VORBIS_invalid_stream: + return "Invalid stream"; + case VORBIS_missing_capture_pattern: + return "Missing capture pattern"; + case VORBIS_invalid_stream_structure_version: + return "Invalid stream structure version"; + case VORBIS_continued_packet_flag_invalid: + return "Continued packet flag invalid"; + case VORBIS_incorrect_stream_serial_number: + return "Incorrect stream serial number"; + case VORBIS_invalid_first_page: + return "Invalid first page"; + case VORBIS_bad_packet_type: + return "Bad packet type"; + case VORBIS_cant_find_last_page: + return "Can't find last page"; + case VORBIS_seek_failed: + return "Seek failed"; + case VORBIS_ogg_skeleton_not_supported: + return "OGG skeleton not supported"; + default: + return "Unrecognized error code"; + } +} + +Ogg::Ogg() noexcept : memory_data_(), instance_(nullptr) { +} + +Ogg::Ogg(std::vector data) : memory_data_(std::move(data)), instance_(nullptr) { + _init_with_data(); +} + +Ogg::Ogg(tcb::span data) : memory_data_(data.begin(), data.end()), instance_(nullptr) { + _init_with_data(); +} + +Ogg::Ogg(Ogg&& rhs) noexcept : memory_data_(), instance_(nullptr) { + std::swap(memory_data_, rhs.memory_data_); + std::swap(instance_, rhs.instance_); +} + +Ogg& Ogg::operator=(Ogg&& rhs) noexcept { + std::swap(memory_data_, rhs.memory_data_); + std::swap(instance_, rhs.instance_); + + return *this; +} + +Ogg::~Ogg() { + if (instance_) { + stb_vorbis_close(instance_); + instance_ = nullptr; + } +} + +std::size_t Ogg::get_samples(tcb::span> buffer) { + SRB2_ASSERT(instance_ != nullptr); + + size_t read = stb_vorbis_get_samples_float_interleaved( + instance_, 1, reinterpret_cast(buffer.data()), buffer.size() * 1); + + return read; +} + +std::size_t Ogg::get_samples(tcb::span> buffer) { + SRB2_ASSERT(instance_ != nullptr); + + size_t read = stb_vorbis_get_samples_float_interleaved( + instance_, 2, reinterpret_cast(buffer.data()), buffer.size() * 2); + + stb_vorbis_info info = stb_vorbis_get_info(instance_); + if (info.channels == 1) { + for (auto& sample : buffer.subspan(0, read)) { + sample.amplitudes[1] = sample.amplitudes[0]; + } + } + + return read; +} + +OggComment Ogg::comment() const { + SRB2_ASSERT(instance_ != nullptr); + + stb_vorbis_comment c_comment = stb_vorbis_get_comment(instance_); + + return OggComment { + std::string(c_comment.vendor), + std::vector(c_comment.comment_list, c_comment.comment_list + c_comment.comment_list_length)}; +} + +std::size_t Ogg::sample_rate() const { + SRB2_ASSERT(instance_ != nullptr); + + stb_vorbis_info info = stb_vorbis_get_info(instance_); + return info.sample_rate; +} + +void Ogg::seek(std::size_t sample) { + SRB2_ASSERT(instance_ != nullptr); + + stb_vorbis_seek(instance_, sample); +} + +std::size_t Ogg::position() const { + SRB2_ASSERT(instance_ != nullptr); + + return stb_vorbis_get_sample_offset(instance_); +} + +float Ogg::position_seconds() const { + return position() / static_cast(sample_rate()); +} + +std::size_t Ogg::duration_samples() const { + SRB2_ASSERT(instance_ != nullptr); + + return stb_vorbis_stream_length_in_samples(instance_); +} + +float Ogg::duration_seconds() const { + SRB2_ASSERT(instance_ != nullptr); + + return stb_vorbis_stream_length_in_seconds(instance_); +} + +std::size_t Ogg::channels() const { + SRB2_ASSERT(instance_ != nullptr); + + stb_vorbis_info info = stb_vorbis_get_info(instance_); + return info.channels; +} + +void Ogg::_init_with_data() { + if (instance_) { + return; + } + + if (memory_data_.size() >= std::numeric_limits::max()) + throw std::logic_error("Buffer is too large for stb_vorbis"); + if (memory_data_.size() == 0) + throw std::logic_error("Insufficient data from stream"); + + int vorbis_result; + instance_ = stb_vorbis_open_memory( + reinterpret_cast(memory_data_.data()), memory_data_.size(), &vorbis_result, NULL); + + if (vorbis_result != VORBIS__no_error) + throw StbVorbisException(vorbis_result); +} diff --git a/src/audio/ogg.hpp b/src/audio/ogg.hpp new file mode 100644 index 000000000..d4b8b9275 --- /dev/null +++ b/src/audio/ogg.hpp @@ -0,0 +1,81 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_AUDIO_OGG_HPP__ +#define __SRB2_AUDIO_OGG_HPP__ + +#include +#include +#include + +#include +#include + +#include "../io/streams.hpp" +#include "source.hpp" + +namespace srb2::audio { + +class StbVorbisException final : public std::exception { + int code_; + +public: + explicit StbVorbisException(int code) noexcept; + + virtual const char* what() const noexcept; +}; + +struct OggComment { + std::string vendor; + std::vector comments; +}; + +class Ogg final { + std::vector memory_data_; + stb_vorbis* instance_; + +public: + Ogg() noexcept; + + explicit Ogg(std::vector data); + explicit Ogg(tcb::span data); + + Ogg(const Ogg&) = delete; + Ogg(Ogg&& rhs) noexcept; + + Ogg& operator=(const Ogg&) = delete; + Ogg& operator=(Ogg&& rhs) noexcept; + + ~Ogg(); + + std::size_t get_samples(tcb::span> buffer); + std::size_t get_samples(tcb::span> buffer); + void seek(std::size_t sample); + std::size_t position() const; + float position_seconds() const; + + OggComment comment() const; + std::size_t sample_rate() const; + std::size_t channels() const; + std::size_t duration_samples() const; + float duration_seconds() const; + +private: + void _init_with_data(); +}; + +template , int> = 0> +inline Ogg load_ogg(I& stream) { + std::vector data = srb2::io::read_to_vec(stream); + return Ogg {std::move(data)}; +} + +} // namespace srb2::audio + +#endif // __SRB2_AUDIO_OGG_HPP__ diff --git a/src/audio/ogg_player.cpp b/src/audio/ogg_player.cpp new file mode 100644 index 000000000..d9028dedb --- /dev/null +++ b/src/audio/ogg_player.cpp @@ -0,0 +1,141 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include "ogg_player.hpp" + +#include +#include +#include +#include +#include + +using namespace srb2; +using namespace srb2::audio; + +namespace { + +std::optional find_loop_point(const Ogg& ogg) { + OggComment comment = ogg.comment(); + std::size_t rate = ogg.sample_rate(); + for (auto& comment : comment.comments) { + if (comment.find("LOOPPOINT=") == 0) { + std::string_view comment_view(comment); + comment_view.remove_prefix(10); + std::string copied {comment_view}; + + try { + int loop_point = std::stoi(copied); + return loop_point; + } catch (...) { + } + } + + if (comment.find("LOOPMS=") == 0) { + std::string_view comment_view(comment); + comment_view.remove_prefix(7); + std::string copied {comment_view}; + + try { + int loop_ms = std::stoi(copied); + int loop_point = std::round(static_cast(loop_ms) / (rate / 1000.)); + + return loop_point; + } catch (...) { + } + } + } + + return std::nullopt; +} + +} // namespace + +template +OggPlayer::OggPlayer(Ogg&& ogg) noexcept + : playing_(false), looping_(false), loop_point_(std::nullopt), ogg_(std::forward(ogg)) { + loop_point_ = find_loop_point(ogg_); +} + +template +OggPlayer::OggPlayer(OggPlayer&& rhs) noexcept = default; + +template +OggPlayer& OggPlayer::operator=(OggPlayer&& rhs) noexcept = default; + +template +OggPlayer::~OggPlayer() = default; + +template +std::size_t OggPlayer::generate(tcb::span> buffer) { + if (!playing_) + return 0; + + std::size_t total = 0; + do { + std::size_t read = ogg_.get_samples(buffer.subspan(total)); + total += read; + + if (read == 0 && !looping_) { + playing_ = false; + break; + } + + if (read == 0 && loop_point_) { + ogg_.seek(*loop_point_); + } + + if (read == 0 && !loop_point_) { + ogg_.seek(0); + } + } while (total < buffer.size()); + + return total; +} + +template +void OggPlayer::seek(float position_seconds) { + ogg_.seek(static_cast(position_seconds * sample_rate())); +} + +template +void OggPlayer::loop_point_seconds(float loop_point) { + std::size_t rate = sample_rate(); + loop_point = static_cast(std::round(loop_point * rate)); +} + +template +void OggPlayer::reset() { + ogg_.seek(0); +} + +template +std::size_t OggPlayer::sample_rate() const { + return ogg_.sample_rate(); +} + +template +float OggPlayer::duration_seconds() const { + return ogg_.duration_seconds(); +} + +template +std::optional OggPlayer::loop_point_seconds() const { + if (!loop_point_) + return std::nullopt; + + return *loop_point_ / static_cast(sample_rate()); +} + +template +float OggPlayer::position_seconds() const { + return ogg_.position_seconds(); +} + +template class srb2::audio::OggPlayer<1>; +template class srb2::audio::OggPlayer<2>; diff --git a/src/audio/ogg_player.hpp b/src/audio/ogg_player.hpp new file mode 100644 index 000000000..049d39862 --- /dev/null +++ b/src/audio/ogg_player.hpp @@ -0,0 +1,72 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_AUDIO_OGG_SOURCE_HPP__ +#define __SRB2_AUDIO_OGG_SOURCE_HPP__ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "../io/streams.hpp" +#include "ogg.hpp" +#include "source.hpp" + +namespace srb2::audio { + +template +class OggPlayer final : public Source { + bool playing_; + bool looping_; + std::optional loop_point_; + Ogg ogg_; + +public: + OggPlayer(Ogg&& ogg) noexcept; + + OggPlayer(const OggPlayer&) = delete; + OggPlayer(OggPlayer&& rhs) noexcept; + + OggPlayer& operator=(const OggPlayer&) = delete; + OggPlayer& operator=(OggPlayer&& rhs) noexcept; + + virtual std::size_t generate(tcb::span> buffer) override final; + + bool looping() const { return looping_; } + + void looping(bool looping) { looping_ = looping; } + + bool playing() const { return playing_; } + void playing(bool playing) { playing_ = playing; } + void seek(float position_seconds); + void loop_point_seconds(float loop_point); + + void reset(); + std::size_t sample_rate() const; + + float duration_seconds() const; + std::optional loop_point_seconds() const; + float position_seconds() const; + + ~OggPlayer(); +}; + +extern template class OggPlayer<1>; +extern template class OggPlayer<2>; + +} // namespace srb2::audio + +#endif // __SRB2_AUDIO_OGG_SOURCE_HPP__ diff --git a/src/audio/resample.cpp b/src/audio/resample.cpp new file mode 100644 index 000000000..a64d0af13 --- /dev/null +++ b/src/audio/resample.cpp @@ -0,0 +1,81 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include "resample.hpp" + +#include +#include +#include +#include +#include + +using std::shared_ptr; +using std::size_t; +using std::vector; + +using namespace srb2::audio; + +template +Resampler::Resampler(std::shared_ptr>&& source, float ratio) + : source_(std::forward>>(source)), ratio_(ratio) { +} + +template +Resampler::Resampler(Resampler&& r) = default; + +template +Resampler::~Resampler() = default; + +template +Resampler& Resampler::operator=(Resampler&& r) = default; + +template +size_t Resampler::generate(tcb::span> buffer) { + if (!source_) + return 0; + + if (ratio_ == 1.f) { + // fast path - generate directly from source + size_t source_read = source_->generate(buffer); + return source_read; + } + + size_t written = 0; + + while (written < buffer.size()) { + // do we need a refill? + if (buf_.size() == 0 || pos_ >= static_cast(buf_.size() - 1)) { + pos_ -= buf_.size(); + last_ = buf_.size() == 0 ? Sample {} : buf_.back(); + buf_.clear(); + buf_.resize(512); + size_t source_read = source_->generate(buf_); + buf_.resize(source_read); + if (source_read == 0) { + break; + } + } + + if (pos_ < 0) { + buffer[written] = (buf_[0] - last_) * pos_frac_ + last_; + advance(ratio_); + written++; + continue; + } + + buffer[written] = (buf_[pos_ + 1] - buf_[pos_]) * pos_frac_ + buf_[pos_]; + advance(ratio_); + written++; + } + + return written; +} + +template class srb2::audio::Resampler<1>; +template class srb2::audio::Resampler<2>; diff --git a/src/audio/resample.hpp b/src/audio/resample.hpp new file mode 100644 index 000000000..7aab4f674 --- /dev/null +++ b/src/audio/resample.hpp @@ -0,0 +1,63 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_AUDIO_RESAMPLE_HPP__ +#define __SRB2_AUDIO_RESAMPLE_HPP__ + +#include +#include +#include +#include +#include + +#include + +#include "sound_chunk.hpp" +#include "source.hpp" + +namespace srb2::audio { + +template +class Resampler : public Source { +public: + Resampler(std::shared_ptr>&& source_, float ratio); + Resampler(const Resampler& r) = delete; + Resampler(Resampler&& r); + virtual ~Resampler(); + + virtual std::size_t generate(tcb::span> buffer); + + Resampler& operator=(const Resampler& r) = delete; + Resampler& operator=(Resampler&& r); + +private: + std::shared_ptr> source_; + float ratio_ {1.f}; + std::vector> buf_; + Sample last_; + int pos_ {0}; + float pos_frac_ {0.f}; + + void advance(float samples) { + pos_frac_ += samples; + float integer; + std::modf(pos_frac_, &integer); + pos_ += integer; + pos_frac_ -= integer; + } + + void refill(); +}; + +extern template class Resampler<1>; +extern template class Resampler<2>; + +} // namespace srb2::audio + +#endif // __SRB2_AUDIO_RESAMPLE_HPP__ diff --git a/src/audio/sample.hpp b/src/audio/sample.hpp new file mode 100644 index 000000000..b1f0298b5 --- /dev/null +++ b/src/audio/sample.hpp @@ -0,0 +1,78 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_AUDIO_SAMPLE_HPP__ +#define __SRB2_AUDIO_SAMPLE_HPP__ + +#include + +namespace srb2::audio { + +template +struct Sample { + std::array amplitudes; + + constexpr Sample& operator+=(const Sample& rhs) noexcept { + for (std::size_t i = 0; i < C; i++) { + amplitudes[i] += rhs.amplitudes[i]; + } + return *this; + } + + constexpr Sample& operator*=(float rhs) noexcept { + for (std::size_t i = 0; i < C; i++) { + amplitudes[i] *= rhs; + } + return *this; + } +}; + +template +constexpr Sample operator+(const Sample& lhs, const Sample& rhs) noexcept { + Sample out; + for (std::size_t i = 0; i < C; i++) { + out.amplitudes[i] = lhs.amplitudes[i] + rhs.amplitudes[i]; + } + return out; +} + +template +constexpr Sample operator-(const Sample& lhs, const Sample& rhs) noexcept { + Sample out; + for (std::size_t i = 0; i < C; i++) { + out.amplitudes[i] = lhs.amplitudes[i] - rhs.amplitudes[i]; + } + return out; +} + +template +constexpr Sample operator*(const Sample& lhs, float rhs) noexcept { + Sample out; + for (std::size_t i = 0; i < C; i++) { + out.amplitudes[i] = lhs.amplitudes[i] * rhs; + } + return out; +} + +template +static constexpr float sample_to_float(T sample) noexcept; + +template <> +constexpr float sample_to_float(uint8_t sample) noexcept { + return (sample / 128.f) - 1.f; +} + +template <> +constexpr float sample_to_float(int16_t sample) noexcept { + return sample / 32768.f; +} + +} // namespace srb2::audio + +#endif // __SRB2_AUDIO_SAMPLE_HPP__ diff --git a/src/audio/sound_chunk.hpp b/src/audio/sound_chunk.hpp new file mode 100644 index 000000000..7fc0a45eb --- /dev/null +++ b/src/audio/sound_chunk.hpp @@ -0,0 +1,25 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_AUDIO_SOUND_CHUNK_HPP__ +#define __SRB2_AUDIO_SOUND_CHUNK_HPP__ + +#include + +#include "source.hpp" + +namespace srb2::audio { + +struct SoundChunk { + std::vector> samples; +}; + +} // namespace srb2::audio + +#endif // __SRB2_AUDIO_SOUND_CHUNK_HPP__ diff --git a/src/audio/sound_effect_player.cpp b/src/audio/sound_effect_player.cpp new file mode 100644 index 000000000..a038ee3d8 --- /dev/null +++ b/src/audio/sound_effect_player.cpp @@ -0,0 +1,72 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include "sound_effect_player.hpp" + +#include +#include +#include + +using std::shared_ptr; +using std::size_t; + +using srb2::audio::Sample; +using srb2::audio::SoundEffectPlayer; +using srb2::audio::Source; + +size_t SoundEffectPlayer::generate(tcb::span> buffer) { + if (!chunk_) + return 0; + if (position_ >= chunk_->samples.size()) { + return 0; + } + + size_t written = 0; + for (; position_ < chunk_->samples.size() && written < buffer.size(); position_++) { + float mono_sample = chunk_->samples[position_].amplitudes[0]; + + float sep_pan = ((sep_ + 1.f) / 2.f) * (3.14159 / 2.f); + + float left_scale = std::cos(sep_pan); + float right_scale = std::sin(sep_pan); + buffer[written] = {mono_sample * volume_ * left_scale, mono_sample * volume_ * right_scale}; + written += 1; + } + return written; +} + +void SoundEffectPlayer::start(const SoundChunk* chunk, float volume, float sep) { + this->update(volume, sep); + position_ = 0; + chunk_ = chunk; +} + +void SoundEffectPlayer::update(float volume, float sep) { + volume_ = volume; + sep_ = sep; +} + +void SoundEffectPlayer::reset() { + position_ = 0; + chunk_ = nullptr; +} + +bool SoundEffectPlayer::finished() const { + if (!chunk_) + return true; + if (position_ >= chunk_->samples.size()) + return true; + return false; +} + +bool SoundEffectPlayer::is_playing_chunk(const SoundChunk* chunk) const { + return chunk_ == chunk; +} + +SoundEffectPlayer::~SoundEffectPlayer() = default; diff --git a/src/audio/sound_effect_player.hpp b/src/audio/sound_effect_player.hpp new file mode 100644 index 000000000..99f5edb9e --- /dev/null +++ b/src/audio/sound_effect_player.hpp @@ -0,0 +1,46 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_AUDIO_SOUND_EFFECT_PLAYER_HPP__ +#define __SRB2_AUDIO_SOUND_EFFECT_PLAYER_HPP__ + +#include + +#include + +#include "sound_chunk.hpp" +#include "source.hpp" + +namespace srb2::audio { + +class SoundEffectPlayer : public Source<2> { +public: + virtual std::size_t generate(tcb::span> buffer) override final; + + virtual ~SoundEffectPlayer() final; + + void start(const SoundChunk* chunk, float volume, float sep); + void update(float volume, float sep); + void reset(); + bool finished() const; + + bool is_playing_chunk(const SoundChunk* chunk) const; + +private: + float volume_; + float sep_; + + std::size_t position_; + + const SoundChunk* chunk_; +}; + +} // namespace srb2::audio + +#endif // __SRB2_AUDIO_SOUND_EFFECT_PLAYER_HPP__ diff --git a/src/audio/source.hpp b/src/audio/source.hpp new file mode 100644 index 000000000..ea4be8761 --- /dev/null +++ b/src/audio/source.hpp @@ -0,0 +1,36 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_AUDIO_SOURCE_HPP__ +#define __SRB2_AUDIO_SOURCE_HPP__ + +#include + +#include + +#include "sample.hpp" + +namespace srb2::audio { + +template +class Source { +public: + virtual std::size_t generate(tcb::span> buffer) = 0; + + virtual ~Source() = default; +}; + +// This audio DSP is Stereo, FP32 system-endian, 44100 Hz internally. +// Conversions to other formats should be handled elsewhere. + +constexpr const std::size_t kSampleRate = 44100; + +} // namespace srb2::audio + +#endif // __SRB2_AUDIO_SOURCE_HPP__ diff --git a/src/audio/wav.cpp b/src/audio/wav.cpp new file mode 100644 index 000000000..31f3a0468 --- /dev/null +++ b/src/audio/wav.cpp @@ -0,0 +1,264 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include "wav.hpp" + +#include +#include +#include + +using namespace srb2; +using srb2::audio::Wav; + +namespace { + +constexpr const uint32_t kMagicRIFF = 0x46464952; +constexpr const uint32_t kMagicWAVE = 0x45564157; +constexpr const uint32_t kMagicFmt = 0x20746d66; +constexpr const uint32_t kMagicData = 0x61746164; + +constexpr const uint16_t kFormatPcm = 1; + +constexpr const std::size_t kRiffHeaderLength = 8; + +struct RiffHeader { + uint32_t magic; + std::size_t filesize; +}; + +struct TagHeader { + uint32_t type; + std::size_t length; +}; + +struct FmtTag { + uint16_t format; + uint16_t channels; + uint32_t rate; + uint32_t bytes_per_second; + uint32_t bytes_per_sample; + uint16_t bit_width; +}; + +struct DataTag {}; + +RiffHeader parse_riff_header(io::SpanStream& stream) { + if (io::remaining(stream) < kRiffHeaderLength) + throw std::runtime_error("insufficient bytes remaining in stream"); + + RiffHeader ret; + ret.magic = io::read_uint32(stream); + ret.filesize = io::read_uint32(stream); + return ret; +} + +TagHeader parse_tag_header(io::SpanStream& stream) { + if (io::remaining(stream) < 8) + throw std::runtime_error("insufficient bytes remaining in stream"); + + TagHeader header; + header.type = io::read_uint32(stream); + header.length = io::read_uint32(stream); + return header; +} + +FmtTag parse_fmt_tag(io::SpanStream& stream) { + if (io::remaining(stream) < 16) + throw std::runtime_error("insufficient bytes in stream"); + + FmtTag tag; + tag.format = io::read_uint16(stream); + tag.channels = io::read_uint16(stream); + tag.rate = io::read_uint32(stream); + tag.bytes_per_second = io::read_uint32(stream); + tag.bytes_per_sample = io::read_uint16(stream); + tag.bit_width = io::read_uint16(stream); + + return tag; +} + +template +void visit_tag(Visitor& visitor, io::SpanStream& stream, const TagHeader& header) { + if (io::remaining(stream) < header.length) + throw std::runtime_error("insufficient bytes in stream"); + + const io::StreamSize start = stream.seek(io::SeekFrom::kCurrent, 0); + const io::StreamSize dest = start + header.length; + + switch (header.type) { + case kMagicFmt: + { + FmtTag fmt_tag {parse_fmt_tag(stream)}; + visitor(fmt_tag); + break; + } + case kMagicData: + { + DataTag data_tag; + visitor(data_tag); + break; + } + default: + // Unrecognized tags are ignored. + break; + } + + stream.seek(io::SeekFrom::kStart, dest); +} + +std::vector read_uint8_samples_from_stream(io::SpanStream& stream, std::size_t count) { + std::vector samples; + samples.reserve(count); + for (std::size_t i = 0; i < count; i++) { + samples.push_back(io::read_uint8(stream)); + } + return samples; +} + +std::vector read_int16_samples_from_stream(io::SpanStream& stream, std::size_t count) { + std::vector samples; + samples.reserve(count); + for (std::size_t i = 0; i < count; i++) { + samples.push_back(io::read_int16(stream)); + } + return samples; +} + +template +struct OverloadVisitor : Ts... { + using Ts::operator()...; +}; + +template +OverloadVisitor(Ts...) -> OverloadVisitor; + +} // namespace + +Wav::Wav() = default; + +Wav::Wav(tcb::span data) { + io::SpanStream stream {data}; + + auto [magic, filesize] = parse_riff_header(stream); + + if (magic != kMagicRIFF) { + throw std::runtime_error("invalid RIFF magic"); + } + + if (io::remaining(stream) < filesize) { + throw std::runtime_error("insufficient data in stream for RIFF's reported filesize"); + } + + const io::StreamSize riff_end = stream.seek(io::SeekFrom::kCurrent, 0) + filesize; + + uint32_t type = io::read_uint32(stream); + if (type != kMagicWAVE) { + throw std::runtime_error("RIFF in stream is not a WAVE"); + } + + std::optional read_fmt; + std::variant, std::vector> interleaved_samples; + + while (stream.seek(io::SeekFrom::kCurrent, 0) < riff_end) { + TagHeader tag_header {parse_tag_header(stream)}; + if (io::remaining(stream) < tag_header.length) { + throw std::runtime_error("WAVE tag length exceeds stream length"); + } + + auto tag_visitor = OverloadVisitor { + [&](const FmtTag& fmt) { + if (read_fmt) { + throw std::runtime_error("WAVE has multiple 'fmt' tags"); + } + if (fmt.format != kFormatPcm) { + throw std::runtime_error("Unsupported WAVE format (only PCM is supported)"); + } + read_fmt = fmt; + }, + [&](const DataTag& data) { + if (!read_fmt) { + throw std::runtime_error("unable to read data tag because no fmt tag was read"); + } + + if (tag_header.length % read_fmt->bytes_per_sample != 0) { + throw std::runtime_error("data tag length not divisible by bytes_per_sample"); + } + + const std::size_t sample_count = tag_header.length / read_fmt->bytes_per_sample; + + switch (read_fmt->bit_width) { + case 8: + interleaved_samples = std::move(read_uint8_samples_from_stream(stream, sample_count)); + break; + case 16: + interleaved_samples = std::move(read_int16_samples_from_stream(stream, sample_count)); + break; + default: + throw std::runtime_error("unsupported sample amplitude bit width"); + } + }}; + + visit_tag(tag_visitor, stream, tag_header); + } + + if (!read_fmt) { + throw std::runtime_error("WAVE did not have a fmt tag"); + } + + interleaved_samples_ = std::move(interleaved_samples); + channels_ = read_fmt->channels; + sample_rate_ = read_fmt->rate; +} + +namespace { + +template +std::size_t read_samples(std::size_t channels, + std::size_t offset, + const std::vector& samples, + tcb::span> buffer) noexcept { + const std::size_t offset_interleaved = offset * channels; + const std::size_t samples_size = samples.size(); + const std::size_t buffer_size = buffer.size(); + + if (offset_interleaved >= samples_size) { + return 0; + } + + const std::size_t remainder = (samples_size - offset_interleaved) / channels; + const std::size_t samples_to_read = std::min(buffer_size, remainder); + + for (std::size_t i = 0; i < samples_to_read; i++) { + buffer[i].amplitudes[0] = 0.f; + for (std::size_t j = 0; j < channels; j++) { + buffer[i].amplitudes[0] += audio::sample_to_float(samples[i * channels + j + offset_interleaved]); + } + buffer[i].amplitudes[0] /= static_cast(channels); + } + + return samples_to_read; +} + +} // namespace + +std::size_t Wav::get_samples(std::size_t offset, tcb::span> buffer) const noexcept { + auto samples_visitor = OverloadVisitor { + [&](const std::vector& samples) { return read_samples(channels(), offset, samples, buffer); }, + [&](const std::vector& samples) { + return read_samples(channels(), offset, samples, buffer); + }}; + + return std::visit(samples_visitor, interleaved_samples_); +} + +std::size_t Wav::interleaved_length() const noexcept { + auto samples_visitor = OverloadVisitor {[](const std::vector& samples) { return samples.size(); }, + [](const std::vector& samples) { return samples.size(); }}; + return std::visit(samples_visitor, interleaved_samples_); +} diff --git a/src/audio/wav.hpp b/src/audio/wav.hpp new file mode 100644 index 000000000..e571969e7 --- /dev/null +++ b/src/audio/wav.hpp @@ -0,0 +1,51 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_AUDIO_WAV_HPP__ +#define __SRB2_AUDIO_WAV_HPP__ + +#include +#include +#include +#include +#include + +#include + +#include "../io/streams.hpp" +#include "sample.hpp" + +namespace srb2::audio { + +class Wav final { + std::variant, std::vector> interleaved_samples_; + std::size_t channels_ = 1; + std::size_t sample_rate_ = 44100; + +public: + Wav(); + + explicit Wav(tcb::span data); + + std::size_t get_samples(std::size_t offset, tcb::span> buffer) const noexcept; + std::size_t interleaved_length() const noexcept; + std::size_t length() const noexcept { return interleaved_length() / channels(); }; + std::size_t channels() const noexcept { return channels_; }; + std::size_t sample_rate() const noexcept { return sample_rate_; }; +}; + +template , int> = 0> +inline Wav load_wav(I& stream) { + std::vector data = srb2::io::read_to_vec(stream); + return Wav {data}; +} + +} // namespace srb2::audio + +#endif // __SRB2_AUDIO_WAV_HPP__ diff --git a/src/audio/wav_player.cpp b/src/audio/wav_player.cpp new file mode 100644 index 000000000..64f9831f0 --- /dev/null +++ b/src/audio/wav_player.cpp @@ -0,0 +1,45 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include "wav_player.hpp" + +using namespace srb2; + +using srb2::audio::WavPlayer; + +WavPlayer::WavPlayer() : WavPlayer(audio::Wav {}) { +} + +WavPlayer::WavPlayer(const WavPlayer& rhs) = default; + +WavPlayer::WavPlayer(WavPlayer&& rhs) noexcept = default; + +WavPlayer& WavPlayer::operator=(const WavPlayer& rhs) = default; + +WavPlayer& WavPlayer::operator=(WavPlayer&& rhs) noexcept = default; + +WavPlayer::WavPlayer(audio::Wav&& wav) noexcept : wav_(std::forward(wav)), position_(0), looping_(false) { +} + +std::size_t WavPlayer::generate(tcb::span> buffer) { + std::size_t samples_read = 0; + while (samples_read < buffer.size()) { + const std::size_t read_this_time = wav_.get_samples(position_, buffer.subspan(samples_read)); + position_ += read_this_time; + samples_read += read_this_time; + + if (position_ > wav_.length() && looping_) { + position_ = 0; + } + if (read_this_time == 0 && !looping_) { + break; + } + } + return samples_read; +} diff --git a/src/audio/wav_player.hpp b/src/audio/wav_player.hpp new file mode 100644 index 000000000..dc6a98864 --- /dev/null +++ b/src/audio/wav_player.hpp @@ -0,0 +1,49 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_AUDIO_WAV_PLAYER_HPP__ +#define __SRB2_AUDIO_WAV_PLAYER_HPP__ + +#include + +#include + +#include "source.hpp" +#include "wav.hpp" + +namespace srb2::audio { + +class WavPlayer final : public Source<1> { + Wav wav_; + std::size_t position_; + bool looping_; + +public: + WavPlayer(); + WavPlayer(const WavPlayer& rhs); + WavPlayer(WavPlayer&& rhs) noexcept; + + WavPlayer& operator=(const WavPlayer& rhs); + WavPlayer& operator=(WavPlayer&& rhs) noexcept; + + WavPlayer(Wav&& wav) noexcept; + + virtual std::size_t generate(tcb::span> buffer) override; + + bool looping() const { return looping_; } + void looping(bool looping) { looping_ = looping; } + + std::size_t sample_rate() const { return wav_.sample_rate(); } + float duration_seconds() const { return wav_.length() / static_cast(wav_.sample_rate()); } + void seek(float seconds) { position_ = seconds * wav_.sample_rate(); } +}; + +} // namespace srb2::audio + +#endif // __SRB2_AUDIO_WAV_PLAYER_HPP__ diff --git a/src/audio/xmp.cpp b/src/audio/xmp.cpp new file mode 100644 index 000000000..9e88f2a7f --- /dev/null +++ b/src/audio/xmp.cpp @@ -0,0 +1,167 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include "xmp.hpp" + +#include + +#include "../cxxutil.hpp" + +using namespace srb2; +using namespace srb2::audio; + +XmpException::XmpException(int code) : code_(code) { +} + +const char* XmpException::what() const noexcept { + switch (code_) { + case -XMP_ERROR_INTERNAL: + return "XMP_ERROR_INTERNAL"; + case -XMP_ERROR_FORMAT: + return "XMP_ERROR_FORMAT"; + case -XMP_ERROR_LOAD: + return "XMP_ERROR_LOAD"; + case -XMP_ERROR_DEPACK: + return "XMP_ERROR_DEPACK"; + case -XMP_ERROR_SYSTEM: + return "XMP_ERROR_SYSTEM"; + case -XMP_ERROR_INVALID: + return "XMP_ERROR_INVALID"; + case -XMP_ERROR_STATE: + return "XMP_ERROR_STATE"; + default: + return "unknown"; + } +} + +template +Xmp::Xmp() : data_(), instance_(nullptr), module_loaded_(false), looping_(false) { +} + +template +Xmp::Xmp(std::vector data) + : data_(std::move(data)), instance_(nullptr), module_loaded_(false), looping_(false) { + _init(); +} + +template +Xmp::Xmp(tcb::span data) + : data_(data.begin(), data.end()), instance_(nullptr), module_loaded_(false), looping_(false) { + _init(); +} + +template +Xmp::Xmp(Xmp&& rhs) noexcept : Xmp() { + std::swap(data_, rhs.data_); + std::swap(instance_, rhs.instance_); + std::swap(module_loaded_, rhs.module_loaded_); + std::swap(looping_, rhs.looping_); +} + +template +Xmp& Xmp::operator=(Xmp&& rhs) noexcept { + std::swap(data_, rhs.data_); + std::swap(instance_, rhs.instance_); + std::swap(module_loaded_, rhs.module_loaded_); + std::swap(looping_, rhs.looping_); + + return *this; +}; + +template +Xmp::~Xmp() { + if (instance_) { + xmp_free_context(instance_); + instance_ = nullptr; + } +} + +template +std::size_t Xmp::play_buffer(tcb::span> buffer) { + SRB2_ASSERT(instance_ != nullptr); + SRB2_ASSERT(module_loaded_ == true); + + int result = xmp_play_buffer(instance_, buffer.data(), buffer.size_bytes(), !looping_); + + if (result == -XMP_END) + return 0; + + if (result != 0) + throw XmpException(result); + + return buffer.size(); +} + +template +void Xmp::reset() { + SRB2_ASSERT(instance_ != nullptr); + SRB2_ASSERT(module_loaded_ == true); + + xmp_restart_module(instance_); +} + +template +float Xmp::duration_seconds() const { + SRB2_ASSERT(instance_ != nullptr); + SRB2_ASSERT(module_loaded_ == true); + + xmp_frame_info info; + xmp_get_frame_info(instance_, &info); + return static_cast(info.total_time) / 1000.f; +} + +template +void Xmp::seek(int position_ms) { + SRB2_ASSERT(instance_ != nullptr); + SRB2_ASSERT(module_loaded_ == true); + + int err = xmp_seek_time(instance_, position_ms); + if (err != 0) + throw XmpException(err); +} + +template +void Xmp::_init() { + if (instance_) + return; + + if (data_.size() >= std::numeric_limits::max()) + throw std::logic_error("Buffer is too large for xmp"); + if (data_.size() == 0) + throw std::logic_error("Insufficient data from stream"); + + instance_ = xmp_create_context(); + if (instance_ == nullptr) { + throw std::bad_alloc(); + } + + int result = xmp_load_module_from_memory(instance_, data_.data(), data_.size()); + if (result != 0) { + xmp_free_context(instance_); + instance_ = nullptr; + throw XmpException(result); + } + module_loaded_ = true; + + int flags = 0; + if constexpr (C == 1) { + flags |= XMP_FORMAT_MONO; + } + result = xmp_start_player(instance_, 44100, flags); + if (result != 0) { + xmp_release_module(instance_); + module_loaded_ = false; + xmp_free_context(instance_); + instance_ = nullptr; + throw XmpException(result); + } +} + +template class srb2::audio::Xmp<1>; +template class srb2::audio::Xmp<2>; diff --git a/src/audio/xmp.hpp b/src/audio/xmp.hpp new file mode 100644 index 000000000..a5b443bfa --- /dev/null +++ b/src/audio/xmp.hpp @@ -0,0 +1,78 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + + #ifndef __SRB2_AUDIO_XMP_HPP__ +#define __SRB2_AUDIO_XMP_HPP__ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "../io/streams.hpp" + +namespace srb2::audio { + +class XmpException : public std::exception { + int code_; + +public: + XmpException(int code); + virtual const char* what() const noexcept override final; +}; + +template +class Xmp final { + std::vector data_; + xmp_context instance_; + bool module_loaded_; + bool looping_; + +public: + Xmp(); + + explicit Xmp(std::vector data); + explicit Xmp(tcb::span data); + + Xmp(const Xmp&) = delete; + Xmp(Xmp&& rhs) noexcept; + + Xmp& operator=(const Xmp&) = delete; + Xmp& operator=(Xmp&& rhs) noexcept; + + std::size_t play_buffer(tcb::span> buffer); + bool looping() const { return looping_; }; + void looping(bool looping) { looping_ = looping; }; + void reset(); + float duration_seconds() const; + void seek(int position_ms); + + ~Xmp(); + +private: + void _init(); +}; + +extern template class Xmp<1>; +extern template class Xmp<2>; + +template , int> = 0> +inline Xmp load_xmp(I& stream) { + std::vector data = srb2::io::read_to_vec(stream); + return Xmp {std::move(data)}; +} + +} // namespace srb2::audio + +#endif // __SRB2_AUDIO_XMP_HPP__ diff --git a/src/audio/xmp_player.cpp b/src/audio/xmp_player.cpp new file mode 100644 index 000000000..a08ee8bb5 --- /dev/null +++ b/src/audio/xmp_player.cpp @@ -0,0 +1,57 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include "xmp_player.hpp" + +#include + +using namespace srb2; +using namespace srb2::audio; + +template +XmpPlayer::XmpPlayer(Xmp&& xmp) : xmp_(std::move(xmp)), buf_() { +} + +template +XmpPlayer::XmpPlayer(XmpPlayer&& rhs) noexcept = default; + +template +XmpPlayer& XmpPlayer::operator=(XmpPlayer&& rhs) noexcept = default; + +template +XmpPlayer::~XmpPlayer() = default; + +template +std::size_t XmpPlayer::generate(tcb::span> buffer) { + buf_.resize(buffer.size()); + std::size_t read = xmp_.play_buffer(tcb::make_span(buf_)); + buf_.resize(read); + std::size_t ret = std::min(buffer.size(), buf_.size()); + + for (std::size_t i = 0; i < ret; i++) { + for (std::size_t j = 0; j < C; j++) { + buffer[i].amplitudes[j] = buf_[i][j] / 32768.f; + } + } + + return ret; +} + +template +float XmpPlayer::duration_seconds() const { + return xmp_.duration_seconds(); +} + +template +void XmpPlayer::seek(float position_seconds) { + xmp_.seek(static_cast(std::round(position_seconds * 1000.f))); +} + +template class srb2::audio::XmpPlayer<1>; +template class srb2::audio::XmpPlayer<2>; diff --git a/src/audio/xmp_player.hpp b/src/audio/xmp_player.hpp new file mode 100644 index 000000000..5829dbd8a --- /dev/null +++ b/src/audio/xmp_player.hpp @@ -0,0 +1,48 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_AUDIO_XMP_PLAYER_HPP__ +#define __SRB2_AUDIO_XMP_PLAYER_HPP__ + +#include "source.hpp" +#include "xmp.hpp" + +namespace srb2::audio { + +template +class XmpPlayer final : public Source { + Xmp xmp_; + std::vector> buf_; + +public: + XmpPlayer(Xmp&& xmp); + + XmpPlayer(const XmpPlayer&) = delete; + XmpPlayer(XmpPlayer&& rhs) noexcept; + + XmpPlayer& operator=(const XmpPlayer&) = delete; + XmpPlayer& operator=(XmpPlayer&& rhs) noexcept; + + ~XmpPlayer(); + + virtual std::size_t generate(tcb::span> buffer) override final; + + bool looping() { return xmp_.looping(); }; + void looping(bool looping) { xmp_.looping(looping); } + void reset() { xmp_.reset(); } + float duration_seconds() const; + void seek(float position_seconds); +}; + +extern template class XmpPlayer<1>; +extern template class XmpPlayer<2>; + +} // namespace srb2::audio + +#endif // __SRB2_AUDIO_XMP_PLAYER_HPP__ diff --git a/src/sdl/CMakeLists.txt b/src/sdl/CMakeLists.txt index 61d2e1a8f..669f4710f 100644 --- a/src/sdl/CMakeLists.txt +++ b/src/sdl/CMakeLists.txt @@ -1,7 +1,7 @@ # Declare SDL2 interface sources target_sources(SRB2SDL2 PRIVATE - mixer_sound.c + new_sound.cpp ogl_sdl.c i_threads.c i_net.c diff --git a/src/sdl/new_sound.cpp b/src/sdl/new_sound.cpp new file mode 100644 index 000000000..3052ee587 --- /dev/null +++ b/src/sdl/new_sound.cpp @@ -0,0 +1,665 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include +#include +#include + +#include + +#include "../audio/chunk_load.hpp" +#include "../audio/gain.hpp" +#include "../audio/mixer.hpp" +#include "../audio/music_player.hpp" +#include "../audio/sound_chunk.hpp" +#include "../audio/sound_effect_player.hpp" +#include "../cxxutil.hpp" +#include "../io/streams.hpp" + +#include "../doomdef.h" +#include "../i_sound.h" +#include "../s_sound.h" +#include "../sounds.h" +#include "../w_wad.h" +#include "../z_zone.h" + +using std::make_shared; +using std::make_unique; +using std::shared_ptr; +using std::unique_ptr; +using std::vector; + +using srb2::audio::Gain; +using srb2::audio::Mixer; +using srb2::audio::MusicPlayer; +using srb2::audio::Sample; +using srb2::audio::SoundChunk; +using srb2::audio::SoundEffectPlayer; +using srb2::audio::Source; +using namespace srb2; +using namespace srb2::io; + +// extern in i_sound.h +UINT8 sound_started = false; + +static unique_ptr> master; +static shared_ptr> mixer_sound_effects; +static shared_ptr> mixer_music; +static shared_ptr music_player; +static shared_ptr> gain_sound_effects; +static shared_ptr> gain_music; + +static vector> sound_effect_channels; + +static void (*music_fade_callback)(); + +void* I_GetSfx(sfxinfo_t* sfx) { + if (sfx->lumpnum == LUMPERROR) + sfx->lumpnum = S_GetSfxLumpNum(sfx); + sfx->length = W_LumpLength(sfx->lumpnum); + + std::byte* lump = static_cast(W_CacheLumpNum(sfx->lumpnum, PU_SOUND)); + auto _ = srb2::finally([lump]() { Z_Free(lump); }); + + tcb::span data_span(lump, sfx->length); + std::optional chunk = srb2::audio::try_load_chunk(data_span); + + if (!chunk) + return nullptr; + + SoundChunk* heap_chunk = new SoundChunk {std::move(*chunk)}; + + return heap_chunk; +} + +void I_FreeSfx(sfxinfo_t* sfx) { + if (sfx->data) { + SoundChunk* chunk = static_cast(sfx->data); + auto _ = srb2::finally([chunk]() { delete chunk; }); + + // Stop any channels playing this chunk + for (auto& player : sound_effect_channels) { + if (player->is_playing_chunk(chunk)) { + player->reset(); + } + } + } + sfx->data = nullptr; + sfx->lumpnum = LUMPERROR; +} + +namespace { + +class SdlAudioLockHandle { +public: + SdlAudioLockHandle() { SDL_LockAudio(); } + ~SdlAudioLockHandle() { SDL_UnlockAudio(); } +}; + +void audio_callback(void* userdata, Uint8* buffer, int len) { + // The SDL Audio lock is implied to be held during callback. + + try { + Sample<2>* float_buffer = reinterpret_cast*>(buffer); + size_t float_len = len / 8; + + for (size_t i = 0; i < float_len; i++) { + float_buffer[i] = Sample<2> {0.f, 0.f}; + } + + if (!master) + return; + + master->generate(tcb::span {float_buffer, float_len}); + + for (size_t i = 0; i < float_len; i++) { + float_buffer[i] = { + std::clamp(float_buffer[i].amplitudes[0], -1.f, 1.f), + std::clamp(float_buffer[i].amplitudes[1], -1.f, 1.f), + }; + } + } catch (...) { + } + + return; +} + +void initialize_sound() { + if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { + CONS_Alert(CONS_ERROR, "Error initializing SDL Audio: %s\n", SDL_GetError()); + return; + } + + SDL_AudioSpec desired; + desired.format = AUDIO_F32SYS; + desired.channels = 2; + desired.samples = 1024; + desired.freq = 44100; + desired.callback = audio_callback; + + if (SDL_OpenAudio(&desired, NULL) < 0) { + CONS_Alert(CONS_ERROR, "Failed to open SDL Audio device: %s\n", SDL_GetError()); + SDL_QuitSubSystem(SDL_INIT_AUDIO); + return; + } + + SDL_PauseAudio(SDL_FALSE); + + { + SdlAudioLockHandle _; + + master = make_unique>(); + mixer_sound_effects = make_shared>(); + mixer_music = make_shared>(); + music_player = make_shared(); + gain_sound_effects = make_shared>(); + gain_music = make_shared>(); + gain_sound_effects->bind(mixer_sound_effects); + gain_music->bind(mixer_music); + master->add_source(gain_sound_effects); + master->add_source(gain_music); + mixer_music->add_source(music_player); + for (size_t i = 0; i < static_cast(cv_numChannels.value); i++) { + shared_ptr player = make_shared(); + sound_effect_channels.push_back(player); + mixer_sound_effects->add_source(player); + } + } + + sound_started = true; +} + +} // namespace + +void I_StartupSound(void) { + if (!sound_started) + initialize_sound(); +} + +void I_ShutdownSound(void) { + SdlAudioLockHandle _; + + for (auto& channel : sound_effect_channels) { + *channel = audio::SoundEffectPlayer(); + } +} + +void I_UpdateSound(void) { + // The SDL audio lock is re-entrant, so it is safe to lock twice + // for the "fade to stop music" callback later. + SdlAudioLockHandle _; + + if (music_fade_callback && !music_player->fading()) { + auto old_callback = music_fade_callback; + music_fade_callback = nullptr; + (old_callback()); + } + return; +} + +// +// SFX I/O +// + +INT32 I_StartSound(sfxenum_t id, UINT8 vol, UINT8 sep, UINT8 pitch, UINT8 priority, INT32 channel) { + (void) pitch; + (void) priority; + + SdlAudioLockHandle _; + + if (channel >= 0 && static_cast(channel) >= sound_effect_channels.size()) + return -1; + + shared_ptr player_channel; + if (channel < 0) { + // find a free sfx channel + for (size_t i = 0; i < sound_effect_channels.size(); i++) { + if (sound_effect_channels[i]->finished()) { + player_channel = sound_effect_channels[i]; + channel = i; + break; + } + } + } else { + player_channel = sound_effect_channels[channel]; + } + + if (!player_channel) + return -1; + + SoundChunk* chunk = static_cast(S_sfx[id].data); + if (chunk == nullptr) + return -1; + + float vol_float = static_cast(vol) / 255.f; + float sep_float = static_cast(sep) / 127.f - 1.f; + + player_channel->start(chunk, vol_float, sep_float); + + return channel; +} + +void I_StopSound(INT32 handle) { + SdlAudioLockHandle _; + + if (sound_effect_channels.empty()) + return; + + if (handle < 0) + return; + + size_t index = handle; + + if (index >= sound_effect_channels.size()) + return; + + sound_effect_channels[index]->reset(); +} + +boolean I_SoundIsPlaying(INT32 handle) { + SdlAudioLockHandle _; + + // Handle is channel index + if (sound_effect_channels.empty()) + return 0; + + if (handle < 0) + return 0; + + size_t index = handle; + + if (index >= sound_effect_channels.size()) + return 0; + + return sound_effect_channels[index]->finished() ? 0 : 1; +} + +void I_UpdateSoundParams(INT32 handle, UINT8 vol, UINT8 sep, UINT8 pitch) { + (void) pitch; + + SdlAudioLockHandle _; + + if (sound_effect_channels.empty()) + return; + + if (handle < 0) + return; + + size_t index = handle; + + if (index >= sound_effect_channels.size()) + return; + + shared_ptr& channel = sound_effect_channels[index]; + if (!channel->finished()) { + float vol_float = static_cast(vol) / 255.f; + float sep_float = static_cast(sep) / 127.f - 1.f; + channel->update(vol_float, sep_float); + } +} + +void I_SetSfxVolume(int volume) { + SdlAudioLockHandle _; + float vol = static_cast(volume) / 100.f; + + if (gain_sound_effects) { + gain_sound_effects->gain(vol * vol * vol); + } +} + +/// ------------------------ +// MUSIC SYSTEM +/// ------------------------ + +void I_InitMusic(void) { + if (!sound_started) + initialize_sound(); + + SdlAudioLockHandle _; + + *music_player = audio::MusicPlayer(); +} + +void I_ShutdownMusic(void) { + SdlAudioLockHandle _; + + *music_player = audio::MusicPlayer(); +} + +/// ------------------------ +// MUSIC PROPERTIES +/// ------------------------ + +musictype_t I_SongType(void) { + if (!music_player) + return MU_NONE; + + SdlAudioLockHandle _; + + std::optional music_type = music_player->music_type(); + + if (music_type == std::nullopt) { + return MU_NONE; + } + + switch (*music_type) { + case audio::MusicType::kOgg: + return MU_OGG; + case audio::MusicType::kGme: + return MU_GME; + case audio::MusicType::kMod: + return MU_MOD; + default: + return MU_NONE; + } +} + +boolean I_SongPlaying(void) { + if (!music_player) + return false; + + SdlAudioLockHandle _; + + return music_player->music_type().has_value(); +} + +boolean I_SongPaused(void) { + if (!music_player) + return false; + + SdlAudioLockHandle _; + + return !music_player->playing(); +} + +/// ------------------------ +// MUSIC EFFECTS +/// ------------------------ + +boolean I_SetSongSpeed(float speed) { + (void) speed; + return false; +} + +/// ------------------------ +// MUSIC SEEKING +/// ------------------------ + +UINT32 I_GetSongLength(void) { + if (!music_player) + return 0; + + SdlAudioLockHandle _; + + std::optional duration = music_player->duration_seconds(); + + if (!duration) + return 0; + + return static_cast(std::round(*duration * 1000.f)); +} + +boolean I_SetSongLoopPoint(UINT32 looppoint) { + if (!music_player) + return 0; + + SdlAudioLockHandle _; + + if (music_player->music_type() == audio::MusicType::kOgg) { + music_player->loop_point_seconds(looppoint / 1000.f); + return true; + } + + return false; +} + +UINT32 I_GetSongLoopPoint(void) { + if (!music_player) + return 0; + + SdlAudioLockHandle _; + + std::optional loop_point_seconds = music_player->loop_point_seconds(); + + if (!loop_point_seconds) + return 0; + + return static_cast(std::round(*loop_point_seconds * 1000.f)); +} + +boolean I_SetSongPosition(UINT32 position) { + if (!music_player) + return false; + + SdlAudioLockHandle _; + + music_player->seek(position / 1000.f); + return true; +} + +UINT32 I_GetSongPosition(void) { + if (!music_player) + return 0; + + SdlAudioLockHandle _; + + std::optional position_seconds = music_player->position_seconds(); + + if (!position_seconds) + return 0; + + return static_cast(std::round(*position_seconds * 1000.f)); +} + +void I_UpdateSongLagThreshold(void) { +} + +void I_UpdateSongLagConditions(void) { +} + +/// ------------------------ +// MUSIC PLAYBACK +/// ------------------------ + +namespace { +void print_walk_ex_stack(const std::exception& ex) { + CONS_Alert(CONS_WARNING, " Caused by: %s\n", ex.what()); + try { + std::rethrow_if_nested(ex); + } catch (const std::exception& ex) { + print_walk_ex_stack(ex); + } +} + +void print_ex(const std::exception& ex) { + CONS_Alert(CONS_WARNING, "Exception loading music: %s\n", ex.what()); + try { + std::rethrow_if_nested(ex); + } catch (const std::exception& ex) { + print_walk_ex_stack(ex); + } +} +} // namespace + +boolean I_LoadSong(char* data, size_t len) { + if (!music_player) + return false; + + tcb::span data_span(reinterpret_cast(data), len); + audio::MusicPlayer new_player; + try { + new_player = audio::MusicPlayer {data_span}; + } catch (const std::exception& ex) { + print_ex(ex); + return false; + } + + if (music_fade_callback && music_player->fading()) { + auto old_callback = music_fade_callback; + music_fade_callback = nullptr; + (old_callback)(); + } + + SdlAudioLockHandle _; + + try { + *music_player = std::move(new_player); + } catch (const std::exception& ex) { + print_ex(ex); + return false; + } + + return true; +} + +void I_UnloadSong(void) { + if (!music_player) + return; + + if (music_fade_callback && music_player->fading()) { + auto old_callback = music_fade_callback; + music_fade_callback = nullptr; + (old_callback)(); + } + + SdlAudioLockHandle _; + + *music_player = audio::MusicPlayer(); +} + +boolean I_PlaySong(boolean looping) { + if (!music_player) + return false; + + SdlAudioLockHandle _; + + music_player->play(looping); + + return true; +} + +void I_StopSong(void) { + if (!music_player) + return; + + SdlAudioLockHandle _; + + music_player->stop(); +} + +void I_PauseSong(void) { + if (!music_player) + return; + + SdlAudioLockHandle _; + + music_player->pause(); +} + +void I_ResumeSong(void) { + if (!music_player) + return; + + SdlAudioLockHandle _; + + music_player->unpause(); +} + +void I_SetMusicVolume(int volume) { + float vol = static_cast(volume) / 100.f; + + if (gain_music) { + gain_music->gain(vol * vol * vol); + } +} + +boolean I_SetSongTrack(int track) { + (void) track; + return false; +} + +/// ------------------------ +// MUSIC FADING +/// ------------------------ + +void I_SetInternalMusicVolume(UINT8 volume) { + if (!music_player) + return; + + SdlAudioLockHandle _; + + float gain = volume / 100.f; + music_player->internal_gain(gain); +} + +void I_StopFadingSong(void) { + if (!music_player) + return; + + SdlAudioLockHandle _; + + music_player->stop_fade(); +} + +boolean I_FadeSongFromVolume(UINT8 target_volume, UINT8 source_volume, UINT32 ms, void (*callback)(void)) { + if (!music_player) + return false; + + SdlAudioLockHandle _; + + float source_gain = source_volume / 100.f; + float target_gain = target_volume / 100.f; + float seconds = ms / 1000.f; + + music_player->fade_from_to(source_gain, target_gain, seconds); + + if (music_fade_callback) + music_fade_callback(); + music_fade_callback = callback; + + return true; +} + +boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void)) { + if (!music_player) + return false; + + SdlAudioLockHandle _; + + float target_gain = target_volume / 100.f; + float seconds = ms / 1000.f; + + music_player->fade_to(target_gain, seconds); + + if (music_fade_callback) + music_fade_callback(); + music_fade_callback = callback; + + return true; +} + +static void stop_song_cb(void) { + if (!music_player) + return; + + SdlAudioLockHandle _; + + music_player->stop(); +} + +boolean I_FadeOutStopSong(UINT32 ms) { + return I_FadeSong(0.f, ms, stop_song_cb); +} + +boolean I_FadeInPlaySong(UINT32 ms, boolean looping) { + if (I_PlaySong(looping)) + return I_FadeSongFromVolume(100, 0, ms, nullptr); + else + return false; +}