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
+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));
}