feat: add audio system

This commit is contained in:
2026-10-23 22:07:52 +02:00
parent a3d3da4a67
commit 0b7cc38543
28 changed files with 7412 additions and 214 deletions
+20
View File
@@ -36,7 +36,12 @@ set(SRC_FILES
"src/Event.cpp"
"src/Window.cpp"
"src/Audio/AudioDecoder.cpp"
"src/Audio/AudioSystem.cpp"
"src/Components/Animator.cpp"
"src/Components/AudioListener.cpp"
"src/Components/AudioSource.cpp"
"src/Components/CameraComponent.cpp"
"src/Components/LightComponent.cpp"
"src/Components/MeshRendererComponent.cpp"
@@ -172,6 +177,8 @@ target_link_libraries(destrum
PRIVATE
freetype::freetype
tinyexr
OpenAL::OpenAL
stb::vorbis
)
target_compile_definitions(destrum
@@ -272,6 +279,19 @@ function(destrum_cook_engine_assets GAME_TARGET GAME_OUTPUT_DIR)
install(DIRECTORY "${_engine_runtime}/" DESTINATION "bin/assets/engine")
endfunction()
# OpenAL is a DLL, every exe that links the engine needs a copy next to it
function(destrum_copy_runtime_dlls TARGET)
if (WIN32)
add_custom_command(TARGET ${TARGET} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_RUNTIME_DLLS:${TARGET}> $<TARGET_FILE_DIR:${TARGET}>
COMMAND_EXPAND_LISTS
)
endif()
endfunction()
install(FILES $<TARGET_FILE:OpenAL::OpenAL> DESTINATION bin)
install(TARGETS destrum destrum_assets
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
@@ -29,6 +29,7 @@ namespace AssetTypes {
[[nodiscard]] inline bool IsModel(std::string_view path) { return HasExtension(path, {".fbx", ".glb", ".gltf", ".obj"}); }
[[nodiscard]] inline bool IsImage(std::string_view path) { return HasExtension(path, {".png", ".jpg", ".jpeg", ".tga", ".bmp"}); }
[[nodiscard]] inline bool IsAudio(std::string_view path) { return HasExtension(path, {".wav", ".ogg"}); }
[[nodiscard]] inline bool IsMaterial(std::string_view path) { return HasExtension(path, {MaterialExtension}); }
[[nodiscard]] inline bool IsScene(std::string_view path) { return HasExtension(path, {SceneExtension}); }
[[nodiscard]] inline bool IsPrefab(std::string_view path) { return HasExtension(path, {PrefabExtension}); }
@@ -0,0 +1,31 @@
#ifndef DESTRUM_AUDIODECODER_H
#define DESTRUM_AUDIODECODER_H
#include <cstdint>
#include <optional>
#include <span>
#include <string_view>
#include <vector>
struct AudioData {
int channels{0};
int sampleRate{0};
std::vector<std::int16_t> samples;
[[nodiscard]] float Duration() const
{
if (channels <= 0 || sampleRate <= 0) {
return 0.0f;
}
return static_cast<float>(samples.size() / static_cast<std::size_t>(channels)) / static_cast<float>(sampleRate);
}
};
namespace AudioDecoder {
[[nodiscard]] std::optional<AudioData> DecodeWav(std::span<const std::uint8_t> bytes);
[[nodiscard]] std::optional<AudioData> DecodeOgg(std::span<const std::uint8_t> bytes);
[[nodiscard]] std::optional<AudioData> Decode(std::span<const std::uint8_t> bytes, std::string_view path);
}
#endif // DESTRUM_AUDIODECODER_H
+111
View File
@@ -0,0 +1,111 @@
#ifndef DESTRUM_AUDIOSYSTEM_H
#define DESTRUM_AUDIOSYSTEM_H
#include <cstdint>
#include <memory>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <glm/glm.hpp>
#include <destrum/Singleton.h>
struct AudioData;
struct ALCdevice;
struct ALCcontext;
class Camera;
class AudioClip {
public:
AudioClip(std::string path, std::uint32_t buffer, int channels, int sampleRate, float duration);
~AudioClip();
AudioClip(const AudioClip&) = delete;
AudioClip& operator=(const AudioClip&) = delete;
[[nodiscard]] const std::string& GetPath() const { return m_Path; }
[[nodiscard]] std::uint32_t GetBuffer() const { return m_Buffer; }
[[nodiscard]] int GetChannels() const { return m_Channels; }
[[nodiscard]] int GetSampleRate() const { return m_SampleRate; }
[[nodiscard]] float GetDuration() const { return m_Duration; }
private:
std::string m_Path;
std::uint32_t m_Buffer{0};
int m_Channels{0};
int m_SampleRate{0};
float m_Duration{0.0f};
};
[[nodiscard]] glm::vec3 SmoothAudioVelocity(const glm::vec3& current, const glm::vec3& previous,
const glm::vec3& smoothed, float dt);
struct OneShotParams {
float volume{1.0f};
float pitch{1.0f};
bool spatial{true};
float minDistance{1.0f};
float maxDistance{50.0f};
};
class AudioSystem final : public Singleton<AudioSystem> {
public:
friend class Singleton<AudioSystem>;
~AudioSystem() override;
bool Init();
void Shutdown();
[[nodiscard]] bool IsAvailable() const { return m_Context != nullptr; }
std::shared_ptr<const AudioClip> LoadClip(std::string_view vpath);
std::shared_ptr<const AudioClip> CreateClip(std::string path, const AudioData& data);
void ReleaseUnusedClips();
std::uint32_t CreateSource();
void DestroySource(std::uint32_t source);
void PlayOneShot(std::string_view vpath, const glm::vec3& position, const OneShotParams& params = {});
void SetListener(const glm::vec3& position, const glm::vec3& forward, const glm::vec3& up);
void Update(float dt, bool paused, const Camera* fallbackListener);
void SetMasterVolume(float volume);
[[nodiscard]] float GetMasterVolume() const { return m_MasterVolume; }
[[nodiscard]] bool IsPaused() const { return m_Paused; }
private:
AudioSystem() = default;
void SetPaused(bool paused);
struct OneShot {
std::uint32_t source{0};
std::shared_ptr<const AudioClip> clip;
};
ALCdevice* m_Device{nullptr};
ALCcontext* m_Context{nullptr};
std::unordered_map<std::string, std::shared_ptr<const AudioClip>> m_Clips;
std::unordered_set<std::uint32_t> m_Sources;
std::vector<std::uint32_t> m_PausedSources;
std::vector<OneShot> m_OneShots;
glm::vec3 m_ListenerPosition{0.0f};
glm::vec3 m_ListenerForward{0.0f, 0.0f, -1.0f};
glm::vec3 m_ListenerUp{0.0f, 1.0f, 0.0f};
glm::vec3 m_LastListenerPosition{0.0f};
glm::vec3 m_ListenerVelocity{0.0f};
bool m_HasLastListenerPosition{false};
bool m_ListenerSetThisFrame{false};
float m_MasterVolume{1.0f};
bool m_Paused{false};
};
#endif // DESTRUM_AUDIOSYSTEM_H
@@ -0,0 +1,22 @@
#ifndef DESTRUM_AUDIOLISTENER_H
#define DESTRUM_AUDIOLISTENER_H
#include <destrum/ObjectModel/Component.h>
class AudioListener final : public Component {
public:
explicit AudioListener(GameObject& parent);
AudioListener(const AudioListener& other) = delete;
AudioListener(AudioListener&& other) noexcept = delete;
AudioListener& operator=(const AudioListener& other) = delete;
AudioListener& operator=(AudioListener&& other) noexcept = delete;
void LateUpdate(float dt) override;
std::string GetTypeName() const override { return "AudioListener"; }
void ImGuiInspector() override;
};
#endif // DESTRUM_AUDIOLISTENER_H
@@ -0,0 +1,86 @@
#ifndef DESTRUM_AUDIOSOURCE_H
#define DESTRUM_AUDIOSOURCE_H
#include <cstdint>
#include <memory>
#include <string>
#include <glm/glm.hpp>
#include <destrum/ObjectModel/Component.h>
class AudioClip;
class AudioSource final : public Component {
public:
explicit AudioSource(GameObject& parent, std::string clipPath = {});
~AudioSource() override;
AudioSource(const AudioSource& other) = delete;
AudioSource(AudioSource&& other) noexcept = delete;
AudioSource& operator=(const AudioSource& other) = delete;
AudioSource& operator=(AudioSource&& other) noexcept = delete;
void Update(float dt) override;
void LateUpdate(float dt) override;
void SetEnabled(bool enabled) override;
std::string GetTypeName() const override { return "AudioSource"; }
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
void Play();
void Pause();
void Stop();
[[nodiscard]] bool IsPlaying() const;
void PlayOneShot(const std::string& clipPath, float volumeScale = 1.0f) const;
void SetClip(const std::string& clipPath);
[[nodiscard]] const std::string& GetClip() const { return m_ClipPath; }
void SetVolume(float volume);
[[nodiscard]] float GetVolume() const { return m_Volume; }
void SetPitch(float pitch);
[[nodiscard]] float GetPitch() const { return m_Pitch; }
void SetLoop(bool loop);
[[nodiscard]] bool GetLoop() const { return m_Loop; }
void SetPlayOnStart(bool playOnStart) { m_PlayOnStart = playOnStart; }
[[nodiscard]] bool GetPlayOnStart() const { return m_PlayOnStart; }
void SetSpatial(bool spatial);
[[nodiscard]] bool GetSpatial() const { return m_Spatial; }
void SetDistances(float minDistance, float maxDistance);
[[nodiscard]] float GetMinDistance() const { return m_MinDistance; }
[[nodiscard]] float GetMaxDistance() const { return m_MaxDistance; }
private:
bool EnsureSource();
void ApplyProperties();
void SyncPosition(float dt);
void ReleaseSource();
std::string m_ClipPath;
float m_Volume{1.0f};
float m_Pitch{1.0f};
bool m_Loop{false};
bool m_PlayOnStart{true};
bool m_Spatial{true};
float m_MinDistance{1.0f};
float m_MaxDistance{50.0f};
std::uint32_t m_Source{0};
std::shared_ptr<const AudioClip> m_Clip;
bool m_AutoPlayed{false};
glm::vec3 m_LastPosition{0.0f};
glm::vec3 m_Velocity{0.0f};
bool m_HasLastPosition{false};
};
#endif // DESTRUM_AUDIOSOURCE_H
+1
View File
@@ -25,6 +25,7 @@ public:
void Init(std::filesystem::path exeDir);
void Reset();
void Mount(std::string scheme, std::filesystem::path root);
void ReloadManifest(std::string_view scheme);
std::vector<uint8_t> ReadBytes(std::string_view vpath);
@@ -2,6 +2,8 @@
#define DESTRUM_ENGINECOMPONENTLIST_H
#include <destrum/Components/Animator.h>
#include <destrum/Components/AudioListener.h>
#include <destrum/Components/AudioSource.h>
#include <destrum/Components/CameraComponent.h>
#include <destrum/Components/LightComponent.h>
#include <destrum/Components/MeshRendererComponent.h>
@@ -25,6 +27,8 @@
X(Spinner) \
X(OrbitAndSpin) \
X(Animator) \
X(AudioSource) \
X(AudioListener) \
X(Rigidbody) \
X(BoxCollider) \
X(SphereCollider) \
+6
View File
@@ -10,6 +10,7 @@
#include <tracy/Tracy.hpp>
#include <common/TracySystem.hpp>
#include <destrum/Audio/AudioSystem.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/Resources/FontCache.h>
#include <destrum/Scene/ResourceCollector.h>
@@ -73,6 +74,7 @@ void App::init(const AppParams& params)
renderer.init(gfxDevice, resources, params.renderSize);
InputManager::GetInstance().Init();
AudioSystem::GetInstance().Init();
Time::GetInstance().Update();
customInitStarted = true;
@@ -134,6 +136,7 @@ void App::resizeRenderTarget(const glm::ivec2& size)
void App::collectUnusedResources()
{
CollectUnusedResources(SceneManager::GetInstance(), resources, gfxDevice);
AudioSystem::GetInstance().ReleaseUnusedClips();
}
void App::run()
@@ -196,6 +199,8 @@ void App::run()
runFixedSteps();
AudioSystem::GetInstance().Update(dt, isPlaying() && (isPaused() || simulation.paused), &getViewCamera());
bool skipFrame = false;
handleSwapchainResize(skipFrame);
if (skipFrame) {
@@ -392,6 +397,7 @@ void App::cleanup()
attempt([this] { customCleanup(); });
}
attempt([] { SceneManager::GetInstance().Destroy(); });
attempt([] { AudioSystem::GetInstance().Shutdown(); });
attempt([] { FontCache::GetInstance().Clear(); });
attempt([this] { renderer.cleanup(gfxDevice); });
attempt([this] { imguiPass.cleanup(); });
+189
View File
@@ -0,0 +1,189 @@
#include <destrum/Audio/AudioDecoder.h>
#include <destrum/Assets/AssetTypes.h>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <spdlog/spdlog.h>
#define STB_VORBIS_HEADER_ONLY
#include <stb_vorbis.c>
namespace {
constexpr std::uint16_t FormatPcm = 1;
constexpr std::uint16_t FormatFloat = 3;
constexpr std::uint16_t FormatExtensible = 0xFFFE;
// OpenAL only has mono and stereo formats in core
constexpr int MaxChannels = 2;
[[nodiscard]] std::uint16_t ReadU16(std::span<const std::uint8_t> bytes, std::size_t offset)
{
return static_cast<std::uint16_t>(bytes[offset] | (bytes[offset + 1] << 8));
}
[[nodiscard]] std::uint32_t ReadU32(std::span<const std::uint8_t> bytes, std::size_t offset)
{
return static_cast<std::uint32_t>(bytes[offset]) |
(static_cast<std::uint32_t>(bytes[offset + 1]) << 8) |
(static_cast<std::uint32_t>(bytes[offset + 2]) << 16) |
(static_cast<std::uint32_t>(bytes[offset + 3]) << 24);
}
[[nodiscard]] bool Tag(std::span<const std::uint8_t> bytes, std::size_t offset, const char (&tag)[5])
{
return std::memcmp(bytes.data() + offset, tag, 4) == 0;
}
[[nodiscard]] std::int16_t FloatToPcm(float value)
{
return static_cast<std::int16_t>(std::lround(std::clamp(value, -1.0f, 1.0f) * 32767.0f));
}
[[nodiscard]] std::int16_t ReadSample(std::span<const std::uint8_t> data, std::size_t offset,
std::uint16_t format, std::uint16_t bits)
{
if (format == FormatFloat) {
float value = 0.0f;
const std::uint32_t raw = ReadU32(data, offset);
std::memcpy(&value, &raw, sizeof(value));
return std::isfinite(value) ? FloatToPcm(value) : std::int16_t{0};
}
switch (bits) {
case 8:
return static_cast<std::int16_t>((static_cast<int>(data[offset]) - 128) << 8);
case 16:
return static_cast<std::int16_t>(ReadU16(data, offset));
case 24:
return static_cast<std::int16_t>(data[offset + 1] | (data[offset + 2] << 8));
default:
return static_cast<std::int16_t>(ReadU32(data, offset) >> 16);
}
}
}
std::optional<AudioData> AudioDecoder::DecodeWav(std::span<const std::uint8_t> bytes)
{
if (bytes.size() < 12 || !Tag(bytes, 0, "RIFF") || !Tag(bytes, 8, "WAVE")) {
return std::nullopt;
}
std::uint16_t format = 0;
std::uint16_t channels = 0;
std::uint32_t sampleRate = 0;
std::uint16_t blockAlign = 0;
std::uint16_t bits = 0;
std::span<const std::uint8_t> data;
bool hasFormat = false;
bool hasData = false;
std::size_t offset = 12;
while (offset + 8 <= bytes.size()) {
const std::size_t chunkSize = ReadU32(bytes, offset + 4);
const std::size_t chunkStart = offset + 8;
// Writers that stream sometimes leave the data size at 0xFFFFFFFF, take what is there
const std::size_t available = std::min(chunkSize, bytes.size() - chunkStart);
if (Tag(bytes, offset, "fmt ")) {
if (available < 16) {
return std::nullopt;
}
format = ReadU16(bytes, chunkStart);
channels = ReadU16(bytes, chunkStart + 2);
sampleRate = ReadU32(bytes, chunkStart + 4);
blockAlign = ReadU16(bytes, chunkStart + 12);
bits = ReadU16(bytes, chunkStart + 14);
if (format == FormatExtensible) {
if (available < 26) {
return std::nullopt;
}
format = ReadU16(bytes, chunkStart + 24);
}
hasFormat = true;
} else if (Tag(bytes, offset, "data")) {
data = bytes.subspan(chunkStart, available);
hasData = true;
}
if (chunkSize > bytes.size() - chunkStart) {
break;
}
// Chunks are padded to an even size
offset = chunkStart + chunkSize + (chunkSize & 1u);
}
if (!hasFormat || !hasData) {
return std::nullopt;
}
const bool pcm = format == FormatPcm && (bits == 8 || bits == 16 || bits == 24 || bits == 32);
const bool floats = format == FormatFloat && bits == 32;
if ((!pcm && !floats) || channels == 0 || channels > MaxChannels || sampleRate == 0 ||
sampleRate > static_cast<std::uint32_t>(std::numeric_limits<int>::max())) {
spdlog::warn("Unsupported WAV: format {}, {} bits, {} channels, {} Hz", format, bits, channels, sampleRate);
return std::nullopt;
}
const std::size_t bytesPerSample = bits / 8u;
if (blockAlign < bytesPerSample * channels) {
return std::nullopt;
}
AudioData result;
result.channels = channels;
result.sampleRate = static_cast<int>(sampleRate);
const std::size_t frames = data.size() / blockAlign;
result.samples.reserve(frames * channels);
for (std::size_t frame = 0; frame < frames; ++frame) {
for (std::size_t channel = 0; channel < channels; ++channel) {
result.samples.push_back(ReadSample(data, frame * blockAlign + channel * bytesPerSample, format, bits));
}
}
return result;
}
std::optional<AudioData> AudioDecoder::DecodeOgg(std::span<const std::uint8_t> bytes)
{
if (bytes.empty() || bytes.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
return std::nullopt;
}
int channels = 0;
int sampleRate = 0;
short* decoded = nullptr;
const int frames = stb_vorbis_decode_memory(bytes.data(), static_cast<int>(bytes.size()), &channels, &sampleRate, &decoded);
if (frames < 0 || decoded == nullptr) {
return std::nullopt;
}
AudioData result;
result.channels = channels;
result.sampleRate = sampleRate;
result.samples.assign(decoded, decoded + static_cast<std::size_t>(frames) * static_cast<std::size_t>(channels));
std::free(decoded);
if (channels <= 0 || channels > MaxChannels || sampleRate <= 0) {
spdlog::warn("Unsupported OGG: {} channels, {} Hz", channels, sampleRate);
return std::nullopt;
}
return result;
}
std::optional<AudioData> AudioDecoder::Decode(std::span<const std::uint8_t> bytes, std::string_view path)
{
const std::string extension = AssetTypes::Extension(path);
if (extension == ".wav") {
return DecodeWav(bytes);
}
if (extension == ".ogg") {
return DecodeOgg(bytes);
}
spdlog::warn("Unsupported audio format: {}", path);
return std::nullopt;
}
+331
View File
@@ -0,0 +1,331 @@
#include <destrum/Audio/AudioSystem.h>
#include <destrum/Audio/AudioDecoder.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/Camera.h>
#include <AL/al.h>
#include <AL/alc.h>
#include <AL/alext.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <exception>
#include <utility>
#include <glm/gtc/type_ptr.hpp>
#include <spdlog/spdlog.h>
#include <tracy/Tracy.hpp>
namespace {
constexpr std::size_t MaxOneShots = 32;
// Above this the object teleported (respawn, camera cut), a doppler shift would only sound wrong
constexpr float MaxSpeed = 100.0f;
constexpr float VelocitySmoothingTime = 0.1f;
[[nodiscard]] ALint SourceState(ALuint source)
{
ALint state = AL_STOPPED;
alGetSourcei(source, AL_SOURCE_STATE, &state);
return state;
}
}
// Physics moves objects in fixed steps, so the raw per frame velocity jumps between 0 and a spike and doppler warbles
glm::vec3 SmoothAudioVelocity(const glm::vec3& current, const glm::vec3& previous, const glm::vec3& smoothed, float dt)
{
if (dt <= 0.0f) {
return smoothed;
}
const glm::vec3 velocity = (current - previous) / dt;
if (glm::length(velocity) > MaxSpeed) {
return glm::vec3{0.0f};
}
return glm::mix(smoothed, velocity, 1.0f - std::exp(-dt / VelocitySmoothingTime));
}
AudioClip::AudioClip(std::string path, std::uint32_t buffer, int channels, int sampleRate, float duration)
: m_Path(std::move(path))
, m_Buffer(buffer)
, m_Channels(channels)
, m_SampleRate(sampleRate)
, m_Duration(duration)
{
}
AudioClip::~AudioClip()
{
if (m_Buffer != 0 && AudioSystem::GetInstance().IsAvailable()) {
const ALuint buffer = m_Buffer;
alDeleteBuffers(1, &buffer);
}
}
AudioSystem::~AudioSystem()
{
try {
Shutdown();
} catch (...) {
}
}
bool AudioSystem::Init()
{
if (IsAvailable()) {
return true;
}
m_Device = alcOpenDevice(nullptr);
if (m_Device == nullptr) {
spdlog::warn("No audio device, sound is disabled");
return false;
}
m_Context = alcCreateContext(m_Device, nullptr);
if (m_Context == nullptr || alcMakeContextCurrent(m_Context) == ALC_FALSE) {
spdlog::warn("Could not create an OpenAL context, sound is disabled");
if (m_Context != nullptr) {
alcDestroyContext(m_Context);
m_Context = nullptr;
}
alcCloseDevice(m_Device);
m_Device = nullptr;
return false;
}
alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED);
if (alIsExtensionPresent("AL_SOFT_source_spatialize") == AL_FALSE) {
spdlog::warn("AL_SOFT_source_spatialize is missing, stereo clips will not be positioned");
}
alListenerf(AL_GAIN, m_MasterVolume);
const ALCchar* name = alcGetString(m_Device, ALC_DEVICE_SPECIFIER);
spdlog::info("Audio device: {}", name != nullptr ? name : "unknown");
return true;
}
void AudioSystem::Shutdown()
{
if (!IsAvailable()) {
return;
}
for (const OneShot& oneShot : m_OneShots) {
DestroySource(oneShot.source);
}
m_OneShots.clear();
while (!m_Sources.empty()) {
DestroySource(*m_Sources.begin());
}
m_PausedSources.clear();
m_Clips.clear();
alcMakeContextCurrent(nullptr);
alcDestroyContext(m_Context);
alcCloseDevice(m_Device);
m_Context = nullptr;
m_Device = nullptr;
m_Paused = false;
m_HasLastListenerPosition = false;
}
std::shared_ptr<const AudioClip> AudioSystem::LoadClip(std::string_view vpath)
{
if (!IsAvailable() || vpath.empty()) {
return nullptr;
}
const std::string key{vpath};
if (const auto it = m_Clips.find(key); it != m_Clips.end()) {
return it->second;
}
ZoneScopedN("AudioSystem::LoadClip");
std::vector<std::uint8_t> bytes;
try {
bytes = AssetFS::GetInstance().ReadBytes(vpath);
} catch (const std::exception& e) {
spdlog::error("Could not read audio clip {}: {}", key, e.what());
return nullptr;
}
const auto data = AudioDecoder::Decode(bytes, vpath);
if (!data) {
spdlog::error("Could not decode audio clip {}", key);
return nullptr;
}
return CreateClip(key, *data);
}
std::shared_ptr<const AudioClip> AudioSystem::CreateClip(std::string path, const AudioData& data)
{
if (!IsAvailable() || data.samples.empty() || (data.channels != 1 && data.channels != 2)) {
return nullptr;
}
alGetError();
ALuint buffer = 0;
alGenBuffers(1, &buffer);
alBufferData(buffer, data.channels == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16, data.samples.data(),
static_cast<ALsizei>(data.samples.size() * sizeof(std::int16_t)), data.sampleRate);
if (const ALenum error = alGetError(); error != AL_NO_ERROR) {
spdlog::error("Could not upload audio clip {}: OpenAL error {:#x}", path, error);
alDeleteBuffers(1, &buffer);
return nullptr;
}
auto clip = std::make_shared<const AudioClip>(path, buffer, data.channels, data.sampleRate, data.Duration());
m_Clips[path] = clip;
return clip;
}
void AudioSystem::ReleaseUnusedClips()
{
std::erase_if(m_Clips, [](const auto& entry) { return entry.second.use_count() == 1; });
}
std::uint32_t AudioSystem::CreateSource()
{
if (!IsAvailable()) {
return 0;
}
alGetError();
ALuint source = 0;
alGenSources(1, &source);
if (alGetError() != AL_NO_ERROR) {
spdlog::warn("Out of OpenAL sources");
return 0;
}
m_Sources.insert(source);
return source;
}
void AudioSystem::DestroySource(std::uint32_t source)
{
if (source == 0 || !IsAvailable() || !m_Sources.erase(source)) {
return;
}
std::erase(m_PausedSources, source);
const ALuint alSource = source;
alSourceStop(alSource);
alSourcei(alSource, AL_BUFFER, 0);
alDeleteSources(1, &alSource);
}
void AudioSystem::PlayOneShot(std::string_view vpath, const glm::vec3& position, const OneShotParams& params)
{
auto clip = LoadClip(vpath);
if (!clip) {
return;
}
if (m_OneShots.size() >= MaxOneShots) {
DestroySource(m_OneShots.front().source);
m_OneShots.erase(m_OneShots.begin());
}
const std::uint32_t source = CreateSource();
if (source == 0) {
return;
}
alSourcei(source, AL_BUFFER, static_cast<ALint>(clip->GetBuffer()));
alSourcef(source, AL_GAIN, params.volume);
alSourcef(source, AL_PITCH, params.pitch);
alSourcei(source, AL_SOURCE_RELATIVE, params.spatial ? AL_FALSE : AL_TRUE);
alSourcei(source, AL_SOURCE_SPATIALIZE_SOFT, params.spatial ? AL_TRUE : AL_FALSE);
alSourcef(source, AL_ROLLOFF_FACTOR, params.spatial ? 1.0f : 0.0f);
alSourcef(source, AL_REFERENCE_DISTANCE, params.minDistance);
alSourcef(source, AL_MAX_DISTANCE, params.maxDistance);
const glm::vec3 sourcePosition = params.spatial ? position : glm::vec3{0.0f};
alSourcefv(source, AL_POSITION, glm::value_ptr(sourcePosition));
alSourcePlay(source);
m_OneShots.push_back({source, std::move(clip)});
}
void AudioSystem::SetListener(const glm::vec3& position, const glm::vec3& forward, const glm::vec3& up)
{
m_ListenerPosition = position;
m_ListenerForward = forward;
m_ListenerUp = up;
m_ListenerSetThisFrame = true;
}
void AudioSystem::Update(float dt, bool paused, const Camera* fallbackListener)
{
if (!IsAvailable()) {
return;
}
ZoneScopedN("AudioSystem::Update");
if (!m_ListenerSetThisFrame && fallbackListener != nullptr) {
SetListener(fallbackListener->GetPosition(), fallbackListener->GetForward(), fallbackListener->GetUp());
}
m_ListenerSetThisFrame = false;
if (!m_HasLastListenerPosition || paused) {
m_ListenerVelocity = glm::vec3{0.0f};
} else {
m_ListenerVelocity = SmoothAudioVelocity(m_ListenerPosition, m_LastListenerPosition, m_ListenerVelocity, dt);
}
m_LastListenerPosition = m_ListenerPosition;
m_HasLastListenerPosition = true;
const std::array orientation{
m_ListenerForward.x, m_ListenerForward.y, m_ListenerForward.z,
m_ListenerUp.x, m_ListenerUp.y, m_ListenerUp.z,
};
alListenerfv(AL_POSITION, glm::value_ptr(m_ListenerPosition));
alListenerfv(AL_VELOCITY, glm::value_ptr(m_ListenerVelocity));
alListenerfv(AL_ORIENTATION, orientation.data());
SetPaused(paused);
std::erase_if(m_OneShots, [this](const OneShot& oneShot) {
const ALint state = SourceState(oneShot.source);
if (state == AL_PLAYING || state == AL_PAUSED) {
return false;
}
DestroySource(oneShot.source);
return true;
});
}
void AudioSystem::SetMasterVolume(float volume)
{
m_MasterVolume = std::max(volume, 0.0f);
if (IsAvailable()) {
alListenerf(AL_GAIN, m_MasterVolume);
}
}
void AudioSystem::SetPaused(bool paused)
{
if (paused == m_Paused) {
return;
}
m_Paused = paused;
if (paused) {
for (const std::uint32_t source : m_Sources) {
if (SourceState(source) == AL_PLAYING) {
alSourcePause(source);
m_PausedSources.push_back(source);
}
}
return;
}
// Sources paused on purpose before the simulation paused stay paused
for (const std::uint32_t source : m_PausedSources) {
if (SourceState(source) == AL_PAUSED) {
alSourcePlay(source);
}
}
m_PausedSources.clear();
}
+33
View File
@@ -0,0 +1,33 @@
#include <destrum/Components/AudioListener.h>
#include <destrum/Audio/AudioSystem.h>
#include <destrum/ObjectModel/Transform.h>
#include <imgui.h>
AudioListener::AudioListener(GameObject& parent)
: Component(parent, "AudioListener")
{
}
void AudioListener::LateUpdate(float)
{
auto& transform = GetTransform();
const glm::quat rotation = transform.GetWorldRotation();
AudioSystem::GetInstance().SetListener(
transform.GetWorldPosition(),
rotation * glm::vec3{0.0f, 0.0f, -1.0f},
rotation * glm::vec3{0.0f, 1.0f, 0.0f});
}
void AudioListener::ImGuiInspector()
{
auto& audio = AudioSystem::GetInstance();
float volume = audio.GetMasterVolume();
if (ImGui::SliderFloat("Master Volume", &volume, 0.0f, 1.0f)) {
audio.SetMasterVolume(volume);
}
ImGui::TextDisabled("Hears the scene while playing, the view camera does otherwise");
ImGui::TextWrapped("Uses this object's position and rotation. If the transform does not turn with the view "
"(a first person body), remove this and let the camera listen.");
}
+315
View File
@@ -0,0 +1,315 @@
#include <destrum/Components/AudioSource.h>
#include <destrum/Assets/AssetDatabase.h>
#include <destrum/Assets/AssetTypes.h>
#include <destrum/Audio/AudioSystem.h>
#include <destrum/ObjectModel/Transform.h>
#include <destrum/Util/ImGuiUtils.h>
#include <AL/al.h>
#include <AL/alext.h>
#include <algorithm>
#include <utility>
#include <glm/gtc/type_ptr.hpp>
#include <imgui.h>
namespace {
[[nodiscard]] ALint SourceState(std::uint32_t source)
{
ALint state = AL_STOPPED;
if (source != 0) {
alGetSourcei(source, AL_SOURCE_STATE, &state);
}
return state;
}
}
AudioSource::AudioSource(GameObject& parent, std::string clipPath)
: Component(parent, "AudioSource")
, m_ClipPath(std::move(clipPath))
{
}
AudioSource::~AudioSource()
{
ReleaseSource();
}
void AudioSource::Update(float)
{
if (m_AutoPlayed) {
return;
}
m_AutoPlayed = true;
if (m_PlayOnStart) {
Play();
}
}
void AudioSource::LateUpdate(float dt)
{
SyncPosition(dt);
}
void AudioSource::SetEnabled(bool enabled)
{
if (!enabled) {
Stop();
}
Component::SetEnabled(enabled);
}
void AudioSource::Play()
{
if (!EnsureSource()) {
return;
}
if (!m_Clip || m_Clip->GetPath() != m_ClipPath) {
alSourceStop(m_Source);
alSourcei(m_Source, AL_BUFFER, 0);
m_Clip = AudioSystem::GetInstance().LoadClip(m_ClipPath);
if (!m_Clip) {
return;
}
alSourcei(m_Source, AL_BUFFER, static_cast<ALint>(m_Clip->GetBuffer()));
}
ApplyProperties();
m_HasLastPosition = false;
SyncPosition(0.0f);
alSourcePlay(m_Source);
}
void AudioSource::Pause()
{
if (SourceState(m_Source) == AL_PLAYING) {
alSourcePause(m_Source);
}
}
void AudioSource::Stop()
{
if (m_Source != 0) {
alSourceStop(m_Source);
}
}
bool AudioSource::IsPlaying() const
{
return SourceState(m_Source) == AL_PLAYING;
}
void AudioSource::PlayOneShot(const std::string& clipPath, float volumeScale) const
{
AudioSystem::GetInstance().PlayOneShot(clipPath, GetTransform().GetWorldPosition(), {
.volume = m_Volume * volumeScale,
.pitch = m_Pitch,
.spatial = m_Spatial,
.minDistance = m_MinDistance,
.maxDistance = m_MaxDistance,
});
}
void AudioSource::SetClip(const std::string& clipPath)
{
if (clipPath == m_ClipPath) {
return;
}
m_ClipPath = clipPath;
if (m_Source != 0) {
alSourceStop(m_Source);
alSourcei(m_Source, AL_BUFFER, 0);
}
m_Clip.reset();
}
void AudioSource::SetVolume(float volume)
{
m_Volume = std::max(volume, 0.0f);
ApplyProperties();
}
void AudioSource::SetPitch(float pitch)
{
m_Pitch = std::max(pitch, 0.01f);
ApplyProperties();
}
void AudioSource::SetLoop(bool loop)
{
m_Loop = loop;
ApplyProperties();
}
void AudioSource::SetSpatial(bool spatial)
{
m_Spatial = spatial;
ApplyProperties();
SyncPosition(0.0f);
}
void AudioSource::SetDistances(float minDistance, float maxDistance)
{
m_MinDistance = std::max(minDistance, 0.01f);
m_MaxDistance = std::max(maxDistance, m_MinDistance);
ApplyProperties();
}
nlohmann::json AudioSource::Serialize() const
{
return {
{"clip", m_ClipPath.empty() ? nlohmann::json(nullptr) : AssetDatabase::GetInstance().WriteReference(m_ClipPath)},
{"volume", m_Volume},
{"pitch", m_Pitch},
{"loop", m_Loop},
{"playOnStart", m_PlayOnStart},
{"spatial", m_Spatial},
{"minDistance", m_MinDistance},
{"maxDistance", m_MaxDistance},
};
}
void AudioSource::Deserialize(const nlohmann::json& data)
{
if (const auto clip = data.find("clip"); clip != data.end()) {
if (clip->is_string()) {
SetClip(clip->get<std::string>());
} else if (const auto reference = AssetDatabase::GetInstance().ReadReference(*clip)) {
SetClip(reference->path);
} else {
SetClip({});
}
}
m_Loop = data.value("loop", m_Loop);
m_PlayOnStart = data.value("playOnStart", m_PlayOnStart);
m_Spatial = data.value("spatial", m_Spatial);
SetVolume(data.value("volume", m_Volume));
SetPitch(data.value("pitch", m_Pitch));
SetDistances(data.value("minDistance", m_MinDistance), data.value("maxDistance", m_MaxDistance));
}
void AudioSource::ImGuiInspector()
{
if (!AudioSystem::GetInstance().IsAvailable()) {
ImGui::TextDisabled("No audio device, sound is disabled");
}
std::string clipPath = m_ClipPath;
if (ImGuiUtils::InputString("Clip", clipPath, 0, "game://sounds/clip.ogg")) {
SetClip(clipPath);
}
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(ImGuiUtils::AssetPayload)) {
const std::string dropped = static_cast<const char*>(payload->Data);
if (AssetTypes::IsAudio(dropped)) {
SetClip(dropped);
}
}
ImGui::EndDragDropTarget();
}
ImGui::SetItemTooltip("Drop a .wav or .ogg from the Assets window");
if (m_Clip && m_Clip->GetPath() == m_ClipPath) {
ImGui::TextDisabled("%s, %d Hz, %.2f s", m_Clip->GetChannels() == 1 ? "Mono" : "Stereo",
m_Clip->GetSampleRate(), m_Clip->GetDuration());
}
float volume = m_Volume;
if (ImGui::SliderFloat("Volume", &volume, 0.0f, 2.0f)) {
SetVolume(volume);
}
float pitch = m_Pitch;
if (ImGui::SliderFloat("Pitch", &pitch, 0.1f, 3.0f)) {
SetPitch(pitch);
}
bool loop = m_Loop;
if (ImGui::Checkbox("Loop", &loop)) {
SetLoop(loop);
}
ImGui::Checkbox("Play On Start", &m_PlayOnStart);
bool spatial = m_Spatial;
if (ImGui::Checkbox("Spatial", &spatial)) {
SetSpatial(spatial);
}
if (m_Spatial) {
float minDistance = m_MinDistance;
float maxDistance = m_MaxDistance;
bool changed = ImGui::DragFloat("Min Distance", &minDistance, 0.05f, 0.01f, 1000.0f, "%.2f m");
ImGui::SetItemTooltip("Full volume up to this distance");
changed |= ImGui::DragFloat("Max Distance", &maxDistance, 0.1f, 0.01f, 10000.0f, "%.2f m");
ImGui::SetItemTooltip("The sound stops getting quieter past this distance");
if (changed) {
SetDistances(minDistance, maxDistance);
}
}
ImGui::BeginDisabled(m_ClipPath.empty());
if (ImGui::Button(IsPlaying() ? "Restart" : "Play")) {
Stop();
Play();
}
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Stop")) {
Stop();
}
}
bool AudioSource::EnsureSource()
{
if (m_Source == 0) {
m_Source = AudioSystem::GetInstance().CreateSource();
}
return m_Source != 0;
}
void AudioSource::ApplyProperties()
{
if (m_Source == 0) {
return;
}
alSourcef(m_Source, AL_GAIN, m_Volume);
alSourcef(m_Source, AL_PITCH, m_Pitch);
alSourcei(m_Source, AL_LOOPING, m_Loop ? AL_TRUE : AL_FALSE);
alSourcei(m_Source, AL_SOURCE_RELATIVE, m_Spatial ? AL_FALSE : AL_TRUE);
alSourcei(m_Source, AL_SOURCE_SPATIALIZE_SOFT, m_Spatial ? AL_TRUE : AL_FALSE);
alSourcef(m_Source, AL_ROLLOFF_FACTOR, m_Spatial ? 1.0f : 0.0f);
alSourcef(m_Source, AL_REFERENCE_DISTANCE, m_MinDistance);
alSourcef(m_Source, AL_MAX_DISTANCE, m_MaxDistance);
}
void AudioSource::SyncPosition(float dt)
{
if (m_Source == 0) {
return;
}
if (!m_Spatial) {
const glm::vec3 zero{0.0f};
alSourcefv(m_Source, AL_POSITION, glm::value_ptr(zero));
alSourcefv(m_Source, AL_VELOCITY, glm::value_ptr(zero));
m_HasLastPosition = false;
return;
}
const glm::vec3 position = GetTransform().GetWorldPosition();
m_Velocity = m_HasLastPosition ? SmoothAudioVelocity(position, m_LastPosition, m_Velocity, dt) : glm::vec3{0.0f};
m_LastPosition = position;
m_HasLastPosition = true;
alSourcefv(m_Source, AL_POSITION, glm::value_ptr(position));
alSourcefv(m_Source, AL_VELOCITY, glm::value_ptr(m_Velocity));
}
void AudioSource::ReleaseSource()
{
AudioSystem::GetInstance().DestroySource(m_Source);
m_Source = 0;
m_Clip.reset();
}
+16
View File
@@ -142,6 +142,22 @@ void AssetFS::Mount(std::string scheme, std::filesystem::path root) {
mounts.push_back(std::move(mount));
}
void AssetFS::ReloadManifest(std::string_view scheme) {
for (FSMount& mount : mounts) {
if (mount.scheme != scheme) {
continue;
}
const std::filesystem::path manifestPath = mount.root / "manifest.json";
std::error_code error;
mount.manifest.reset();
if (std::filesystem::is_regular_file(manifestPath, error)) {
mount.manifest = FS::LoadAssetManifest(manifestPath);
}
return;
}
throw std::runtime_error("asset mount not found: " + std::string{scheme});
}
std::vector<uint8_t> AssetFS::ReadBytes(std::string_view vpath) {
return ReadFile(GetFullPath(vpath));
}
+24
View File
@@ -157,3 +157,27 @@ option(TRACY_ON_DEMAND "" ON)
add_subdirectory(tracy)
# OpenAL Soft is LGPL, so it stays a DLL next to the exe instead of being linked in
set(LIBTYPE SHARED CACHE STRING "" FORCE)
set(ALSOFT_UTILS OFF CACHE BOOL "" FORCE)
set(ALSOFT_EXAMPLES OFF CACHE BOOL "" FORCE)
set(ALSOFT_TESTS OFF CACHE BOOL "" FORCE)
set(ALSOFT_INSTALL OFF CACHE BOOL "" FORCE)
set(ALSOFT_INSTALL_CONFIG OFF CACHE BOOL "" FORCE)
set(ALSOFT_INSTALL_HRTF_DATA OFF CACHE BOOL "" FORCE)
set(ALSOFT_INSTALL_AMBDEC_PRESETS OFF CACHE BOOL "" FORCE)
set(ALSOFT_INSTALL_EXAMPLES OFF CACHE BOOL "" FORCE)
set(ALSOFT_INSTALL_UTILS OFF CACHE BOOL "" FORCE)
set(ALSOFT_EAX OFF CACHE BOOL "" FORCE)
add_subdirectory(openal-soft)
add_library(stb_vorbis STATIC "${CMAKE_CURRENT_LIST_DIR}/stb/src/stb_vorbis.c")
target_include_directories(stb_vorbis PUBLIC "${CMAKE_CURRENT_LIST_DIR}/stb/src")
target_compile_definitions(stb_vorbis PRIVATE _CRT_SECURE_NO_WARNINGS)
if (MSVC)
target_compile_options(stb_vorbis PRIVATE /W0)
else ()
target_compile_options(stb_vorbis PRIVATE -w)
endif ()
add_library(stb::vorbis ALIAS stb_vorbis)
File diff suppressed because it is too large Load Diff