diff --git a/src/openrct2-ui/CMakeLists.txt b/src/openrct2-ui/CMakeLists.txt index de9defa8da..10717a34d1 100644 --- a/src/openrct2-ui/CMakeLists.txt +++ b/src/openrct2-ui/CMakeLists.txt @@ -15,7 +15,7 @@ else () PKG_CHECK_MODULES(SDL2 REQUIRED IMPORTED_TARGET sdl2) PKG_CHECK_MODULES(SPEEX REQUIRED IMPORTED_TARGET speexdsp) PKG_CHECK_MODULES(OGG REQUIRED IMPORTED_TARGET ogg) - PKG_CHECK_MODULES(VORBIS REQUIRED IMPORTED_TARGET vorbis) + PKG_CHECK_MODULES(VORBISFILE REQUIRED IMPORTED_TARGET vorbisfile) PKG_CHECK_MODULES(FLAC REQUIRED IMPORTED_TARGET flac) endif () @@ -52,7 +52,7 @@ if (NOT MSVC AND NOT WIN32) PkgConfig::SDL2 PkgConfig::SPEEX PkgConfig::OGG - PkgConfig::VORBIS + PkgConfig::VORBISFILE PkgConfig::FLAC) else () target_link_libraries(${PROJECT_NAME} "libopenrct2" diff --git a/src/openrct2-ui/audio/AudioContext.cpp b/src/openrct2-ui/audio/AudioContext.cpp index 48c4398061..a1b7697d1d 100644 --- a/src/openrct2-ui/audio/AudioContext.cpp +++ b/src/openrct2-ui/audio/AudioContext.cpp @@ -21,6 +21,19 @@ namespace OpenRCT2::Audio { + enum class AudioCodecKind + { + Wav, + Ogg, + Flac, + }; + + [[nodiscard]] int32_t ISDLAudioSource::GetBytesPerSecond() const + { + auto format = GetFormat(); + return format.GetBytesPerSecond(); + } + class AudioContext final : public IAudioContext { private: @@ -90,6 +103,27 @@ namespace OpenRCT2::Audio return AddSource(AudioSource::CreateMemoryFromCSS1(rw, index, &format)); } + static AudioCodecKind GetAudioCodec(SDL_RWops* rw) + { + constexpr uint32_t MAGIC_FLAC = 0x43614C66; + constexpr uint32_t MAGIC_OGG = 0x5367674F; + + auto magic = SDL_ReadLE32(rw); + SDL_RWseek(rw, -4, RW_SEEK_CUR); + if (magic == MAGIC_FLAC) + { + return AudioCodecKind::Flac; + } + else if (magic == MAGIC_OGG) + { + return AudioCodecKind::Ogg; + } + else + { + return AudioCodecKind::Wav; + } + } + IAudioSource* CreateStreamFromWAV(std::unique_ptr stream) override { constexpr size_t STREAM_MIN_SIZE = 2 * 1024 * 1024; // 2 MiB @@ -100,9 +134,21 @@ namespace OpenRCT2::Audio return nullptr; } - return AddSource( - loadIntoRAM ? AudioSource::CreateMemoryFromWAV(rw, &_audioMixer->GetFormat()) - : AudioSource::CreateStreamFromWAV(rw)); + auto codec = GetAudioCodec(rw); + switch (codec) + { + case AudioCodecKind::Wav: + return AddSource( + loadIntoRAM ? AudioSource::CreateMemoryFromWAV(rw, &_audioMixer->GetFormat()) + : AudioSource::CreateStreamFromWAV(rw)); + case AudioCodecKind::Flac: + return AddSource(AudioSource::CreateStreamFromFlac(rw)); + case AudioCodecKind::Ogg: + return AddSource(AudioSource::CreateStreamFromOgg(rw)); + default: + log_verbose("Unsupported audio codec"); + return nullptr; + } } void StartTitleMusic() override diff --git a/src/openrct2-ui/audio/AudioContext.h b/src/openrct2-ui/audio/AudioContext.h index 9b96a11a53..92d0ad556d 100644 --- a/src/openrct2-ui/audio/AudioContext.h +++ b/src/openrct2-ui/audio/AudioContext.h @@ -51,6 +51,8 @@ namespace OpenRCT2::Audio struct ISDLAudioSource : public IAudioSource { [[nodiscard]] virtual AudioFormat GetFormat() const abstract; + + [[nodiscard]] int32_t GetBytesPerSecond() const override; }; struct ISDLAudioChannel : public IAudioChannel @@ -70,6 +72,7 @@ namespace OpenRCT2::Audio std::unique_ptr CreateStreamFromWAV(const std::string& path); std::unique_ptr CreateStreamFromWAV(SDL_RWops* rw); std::unique_ptr CreateStreamFromFlac(SDL_RWops* rw); + std::unique_ptr CreateStreamFromOgg(SDL_RWops* rw); } // namespace AudioSource namespace AudioChannel diff --git a/src/openrct2-ui/audio/AudioFormat.h b/src/openrct2-ui/audio/AudioFormat.h index db4ab45520..8f1a767edd 100644 --- a/src/openrct2-ui/audio/AudioFormat.h +++ b/src/openrct2-ui/audio/AudioFormat.h @@ -33,6 +33,11 @@ namespace OpenRCT2::Audio { return BytesPerSample() * channels; } + + [[nodiscard]] int32_t GetBytesPerSecond() const + { + return BytesPerSample() * channels * freq; + } }; inline bool operator==(const AudioFormat& lhs, const AudioFormat& rhs) diff --git a/src/openrct2-ui/audio/FileAudioSource.cpp b/src/openrct2-ui/audio/FileAudioSource.cpp index a2b34aab1c..685efb7ec7 100644 --- a/src/openrct2-ui/audio/FileAudioSource.cpp +++ b/src/openrct2-ui/audio/FileAudioSource.cpp @@ -208,13 +208,6 @@ namespace OpenRCT2::Audio std::unique_ptr AudioSource::CreateStreamFromWAV(SDL_RWops* rw) { - auto magic = SDL_ReadLE32(rw); - SDL_RWseek(rw, -4, RW_SEEK_CUR); - if (magic == 0x43614C66) - { - return AudioSource::CreateStreamFromFlac(rw); - } - auto source = std::make_unique(); if (!source->LoadWAV(rw)) { diff --git a/src/openrct2-ui/audio/OggStream.cpp b/src/openrct2-ui/audio/OggStream.cpp new file mode 100644 index 0000000000..e7b0d0b9c5 --- /dev/null +++ b/src/openrct2-ui/audio/OggStream.cpp @@ -0,0 +1,177 @@ +/***************************************************************************** + * Copyright (c) 2014-2022 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#include "AudioContext.h" +#include "AudioFormat.h" + +#include +#include +#include +#include +#include +#include + +namespace OpenRCT2::Audio +{ + /** + * An audio source which decodes a OGG/Vorbis stream. + */ + class OggAudioSource final : public ISDLAudioSource + { + private: + AudioFormat _format = {}; + SDL_RWops* _rw = nullptr; + bool _released{}; + + std::optional _file; + uint64_t _dataLength{}; + uint32_t _totalSamples{}; + int32_t _section{}; + std::vector _decodeBuffer; + size_t _decodeBufferReadOffset{}; + size_t _currentOffset{}; + + public: + ~OggAudioSource() override + { + Release(); + } + + bool IsReleased() const override + { + return _released; + } + + void Release() override + { + if (!_released) + { + Unload(); + _released = true; + } + } + + [[nodiscard]] uint64_t GetLength() const override + { + return _dataLength; + } + + [[nodiscard]] AudioFormat GetFormat() const override + { + return _format; + } + + bool LoadOgg(SDL_RWops* rw) + { + _rw = rw; + + ov_callbacks callbacks{}; + callbacks.read_func = VorbisCallbackRead; + callbacks.tell_func = VorbisCallbackTell; + callbacks.seek_func = VorbisCallbackSeek; + _file.emplace(); + if (ov_open_callbacks(_rw, &*_file, NULL, 0, callbacks) < 0) + { + log_verbose("Could not open OGG/Vorbis stream"); + return false; + } + + auto vi = ov_info(&*_file, -1); + if (vi == nullptr) + { + log_verbose("Failed to get OGG/Vorbis info"); + return false; + } + + _format.format = AUDIO_S16LSB; + _format.channels = vi->channels; + _format.freq = vi->rate; + _totalSamples = ov_pcm_total(&*_file, -1); + _dataLength = _totalSamples * _format.channels * sizeof(int16_t); + _currentOffset = 0; + _section = -1; + return true; + } + + size_t Read(void* dst, uint64_t offset, size_t len) override + { + if (!_file) + return 0; + + if (_currentOffset != offset) + { + // We have been asked for a new position in the stream + auto byteRate = _format.GetByteRate(); + auto sampleIndex = offset / byteRate; + ov_pcm_seek(&*_file, sampleIndex); + _currentOffset = offset; + } + + auto readLen = static_cast(len); + auto dst8 = reinterpret_cast(dst); + int64_t totalBytesRead{}; + int64_t bytesRead; + do + { + int section = _section; + bytesRead = ov_read(&*_file, dst8, readLen, SDL_BYTEORDER == SDL_BIG_ENDIAN, 2, 1, §ion); + if (_section != section) + { + _section = section; + } + assert(bytesRead <= readLen); + dst8 += bytesRead; + readLen -= bytesRead; + totalBytesRead += bytesRead; + } while (bytesRead > 0 && readLen > 0); + + _currentOffset += totalBytesRead; + return totalBytesRead; + } + + private: + void Unload() + { + if (_file) + { + ov_clear(&*_file); + } + if (_rw != nullptr) + { + SDL_RWclose(_rw); + _rw = nullptr; + } + } + + static size_t VorbisCallbackRead(void* ptr, size_t size, size_t nmemb, void* datasource) + { + return SDL_RWread(reinterpret_cast(datasource), ptr, size, nmemb); + } + + static int VorbisCallbackSeek(void* datasource, ogg_int64_t offset, int whence) + { + return (SDL_RWseek(reinterpret_cast(datasource), offset, whence) < 0) ? -1 : 0; + } + + static long VorbisCallbackTell(void* datasource) + { + return static_cast(SDL_RWtell(reinterpret_cast(datasource))); + } + }; + + std::unique_ptr AudioSource::CreateStreamFromOgg(SDL_RWops* rw) + { + auto source = std::make_unique(); + if (!source->LoadOgg(rw)) + { + source = nullptr; + } + return source; + } +} // namespace OpenRCT2::Audio diff --git a/src/openrct2/audio/AudioSource.h b/src/openrct2/audio/AudioSource.h index 73894f5028..28673d377f 100644 --- a/src/openrct2/audio/AudioSource.h +++ b/src/openrct2/audio/AudioSource.h @@ -23,6 +23,7 @@ namespace OpenRCT2::Audio virtual void Release() abstract; virtual bool IsReleased() const abstract; + virtual int32_t GetBytesPerSecond() const abstract; virtual uint64_t GetLength() const abstract; virtual size_t Read(void* dst, uint64_t offset, size_t len) abstract; }; diff --git a/src/openrct2/object/MusicObject.cpp b/src/openrct2/object/MusicObject.cpp index 8f422a6439..028dfa13a4 100644 --- a/src/openrct2/object/MusicObject.cpp +++ b/src/openrct2/object/MusicObject.cpp @@ -12,6 +12,8 @@ #include "../Context.h" #include "../OpenRCT2.h" #include "../PlatformEnvironment.h" +#include "../audio/AudioContext.h" +#include "../audio/AudioSource.h" #include "../core/IStream.hpp" #include "../core/Json.hpp" #include "../core/Path.hpp" @@ -30,10 +32,30 @@ void MusicObject::Load() GetStringTable().Sort(); NameStringId = language_allocate_object_string(GetName()); + auto audioContext = GetContext()->GetAudioContext(); for (auto& track : _tracks) { - track.BytesPerTick = DEFAULT_BYTES_PER_TICK; - track.Size = track.Asset.GetSize(); + auto stream = track.Asset.GetStream(); + if (stream != nullptr) + { + auto source = audioContext->CreateStreamFromWAV(std::move(stream)); + if (source != nullptr) + { + track.BytesPerTick = source->GetBytesPerSecond() / 40; + track.Size = source->GetLength(); + source->Release(); + } + else + { + track.BytesPerTick = DEFAULT_BYTES_PER_TICK; + track.Size = track.Asset.GetSize(); + } + } + else + { + track.BytesPerTick = DEFAULT_BYTES_PER_TICK; + track.Size = track.Asset.GetSize(); + } } }