2026-02-09 00:40:50 -08:00
|
|
|
#define MINIAUDIO_IMPLEMENTATION
|
|
|
|
|
#include "audio/audio_engine.hpp"
|
|
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
#include "pipeline/asset_manager.hpp"
|
|
|
|
|
|
2026-02-17 18:52:19 -08:00
|
|
|
|
2026-02-09 00:40:50 -08:00
|
|
|
#include "../../extern/miniaudio.h"
|
|
|
|
|
|
|
|
|
|
#include <cstring>
|
2026-02-19 16:17:06 -08:00
|
|
|
#include <cstdlib>
|
2026-02-11 22:27:02 -08:00
|
|
|
#include <memory>
|
|
|
|
|
#include <unordered_map>
|
2026-02-09 00:40:50 -08:00
|
|
|
|
|
|
|
|
namespace wowee {
|
|
|
|
|
namespace audio {
|
|
|
|
|
|
2026-02-11 22:27:02 -08:00
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
struct DecodedWavCacheEntry {
|
|
|
|
|
ma_format format = ma_format_unknown;
|
|
|
|
|
ma_uint32 channels = 0;
|
|
|
|
|
ma_uint32 sampleRate = 0;
|
|
|
|
|
ma_uint64 frames = 0;
|
|
|
|
|
std::shared_ptr<std::vector<uint8_t>> pcmData;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static std::unordered_map<uint64_t, DecodedWavCacheEntry> gDecodedWavCache;
|
|
|
|
|
|
|
|
|
|
static uint64_t makeWavCacheKey(const std::vector<uint8_t>& wavData) {
|
2026-02-17 15:13:54 -08:00
|
|
|
// FNV-1a over the first 256 bytes + last 256 bytes + total size.
|
|
|
|
|
// Full-content hash would be correct but slow for large files; sampling the
|
|
|
|
|
// edges catches virtually all distinct files while keeping cost O(1).
|
|
|
|
|
constexpr uint64_t FNV_OFFSET = 14695981039346656037ull;
|
|
|
|
|
constexpr uint64_t FNV_PRIME = 1099511628211ull;
|
|
|
|
|
uint64_t h = FNV_OFFSET;
|
|
|
|
|
auto mix = [&](uint8_t b) { h ^= b; h *= FNV_PRIME; };
|
|
|
|
|
|
|
|
|
|
const size_t sz = wavData.size();
|
|
|
|
|
const size_t head = std::min(sz, size_t(256));
|
|
|
|
|
for (size_t i = 0; i < head; ++i) mix(wavData[i]);
|
|
|
|
|
if (sz > 256) {
|
|
|
|
|
const size_t tail_start = sz > 512 ? sz - 256 : 256;
|
|
|
|
|
for (size_t i = tail_start; i < sz; ++i) mix(wavData[i]);
|
|
|
|
|
}
|
|
|
|
|
// Mix in the total size so files with identical head/tail but different
|
|
|
|
|
// lengths still produce different keys.
|
|
|
|
|
for (int s = 0; s < 8; ++s) mix(static_cast<uint8_t>(sz >> (s * 8)));
|
|
|
|
|
return h;
|
2026-02-11 22:27:02 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool decodeWavCached(const std::vector<uint8_t>& wavData, DecodedWavCacheEntry& out) {
|
|
|
|
|
if (wavData.empty()) return false;
|
|
|
|
|
|
|
|
|
|
const uint64_t key = makeWavCacheKey(wavData);
|
|
|
|
|
if (auto it = gDecodedWavCache.find(key); it != gDecodedWavCache.end()) {
|
|
|
|
|
out = it->second;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ma_decoder decoder;
|
|
|
|
|
ma_decoder_config decoderConfig = ma_decoder_config_init_default();
|
|
|
|
|
ma_result result = ma_decoder_init_memory(
|
|
|
|
|
wavData.data(),
|
|
|
|
|
wavData.size(),
|
|
|
|
|
&decoderConfig,
|
|
|
|
|
&decoder
|
|
|
|
|
);
|
|
|
|
|
if (result != MA_SUCCESS) {
|
|
|
|
|
LOG_ERROR("AudioEngine: Failed to decode WAV data (", wavData.size(), " bytes): error ", result);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ma_uint64 totalFrames = 0;
|
|
|
|
|
result = ma_decoder_get_length_in_pcm_frames(&decoder, &totalFrames);
|
|
|
|
|
if (result != MA_SUCCESS) totalFrames = 0;
|
|
|
|
|
|
|
|
|
|
ma_format format = decoder.outputFormat;
|
|
|
|
|
ma_uint32 channels = decoder.outputChannels;
|
|
|
|
|
ma_uint32 sampleRate = decoder.outputSampleRate;
|
|
|
|
|
ma_uint64 maxFrames = sampleRate * 60;
|
|
|
|
|
if (totalFrames == 0 || totalFrames > maxFrames) totalFrames = maxFrames;
|
|
|
|
|
|
|
|
|
|
size_t bufferSize = totalFrames * channels * ma_get_bytes_per_sample(format);
|
|
|
|
|
auto pcmData = std::make_shared<std::vector<uint8_t>>(bufferSize);
|
|
|
|
|
ma_uint64 framesRead = 0;
|
|
|
|
|
result = ma_decoder_read_pcm_frames(&decoder, pcmData->data(), totalFrames, &framesRead);
|
|
|
|
|
ma_decoder_uninit(&decoder);
|
|
|
|
|
if (result != MA_SUCCESS || framesRead == 0) {
|
|
|
|
|
LOG_ERROR("AudioEngine: Failed to read frames from WAV: error ", result, ", framesRead=", framesRead);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pcmData->resize(framesRead * channels * ma_get_bytes_per_sample(format));
|
|
|
|
|
|
|
|
|
|
DecodedWavCacheEntry entry;
|
|
|
|
|
entry.format = format;
|
|
|
|
|
entry.channels = channels;
|
|
|
|
|
entry.sampleRate = sampleRate;
|
|
|
|
|
entry.frames = framesRead;
|
|
|
|
|
entry.pcmData = pcmData;
|
2026-02-17 15:13:54 -08:00
|
|
|
// Evict oldest half when cache grows too large (keeps ~128 most-recent sounds)
|
|
|
|
|
if (gDecodedWavCache.size() >= 256) {
|
|
|
|
|
auto it = gDecodedWavCache.begin();
|
|
|
|
|
for (size_t n = gDecodedWavCache.size() / 2; n > 0; --n, ++it) {}
|
|
|
|
|
gDecodedWavCache.erase(gDecodedWavCache.begin(), it);
|
|
|
|
|
}
|
2026-02-11 22:27:02 -08:00
|
|
|
gDecodedWavCache.emplace(key, entry);
|
|
|
|
|
out = entry;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
2026-02-09 00:40:50 -08:00
|
|
|
AudioEngine& AudioEngine::instance() {
|
|
|
|
|
static AudioEngine instance;
|
|
|
|
|
return instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AudioEngine::AudioEngine() = default;
|
|
|
|
|
|
|
|
|
|
AudioEngine::~AudioEngine() {
|
|
|
|
|
shutdown();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool AudioEngine::initialize() {
|
|
|
|
|
if (initialized_) {
|
|
|
|
|
LOG_WARNING("AudioEngine already initialized");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Allocate miniaudio engine
|
|
|
|
|
engine_ = new ma_engine();
|
|
|
|
|
|
|
|
|
|
// Initialize with default config
|
|
|
|
|
ma_result result = ma_engine_init(nullptr, engine_);
|
|
|
|
|
if (result != MA_SUCCESS) {
|
|
|
|
|
LOG_ERROR("Failed to initialize miniaudio engine: ", result);
|
|
|
|
|
delete engine_;
|
|
|
|
|
engine_ = nullptr;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set default master volume
|
|
|
|
|
ma_engine_set_volume(engine_, masterVolume_);
|
|
|
|
|
|
|
|
|
|
// Log audio backend info
|
|
|
|
|
ma_backend backend = ma_engine_get_device(engine_)->pContext->backend;
|
|
|
|
|
const char* backendName = "unknown";
|
|
|
|
|
switch (backend) {
|
|
|
|
|
case ma_backend_wasapi: backendName = "WASAPI"; break;
|
|
|
|
|
case ma_backend_dsound: backendName = "DirectSound"; break;
|
|
|
|
|
case ma_backend_winmm: backendName = "WinMM"; break;
|
|
|
|
|
case ma_backend_coreaudio: backendName = "CoreAudio"; break;
|
|
|
|
|
case ma_backend_sndio: backendName = "sndio"; break;
|
|
|
|
|
case ma_backend_audio4: backendName = "audio(4)"; break;
|
|
|
|
|
case ma_backend_oss: backendName = "OSS"; break;
|
|
|
|
|
case ma_backend_pulseaudio: backendName = "PulseAudio"; break;
|
|
|
|
|
case ma_backend_alsa: backendName = "ALSA"; break;
|
|
|
|
|
case ma_backend_jack: backendName = "JACK"; break;
|
|
|
|
|
case ma_backend_aaudio: backendName = "AAudio"; break;
|
|
|
|
|
case ma_backend_opensl: backendName = "OpenSL|ES"; break;
|
|
|
|
|
case ma_backend_webaudio: backendName = "WebAudio"; break;
|
|
|
|
|
case ma_backend_custom: backendName = "Custom"; break;
|
|
|
|
|
case ma_backend_null: backendName = "Null (no output)"; break;
|
|
|
|
|
default: break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
initialized_ = true;
|
|
|
|
|
LOG_INFO("AudioEngine initialized (miniaudio, backend: ", backendName, ")");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioEngine::shutdown() {
|
|
|
|
|
if (!initialized_) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stop music
|
|
|
|
|
stopMusic();
|
|
|
|
|
|
|
|
|
|
// Clean up all active sounds
|
|
|
|
|
for (auto& activeSound : activeSounds_) {
|
|
|
|
|
ma_sound_uninit(activeSound.sound);
|
2026-02-19 16:17:06 -08:00
|
|
|
std::free(activeSound.sound);
|
2026-02-09 00:40:50 -08:00
|
|
|
ma_audio_buffer* buffer = static_cast<ma_audio_buffer*>(activeSound.buffer);
|
|
|
|
|
ma_audio_buffer_uninit(buffer);
|
2026-02-19 16:17:06 -08:00
|
|
|
std::free(buffer);
|
2026-02-09 00:40:50 -08:00
|
|
|
}
|
|
|
|
|
activeSounds_.clear();
|
|
|
|
|
|
|
|
|
|
if (engine_) {
|
|
|
|
|
ma_engine_uninit(engine_);
|
|
|
|
|
delete engine_;
|
|
|
|
|
engine_ = nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
initialized_ = false;
|
|
|
|
|
LOG_INFO("AudioEngine shutdown");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioEngine::setMasterVolume(float volume) {
|
|
|
|
|
masterVolume_ = glm::clamp(volume, 0.0f, 1.0f);
|
|
|
|
|
if (engine_) {
|
|
|
|
|
ma_engine_set_volume(engine_, masterVolume_);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioEngine::setListenerPosition(const glm::vec3& position) {
|
|
|
|
|
listenerPosition_ = position;
|
|
|
|
|
if (engine_) {
|
|
|
|
|
ma_engine_listener_set_position(engine_, 0, position.x, position.y, position.z);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioEngine::setListenerOrientation(const glm::vec3& forward, const glm::vec3& up) {
|
|
|
|
|
listenerForward_ = forward;
|
|
|
|
|
listenerUp_ = up;
|
|
|
|
|
if (engine_) {
|
|
|
|
|
ma_engine_listener_set_direction(engine_, 0, forward.x, forward.y, forward.z);
|
|
|
|
|
ma_engine_listener_set_world_up(engine_, 0, up.x, up.y, up.z);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool AudioEngine::playSound2D(const std::vector<uint8_t>& wavData, float volume, float pitch) {
|
2026-02-11 22:27:02 -08:00
|
|
|
(void)pitch;
|
|
|
|
|
if (!initialized_ || !engine_ || wavData.empty()) return false;
|
2026-02-09 00:40:50 -08:00
|
|
|
|
2026-02-11 22:27:02 -08:00
|
|
|
DecodedWavCacheEntry decoded;
|
|
|
|
|
if (!decodeWavCached(wavData, decoded) || !decoded.pcmData || decoded.frames == 0) {
|
2026-02-09 00:40:50 -08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create audio buffer from decoded PCM data (heap allocated to keep alive)
|
|
|
|
|
ma_audio_buffer_config bufferConfig = ma_audio_buffer_config_init(
|
2026-02-11 22:27:02 -08:00
|
|
|
decoded.format,
|
|
|
|
|
decoded.channels,
|
|
|
|
|
decoded.frames,
|
|
|
|
|
decoded.pcmData->data(),
|
2026-02-09 00:40:50 -08:00
|
|
|
nullptr // No custom allocator
|
|
|
|
|
);
|
2026-02-11 22:27:02 -08:00
|
|
|
bufferConfig.sampleRate = decoded.sampleRate; // Critical: preserve original sample rate!
|
2026-02-09 00:40:50 -08:00
|
|
|
|
2026-02-19 16:17:06 -08:00
|
|
|
ma_audio_buffer* audioBuffer = static_cast<ma_audio_buffer*>(std::malloc(sizeof(ma_audio_buffer)));
|
|
|
|
|
if (!audioBuffer) return false;
|
2026-02-11 22:27:02 -08:00
|
|
|
ma_result result = ma_audio_buffer_init(&bufferConfig, audioBuffer);
|
2026-02-09 00:40:50 -08:00
|
|
|
if (result != MA_SUCCESS) {
|
|
|
|
|
LOG_WARNING("Failed to create audio buffer: ", result);
|
2026-02-19 16:17:06 -08:00
|
|
|
std::free(audioBuffer);
|
2026-02-09 00:40:50 -08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create sound from audio buffer
|
2026-02-19 16:17:06 -08:00
|
|
|
ma_sound* sound = static_cast<ma_sound*>(std::malloc(sizeof(ma_sound)));
|
|
|
|
|
if (!sound) {
|
|
|
|
|
ma_audio_buffer_uninit(audioBuffer);
|
|
|
|
|
std::free(audioBuffer);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-09 00:40:50 -08:00
|
|
|
result = ma_sound_init_from_data_source(
|
|
|
|
|
engine_,
|
|
|
|
|
audioBuffer,
|
|
|
|
|
MA_SOUND_FLAG_DECODE | MA_SOUND_FLAG_ASYNC | MA_SOUND_FLAG_NO_PITCH | MA_SOUND_FLAG_NO_SPATIALIZATION,
|
|
|
|
|
nullptr,
|
|
|
|
|
sound
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (result != MA_SUCCESS) {
|
|
|
|
|
LOG_WARNING("Failed to create sound: ", result);
|
|
|
|
|
ma_audio_buffer_uninit(audioBuffer);
|
2026-02-19 16:17:06 -08:00
|
|
|
std::free(audioBuffer);
|
|
|
|
|
std::free(sound);
|
2026-02-09 00:40:50 -08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set volume (pitch not supported with NO_PITCH flag)
|
|
|
|
|
ma_sound_set_volume(sound, volume * masterVolume_);
|
|
|
|
|
|
|
|
|
|
// Start playback
|
|
|
|
|
result = ma_sound_start(sound);
|
|
|
|
|
if (result != MA_SUCCESS) {
|
|
|
|
|
LOG_WARNING("Failed to start sound: ", result);
|
|
|
|
|
ma_sound_uninit(sound);
|
|
|
|
|
ma_audio_buffer_uninit(audioBuffer);
|
2026-02-19 16:17:06 -08:00
|
|
|
std::free(audioBuffer);
|
|
|
|
|
std::free(sound);
|
2026-02-09 00:40:50 -08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 22:27:02 -08:00
|
|
|
// Track this sound for cleanup (decoded PCM shared across plays)
|
|
|
|
|
activeSounds_.push_back({sound, audioBuffer, decoded.pcmData});
|
2026-02-09 00:40:50 -08:00
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool AudioEngine::playSound2D(const std::string& mpqPath, float volume, float pitch) {
|
2026-02-17 18:52:19 -08:00
|
|
|
if (!assetManager_) {
|
|
|
|
|
LOG_WARNING("AudioEngine::playSound2D(path): no AssetManager set");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
auto data = assetManager_->readFile(mpqPath);
|
|
|
|
|
if (data.empty()) {
|
|
|
|
|
LOG_WARNING("AudioEngine::playSound2D: failed to load '", mpqPath, "'");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return playSound2D(data, volume, pitch);
|
2026-02-09 00:40:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool AudioEngine::playSound3D(const std::vector<uint8_t>& wavData, const glm::vec3& position,
|
|
|
|
|
float volume, float pitch, float maxDistance) {
|
2026-02-11 22:27:02 -08:00
|
|
|
if (!initialized_ || !engine_ || wavData.empty()) return false;
|
2026-02-09 01:01:21 -08:00
|
|
|
|
2026-02-11 22:27:02 -08:00
|
|
|
DecodedWavCacheEntry decoded;
|
|
|
|
|
if (!decodeWavCached(wavData, decoded) || !decoded.pcmData || decoded.frames == 0) {
|
2026-02-09 01:01:21 -08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 22:27:02 -08:00
|
|
|
LOG_DEBUG("playSound3D: cached WAV - format:", decoded.format,
|
|
|
|
|
" channels:", decoded.channels, " sampleRate:", decoded.sampleRate,
|
|
|
|
|
" pitch:", pitch);
|
2026-02-09 01:01:21 -08:00
|
|
|
|
2026-02-09 02:22:20 -08:00
|
|
|
// Create audio buffer with correct sample rate
|
2026-02-09 01:01:21 -08:00
|
|
|
ma_audio_buffer_config bufferConfig = ma_audio_buffer_config_init(
|
2026-02-11 22:27:02 -08:00
|
|
|
decoded.format,
|
|
|
|
|
decoded.channels,
|
|
|
|
|
decoded.frames,
|
|
|
|
|
decoded.pcmData->data(),
|
2026-02-09 01:01:21 -08:00
|
|
|
nullptr
|
|
|
|
|
);
|
2026-02-11 22:27:02 -08:00
|
|
|
bufferConfig.sampleRate = decoded.sampleRate; // Critical: preserve original sample rate!
|
2026-02-09 01:01:21 -08:00
|
|
|
|
2026-02-19 16:17:06 -08:00
|
|
|
ma_audio_buffer* audioBuffer = static_cast<ma_audio_buffer*>(std::malloc(sizeof(ma_audio_buffer)));
|
|
|
|
|
if (!audioBuffer) return false;
|
2026-02-11 22:27:02 -08:00
|
|
|
ma_result result = ma_audio_buffer_init(&bufferConfig, audioBuffer);
|
2026-02-09 01:01:21 -08:00
|
|
|
if (result != MA_SUCCESS) {
|
2026-02-19 16:17:06 -08:00
|
|
|
std::free(audioBuffer);
|
2026-02-09 01:01:21 -08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 02:22:20 -08:00
|
|
|
// Create 3D sound (spatialization enabled, pitch enabled)
|
2026-02-19 16:17:06 -08:00
|
|
|
ma_sound* sound = static_cast<ma_sound*>(std::malloc(sizeof(ma_sound)));
|
|
|
|
|
if (!sound) {
|
|
|
|
|
ma_audio_buffer_uninit(audioBuffer);
|
|
|
|
|
std::free(audioBuffer);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-09 01:01:21 -08:00
|
|
|
result = ma_sound_init_from_data_source(
|
|
|
|
|
engine_,
|
|
|
|
|
audioBuffer,
|
2026-02-09 02:22:20 -08:00
|
|
|
MA_SOUND_FLAG_DECODE | MA_SOUND_FLAG_ASYNC, // Removed NO_PITCH flag
|
2026-02-09 01:01:21 -08:00
|
|
|
nullptr,
|
|
|
|
|
sound
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (result != MA_SUCCESS) {
|
2026-02-09 02:22:20 -08:00
|
|
|
LOG_WARNING("playSound3D: Failed to create sound, error: ", result);
|
2026-02-09 01:01:21 -08:00
|
|
|
ma_audio_buffer_uninit(audioBuffer);
|
2026-02-19 16:17:06 -08:00
|
|
|
std::free(audioBuffer);
|
|
|
|
|
std::free(sound);
|
2026-02-09 01:01:21 -08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set 3D position and attenuation
|
|
|
|
|
ma_sound_set_position(sound, position.x, position.y, position.z);
|
|
|
|
|
ma_sound_set_volume(sound, volume * masterVolume_);
|
2026-02-09 02:22:20 -08:00
|
|
|
ma_sound_set_pitch(sound, pitch); // Enable pitch variation
|
2026-02-09 01:01:21 -08:00
|
|
|
ma_sound_set_attenuation_model(sound, ma_attenuation_model_inverse);
|
|
|
|
|
ma_sound_set_min_gain(sound, 0.0f);
|
|
|
|
|
ma_sound_set_max_gain(sound, 1.0f);
|
|
|
|
|
ma_sound_set_min_distance(sound, 1.0f);
|
|
|
|
|
ma_sound_set_max_distance(sound, maxDistance);
|
|
|
|
|
ma_sound_set_rolloff(sound, 1.0f);
|
|
|
|
|
|
|
|
|
|
result = ma_sound_start(sound);
|
|
|
|
|
if (result != MA_SUCCESS) {
|
|
|
|
|
ma_sound_uninit(sound);
|
|
|
|
|
ma_audio_buffer_uninit(audioBuffer);
|
2026-02-19 16:17:06 -08:00
|
|
|
std::free(audioBuffer);
|
|
|
|
|
std::free(sound);
|
2026-02-09 01:01:21 -08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Track for cleanup
|
2026-02-11 22:27:02 -08:00
|
|
|
activeSounds_.push_back({sound, audioBuffer, decoded.pcmData});
|
2026-02-09 01:01:21 -08:00
|
|
|
|
|
|
|
|
return true;
|
2026-02-09 00:40:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool AudioEngine::playSound3D(const std::string& mpqPath, const glm::vec3& position,
|
|
|
|
|
float volume, float pitch, float maxDistance) {
|
2026-02-17 18:52:19 -08:00
|
|
|
if (!assetManager_) {
|
|
|
|
|
LOG_WARNING("AudioEngine::playSound3D(path): no AssetManager set");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
auto data = assetManager_->readFile(mpqPath);
|
|
|
|
|
if (data.empty()) {
|
|
|
|
|
LOG_WARNING("AudioEngine::playSound3D: failed to load '", mpqPath, "'");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return playSound3D(data, position, volume, pitch, maxDistance);
|
2026-02-09 00:40:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool AudioEngine::playMusic(const std::vector<uint8_t>& musicData, float volume, bool loop) {
|
|
|
|
|
if (!initialized_ || !engine_ || musicData.empty()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("AudioEngine::playMusic - data size: ", musicData.size(), " bytes, volume: ", volume);
|
|
|
|
|
|
|
|
|
|
// Stop any currently playing music
|
|
|
|
|
stopMusic();
|
|
|
|
|
|
|
|
|
|
// Keep the music data alive
|
|
|
|
|
musicData_ = musicData;
|
|
|
|
|
musicVolume_ = volume;
|
|
|
|
|
|
|
|
|
|
// Create decoder from memory (for streaming MP3/OGG)
|
|
|
|
|
ma_decoder* decoder = new ma_decoder();
|
|
|
|
|
ma_decoder_config decoderConfig = ma_decoder_config_init_default();
|
|
|
|
|
ma_result result = ma_decoder_init_memory(
|
|
|
|
|
musicData_.data(),
|
|
|
|
|
musicData_.size(),
|
|
|
|
|
&decoderConfig,
|
|
|
|
|
decoder
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (result != MA_SUCCESS) {
|
|
|
|
|
LOG_ERROR("Failed to create music decoder: ", result);
|
|
|
|
|
delete decoder;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Decoder created - format: ", decoder->outputFormat,
|
|
|
|
|
", channels: ", decoder->outputChannels,
|
|
|
|
|
", sampleRate: ", decoder->outputSampleRate);
|
|
|
|
|
|
|
|
|
|
musicDecoder_ = decoder;
|
|
|
|
|
|
|
|
|
|
// Create streaming sound from decoder
|
2026-02-19 16:17:06 -08:00
|
|
|
musicSound_ = static_cast<ma_sound*>(std::malloc(sizeof(ma_sound)));
|
|
|
|
|
if (!musicSound_) {
|
|
|
|
|
ma_decoder_uninit(decoder);
|
|
|
|
|
delete decoder;
|
|
|
|
|
musicDecoder_ = nullptr;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-09 00:40:50 -08:00
|
|
|
result = ma_sound_init_from_data_source(
|
|
|
|
|
engine_,
|
|
|
|
|
decoder,
|
|
|
|
|
MA_SOUND_FLAG_STREAM | MA_SOUND_FLAG_NO_PITCH | MA_SOUND_FLAG_NO_SPATIALIZATION,
|
|
|
|
|
nullptr,
|
|
|
|
|
musicSound_
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (result != MA_SUCCESS) {
|
|
|
|
|
LOG_ERROR("Failed to create music sound: ", result);
|
|
|
|
|
ma_decoder_uninit(decoder);
|
|
|
|
|
delete decoder;
|
|
|
|
|
musicDecoder_ = nullptr;
|
2026-02-19 16:17:06 -08:00
|
|
|
std::free(musicSound_);
|
2026-02-09 00:40:50 -08:00
|
|
|
musicSound_ = nullptr;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set volume and looping
|
|
|
|
|
ma_sound_set_volume(musicSound_, volume * masterVolume_);
|
|
|
|
|
ma_sound_set_looping(musicSound_, loop ? MA_TRUE : MA_FALSE);
|
|
|
|
|
|
|
|
|
|
// Start playback
|
|
|
|
|
result = ma_sound_start(musicSound_);
|
|
|
|
|
if (result != MA_SUCCESS) {
|
|
|
|
|
LOG_ERROR("Failed to start music playback: ", result);
|
|
|
|
|
ma_sound_uninit(musicSound_);
|
2026-02-19 16:17:06 -08:00
|
|
|
std::free(musicSound_);
|
2026-02-09 00:40:50 -08:00
|
|
|
musicSound_ = nullptr;
|
|
|
|
|
ma_decoder_uninit(decoder);
|
|
|
|
|
delete decoder;
|
|
|
|
|
musicDecoder_ = nullptr;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Music playback started successfully - volume: ", volume,
|
|
|
|
|
", loop: ", loop,
|
|
|
|
|
", is_playing: ", ma_sound_is_playing(musicSound_));
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioEngine::stopMusic() {
|
|
|
|
|
if (musicSound_) {
|
|
|
|
|
ma_sound_uninit(musicSound_);
|
2026-02-19 16:17:06 -08:00
|
|
|
std::free(musicSound_);
|
2026-02-09 00:40:50 -08:00
|
|
|
musicSound_ = nullptr;
|
|
|
|
|
}
|
|
|
|
|
if (musicDecoder_) {
|
|
|
|
|
ma_decoder* decoder = static_cast<ma_decoder*>(musicDecoder_);
|
|
|
|
|
ma_decoder_uninit(decoder);
|
|
|
|
|
delete decoder;
|
|
|
|
|
musicDecoder_ = nullptr;
|
|
|
|
|
}
|
|
|
|
|
musicData_.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool AudioEngine::isMusicPlaying() const {
|
|
|
|
|
if (!musicSound_) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return ma_sound_is_playing(musicSound_) == MA_TRUE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioEngine::setMusicVolume(float volume) {
|
|
|
|
|
musicVolume_ = glm::clamp(volume, 0.0f, 1.0f);
|
|
|
|
|
if (musicSound_) {
|
|
|
|
|
ma_sound_set_volume(musicSound_, musicVolume_ * masterVolume_);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AudioEngine::update(float deltaTime) {
|
|
|
|
|
(void)deltaTime;
|
|
|
|
|
|
|
|
|
|
if (!initialized_ || !engine_) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clean up finished sounds
|
|
|
|
|
for (auto it = activeSounds_.begin(); it != activeSounds_.end(); ) {
|
|
|
|
|
if (!ma_sound_is_playing(it->sound)) {
|
|
|
|
|
// Sound finished, clean up
|
|
|
|
|
ma_sound_uninit(it->sound);
|
2026-02-19 16:17:06 -08:00
|
|
|
std::free(it->sound);
|
2026-02-09 00:40:50 -08:00
|
|
|
ma_audio_buffer* buffer = static_cast<ma_audio_buffer*>(it->buffer);
|
|
|
|
|
ma_audio_buffer_uninit(buffer);
|
2026-02-19 16:17:06 -08:00
|
|
|
std::free(buffer);
|
2026-02-09 00:40:50 -08:00
|
|
|
it = activeSounds_.erase(it);
|
|
|
|
|
} else {
|
|
|
|
|
++it;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace audio
|
|
|
|
|
} // namespace wowee
|