feat: add audio system
This commit is contained in:
@@ -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(); });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user