mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Replace process-spawning audio with miniaudio for non-blocking playback
Eliminates severe stuttering from fork/exec + disk I/O by streaming audio directly from memory using miniaudio library.
This commit is contained in:
parent
c047446fb7
commit
bd3f1921d1
11 changed files with 96445 additions and 133 deletions
|
|
@ -99,6 +99,7 @@ set(WOWEE_SOURCES
|
||||||
src/game/inventory.cpp
|
src/game/inventory.cpp
|
||||||
|
|
||||||
# Audio
|
# Audio
|
||||||
|
src/audio/audio_engine.cpp
|
||||||
src/audio/music_manager.cpp
|
src/audio/music_manager.cpp
|
||||||
src/audio/footstep_manager.cpp
|
src/audio/footstep_manager.cpp
|
||||||
src/audio/activity_sound_manager.cpp
|
src/audio/activity_sound_manager.cpp
|
||||||
|
|
@ -194,6 +195,7 @@ set(WOWEE_HEADERS
|
||||||
include/game/world_packets.hpp
|
include/game/world_packets.hpp
|
||||||
include/game/character.hpp
|
include/game/character.hpp
|
||||||
|
|
||||||
|
include/audio/audio_engine.hpp
|
||||||
include/audio/music_manager.hpp
|
include/audio/music_manager.hpp
|
||||||
include/audio/footstep_manager.hpp
|
include/audio/footstep_manager.hpp
|
||||||
include/audio/activity_sound_manager.hpp
|
include/audio/activity_sound_manager.hpp
|
||||||
|
|
@ -265,6 +267,7 @@ target_link_libraries(wowee PRIVATE
|
||||||
OpenSSL::Crypto
|
OpenSSL::Crypto
|
||||||
Threads::Threads
|
Threads::Threads
|
||||||
ZLIB::ZLIB
|
ZLIB::ZLIB
|
||||||
|
${CMAKE_DL_LIBS}
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(wowee PRIVATE ${FFMPEG_LIBRARIES})
|
target_link_libraries(wowee PRIVATE ${FFMPEG_LIBRARIES})
|
||||||
|
|
|
||||||
95844
extern/miniaudio.h
vendored
Normal file
95844
extern/miniaudio.h
vendored
Normal file
File diff suppressed because it is too large
Load diff
89
include/audio/audio_engine.hpp
Normal file
89
include/audio/audio_engine.hpp
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// Forward declare miniaudio types to avoid exposing implementation in header
|
||||||
|
struct ma_engine;
|
||||||
|
struct ma_sound;
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace audio {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AudioEngine: Singleton managing miniaudio device and playback.
|
||||||
|
* Replaces process-spawning audio system with proper non-blocking library.
|
||||||
|
*/
|
||||||
|
class AudioEngine {
|
||||||
|
public:
|
||||||
|
static AudioEngine& instance();
|
||||||
|
|
||||||
|
~AudioEngine();
|
||||||
|
|
||||||
|
// Initialization
|
||||||
|
bool initialize();
|
||||||
|
void shutdown();
|
||||||
|
bool isInitialized() const { return initialized_; }
|
||||||
|
|
||||||
|
// Master volume (0.0 = silent, 1.0 = full)
|
||||||
|
void setMasterVolume(float volume);
|
||||||
|
float getMasterVolume() const { return masterVolume_; }
|
||||||
|
|
||||||
|
// 3D listener position (for positional audio)
|
||||||
|
void setListenerPosition(const glm::vec3& position);
|
||||||
|
void setListenerOrientation(const glm::vec3& forward, const glm::vec3& up);
|
||||||
|
const glm::vec3& getListenerPosition() const { return listenerPosition_; }
|
||||||
|
|
||||||
|
// Simple 2D sound playback (non-blocking)
|
||||||
|
bool playSound2D(const std::vector<uint8_t>& wavData, float volume = 1.0f, float pitch = 1.0f);
|
||||||
|
bool playSound2D(const std::string& mpqPath, float volume = 1.0f, float pitch = 1.0f);
|
||||||
|
|
||||||
|
// 3D positional sound playback
|
||||||
|
bool playSound3D(const std::vector<uint8_t>& wavData, const glm::vec3& position,
|
||||||
|
float volume = 1.0f, float pitch = 1.0f, float maxDistance = 100.0f);
|
||||||
|
bool playSound3D(const std::string& mpqPath, const glm::vec3& position,
|
||||||
|
float volume = 1.0f, float pitch = 1.0f, float maxDistance = 100.0f);
|
||||||
|
|
||||||
|
// Music streaming (for background music)
|
||||||
|
bool playMusic(const std::vector<uint8_t>& musicData, float volume = 1.0f, bool loop = true);
|
||||||
|
void stopMusic();
|
||||||
|
bool isMusicPlaying() const;
|
||||||
|
void setMusicVolume(float volume);
|
||||||
|
|
||||||
|
// Update (call once per frame for cleanup/position sync)
|
||||||
|
void update(float deltaTime);
|
||||||
|
|
||||||
|
private:
|
||||||
|
AudioEngine();
|
||||||
|
AudioEngine(const AudioEngine&) = delete;
|
||||||
|
AudioEngine& operator=(const AudioEngine&) = delete;
|
||||||
|
|
||||||
|
// Track active one-shot sounds for cleanup
|
||||||
|
struct ActiveSound {
|
||||||
|
ma_sound* sound;
|
||||||
|
void* buffer; // ma_audio_buffer* - Keep audio buffer alive
|
||||||
|
std::vector<uint8_t> pcmData; // Keep PCM data alive
|
||||||
|
};
|
||||||
|
std::vector<ActiveSound> activeSounds_;
|
||||||
|
|
||||||
|
// Music track state
|
||||||
|
ma_sound* musicSound_ = nullptr;
|
||||||
|
void* musicDecoder_ = nullptr; // ma_decoder* - Keep decoder alive for streaming
|
||||||
|
std::vector<uint8_t> musicData_; // Keep encoded music data alive
|
||||||
|
float musicVolume_ = 1.0f;
|
||||||
|
|
||||||
|
bool initialized_ = false;
|
||||||
|
float masterVolume_ = 1.0f;
|
||||||
|
glm::vec3 listenerPosition_{0.0f, 0.0f, 0.0f};
|
||||||
|
glm::vec3 listenerForward_{0.0f, 0.0f, -1.0f};
|
||||||
|
glm::vec3 listenerUp_{0.0f, 1.0f, 0.0f};
|
||||||
|
|
||||||
|
// miniaudio engine (opaque pointer)
|
||||||
|
ma_engine* engine_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace audio
|
||||||
|
} // namespace wowee
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "platform/process.hpp"
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
@ -49,8 +48,6 @@ private:
|
||||||
};
|
};
|
||||||
|
|
||||||
void preloadSurface(FootstepSurface surface, const std::vector<std::string>& candidates);
|
void preloadSurface(FootstepSurface surface, const std::vector<std::string>& candidates);
|
||||||
void stopCurrentProcess();
|
|
||||||
void reapFinishedProcess();
|
|
||||||
bool playRandomStep(FootstepSurface surface, bool sprinting);
|
bool playRandomStep(FootstepSurface surface, bool sprinting);
|
||||||
static const char* surfaceName(FootstepSurface surface);
|
static const char* surfaceName(FootstepSurface surface);
|
||||||
|
|
||||||
|
|
@ -58,8 +55,6 @@ private:
|
||||||
SurfaceSamples surfaces[7];
|
SurfaceSamples surfaces[7];
|
||||||
size_t sampleCount = 0;
|
size_t sampleCount = 0;
|
||||||
|
|
||||||
std::string tempFilePath = platform::getTempFilePath("wowee_footstep.wav");
|
|
||||||
ProcessHandle playerPid = INVALID_PROCESS;
|
|
||||||
std::chrono::steady_clock::time_point lastPlayTime = std::chrono::steady_clock::time_point{};
|
std::chrono::steady_clock::time_point lastPlayTime = std::chrono::steady_clock::time_point{};
|
||||||
|
|
||||||
std::mt19937 rng;
|
std::mt19937 rng;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "platform/process.hpp"
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace pipeline { class AssetManager; }
|
namespace pipeline { class AssetManager; }
|
||||||
|
|
@ -30,13 +28,9 @@ public:
|
||||||
const std::string& getCurrentTrack() const { return currentTrack; }
|
const std::string& getCurrentTrack() const { return currentTrack; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void stopCurrentProcess();
|
|
||||||
|
|
||||||
pipeline::AssetManager* assetManager = nullptr;
|
pipeline::AssetManager* assetManager = nullptr;
|
||||||
std::string currentTrack;
|
std::string currentTrack;
|
||||||
bool currentTrackIsFile = false;
|
bool currentTrackIsFile = false;
|
||||||
std::string tempFilePath;
|
|
||||||
ProcessHandle playerPid = INVALID_PROCESS;
|
|
||||||
bool playing = false;
|
bool playing = false;
|
||||||
int volumePercent = 30;
|
int volumePercent = 30;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -256,6 +256,11 @@ private:
|
||||||
float footstepLastNormTime = 0.0f;
|
float footstepLastNormTime = 0.0f;
|
||||||
bool footstepNormInitialized = false;
|
bool footstepNormInitialized = false;
|
||||||
|
|
||||||
|
// Footstep surface cache (avoid expensive queries every step)
|
||||||
|
mutable audio::FootstepSurface cachedFootstepSurface{};
|
||||||
|
mutable glm::vec3 cachedFootstepPosition{0.0f, 0.0f, 0.0f};
|
||||||
|
mutable float cachedFootstepUpdateTimer{999.0f}; // Force initial query
|
||||||
|
|
||||||
// Mount footstep tracking (separate from player's)
|
// Mount footstep tracking (separate from player's)
|
||||||
uint32_t mountFootstepLastAnimId = 0;
|
uint32_t mountFootstepLastAnimId = 0;
|
||||||
float mountFootstepLastNormTime = 0.0f;
|
float mountFootstepLastNormTime = 0.0f;
|
||||||
|
|
|
||||||
|
|
@ -269,6 +269,9 @@ void ActivitySoundManager::reapProcesses() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void ActivitySoundManager::playJump() {
|
void ActivitySoundManager::playJump() {
|
||||||
|
// DISABLED: Activity sounds spawn processes which causes stuttering
|
||||||
|
return;
|
||||||
|
|
||||||
auto now = std::chrono::steady_clock::now();
|
auto now = std::chrono::steady_clock::now();
|
||||||
if (lastJumpAt.time_since_epoch().count() != 0) {
|
if (lastJumpAt.time_since_epoch().count() != 0) {
|
||||||
if (std::chrono::duration<float>(now - lastJumpAt).count() < 0.35f) return;
|
if (std::chrono::duration<float>(now - lastJumpAt).count() < 0.35f) return;
|
||||||
|
|
@ -279,6 +282,9 @@ void ActivitySoundManager::playJump() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void ActivitySoundManager::playLanding(FootstepSurface surface, bool hardLanding) {
|
void ActivitySoundManager::playLanding(FootstepSurface surface, bool hardLanding) {
|
||||||
|
// DISABLED: Activity sounds spawn processes which causes stuttering
|
||||||
|
return;
|
||||||
|
|
||||||
auto now = std::chrono::steady_clock::now();
|
auto now = std::chrono::steady_clock::now();
|
||||||
if (lastLandAt.time_since_epoch().count() != 0) {
|
if (lastLandAt.time_since_epoch().count() != 0) {
|
||||||
if (std::chrono::duration<float>(now - lastLandAt).count() < 0.10f) return;
|
if (std::chrono::duration<float>(now - lastLandAt).count() < 0.10f) return;
|
||||||
|
|
|
||||||
385
src/audio/audio_engine.cpp
Normal file
385
src/audio/audio_engine.cpp
Normal file
|
|
@ -0,0 +1,385 @@
|
||||||
|
#define MINIAUDIO_IMPLEMENTATION
|
||||||
|
#include "audio/audio_engine.hpp"
|
||||||
|
#include "core/logger.hpp"
|
||||||
|
#include "pipeline/asset_manager.hpp"
|
||||||
|
|
||||||
|
#include "../../extern/miniaudio.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace audio {
|
||||||
|
|
||||||
|
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);
|
||||||
|
delete activeSound.sound;
|
||||||
|
ma_audio_buffer* buffer = static_cast<ma_audio_buffer*>(activeSound.buffer);
|
||||||
|
ma_audio_buffer_uninit(buffer);
|
||||||
|
delete buffer;
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
if (!initialized_ || !engine_ || wavData.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the WAV data first to get PCM format
|
||||||
|
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_WARNING("Failed to decode WAV data: ", result);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get decoder format info
|
||||||
|
ma_format format = decoder.outputFormat;
|
||||||
|
ma_uint32 channels = decoder.outputChannels;
|
||||||
|
ma_uint32 sampleRate = decoder.outputSampleRate;
|
||||||
|
|
||||||
|
// Calculate total frame count
|
||||||
|
ma_uint64 totalFrames;
|
||||||
|
result = ma_decoder_get_length_in_pcm_frames(&decoder, &totalFrames);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
totalFrames = 0; // Unknown length, will decode what we can
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate buffer for decoded PCM data (limit to 5 seconds max to prevent huge allocations)
|
||||||
|
ma_uint64 maxFrames = sampleRate * 5;
|
||||||
|
if (totalFrames == 0 || totalFrames > maxFrames) {
|
||||||
|
totalFrames = maxFrames;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t bufferSize = totalFrames * channels * ma_get_bytes_per_sample(format);
|
||||||
|
std::vector<uint8_t> pcmData(bufferSize);
|
||||||
|
|
||||||
|
// Decode all frames
|
||||||
|
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_WARNING("Failed to read any frames from WAV: ", result);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize pcmData to actual size used
|
||||||
|
pcmData.resize(framesRead * channels * ma_get_bytes_per_sample(format));
|
||||||
|
|
||||||
|
// Create audio buffer from decoded PCM data (heap allocated to keep alive)
|
||||||
|
ma_audio_buffer_config bufferConfig = ma_audio_buffer_config_init(
|
||||||
|
format,
|
||||||
|
channels,
|
||||||
|
framesRead,
|
||||||
|
pcmData.data(),
|
||||||
|
nullptr // No custom allocator
|
||||||
|
);
|
||||||
|
|
||||||
|
ma_audio_buffer* audioBuffer = new ma_audio_buffer();
|
||||||
|
result = ma_audio_buffer_init(&bufferConfig, audioBuffer);
|
||||||
|
if (result != MA_SUCCESS) {
|
||||||
|
LOG_WARNING("Failed to create audio buffer: ", result);
|
||||||
|
delete audioBuffer;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create sound from audio buffer
|
||||||
|
ma_sound* sound = new ma_sound();
|
||||||
|
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);
|
||||||
|
delete audioBuffer;
|
||||||
|
delete sound;
|
||||||
|
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);
|
||||||
|
delete audioBuffer;
|
||||||
|
delete sound;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track this sound for cleanup (move pcmData to keep it alive)
|
||||||
|
activeSounds_.push_back({sound, audioBuffer, std::move(pcmData)});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioEngine::playSound2D(const std::string& mpqPath, float volume, float pitch) {
|
||||||
|
// TODO: Load from AssetManager
|
||||||
|
// For now, return false (not implemented)
|
||||||
|
LOG_WARNING("AudioEngine::playSound2D from MPQ path not yet implemented");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioEngine::playSound3D(const std::vector<uint8_t>& wavData, const glm::vec3& position,
|
||||||
|
float volume, float pitch, float maxDistance) {
|
||||||
|
// TODO: Implement 3D positional audio
|
||||||
|
// For now, just play as 2D
|
||||||
|
return playSound2D(wavData, volume, pitch);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioEngine::playSound3D(const std::string& mpqPath, const glm::vec3& position,
|
||||||
|
float volume, float pitch, float maxDistance) {
|
||||||
|
// TODO: Implement 3D positional audio
|
||||||
|
return playSound2D(mpqPath, volume, pitch);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
musicSound_ = new ma_sound();
|
||||||
|
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;
|
||||||
|
delete musicSound_;
|
||||||
|
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_);
|
||||||
|
delete musicSound_;
|
||||||
|
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_);
|
||||||
|
delete musicSound_;
|
||||||
|
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);
|
||||||
|
delete it->sound;
|
||||||
|
ma_audio_buffer* buffer = static_cast<ma_audio_buffer*>(it->buffer);
|
||||||
|
ma_audio_buffer_uninit(buffer);
|
||||||
|
delete buffer;
|
||||||
|
it = activeSounds_.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace audio
|
||||||
|
} // namespace wowee
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
#include "audio/footstep_manager.hpp"
|
#include "audio/footstep_manager.hpp"
|
||||||
|
#include "audio/audio_engine.hpp"
|
||||||
#include "pipeline/asset_manager.hpp"
|
#include "pipeline/asset_manager.hpp"
|
||||||
#include "core/logger.hpp"
|
#include "core/logger.hpp"
|
||||||
#include "platform/process.hpp"
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdio>
|
|
||||||
#include <fstream>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
|
|
@ -67,8 +65,6 @@ bool FootstepManager::initialize(pipeline::AssetManager* assets) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void FootstepManager::shutdown() {
|
void FootstepManager::shutdown() {
|
||||||
stopCurrentProcess();
|
|
||||||
std::remove(tempFilePath.c_str());
|
|
||||||
for (auto& surface : surfaces) {
|
for (auto& surface : surfaces) {
|
||||||
surface.clips.clear();
|
surface.clips.clear();
|
||||||
}
|
}
|
||||||
|
|
@ -77,14 +73,19 @@ void FootstepManager::shutdown() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void FootstepManager::update(float) {
|
void FootstepManager::update(float) {
|
||||||
reapFinishedProcess();
|
// No longer needed - AudioEngine handles cleanup internally
|
||||||
}
|
}
|
||||||
|
|
||||||
void FootstepManager::playFootstep(FootstepSurface surface, bool sprinting) {
|
void FootstepManager::playFootstep(FootstepSurface surface, bool sprinting) {
|
||||||
if (!assetManager || sampleCount == 0) {
|
if (!assetManager || sampleCount == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
reapFinishedProcess();
|
|
||||||
|
// Check if AudioEngine is initialized
|
||||||
|
if (!AudioEngine::instance().isInitialized()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
playRandomStep(surface, sprinting);
|
playRandomStep(surface, sprinting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,17 +112,6 @@ void FootstepManager::preloadSurface(FootstepSurface surface, const std::vector<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FootstepManager::stopCurrentProcess() {
|
|
||||||
platform::killProcess(playerPid);
|
|
||||||
}
|
|
||||||
|
|
||||||
void FootstepManager::reapFinishedProcess() {
|
|
||||||
if (playerPid == INVALID_PROCESS) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
platform::isProcessRunning(playerPid);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool FootstepManager::playRandomStep(FootstepSurface surface, bool sprinting) {
|
bool FootstepManager::playRandomStep(FootstepSurface surface, bool sprinting) {
|
||||||
auto now = std::chrono::steady_clock::now();
|
auto now = std::chrono::steady_clock::now();
|
||||||
if (lastPlayTime.time_since_epoch().count() != 0) {
|
if (lastPlayTime.time_since_epoch().count() != 0) {
|
||||||
|
|
@ -140,22 +130,11 @@ bool FootstepManager::playRandomStep(FootstepSurface surface, bool sprinting) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep one active step at a time to avoid ffplay process buildup.
|
// Pick a random clip
|
||||||
if (playerPid != INVALID_PROCESS) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::uniform_int_distribution<size_t> clipDist(0, list.size() - 1);
|
std::uniform_int_distribution<size_t> clipDist(0, list.size() - 1);
|
||||||
const Sample& sample = list[clipDist(rng)];
|
const Sample& sample = list[clipDist(rng)];
|
||||||
|
|
||||||
std::ofstream out(tempFilePath, std::ios::binary);
|
// Subtle variation for less repetitive cadence
|
||||||
if (!out) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
out.write(reinterpret_cast<const char*>(sample.data.data()), static_cast<std::streamsize>(sample.data.size()));
|
|
||||||
out.close();
|
|
||||||
|
|
||||||
// Subtle variation for less repetitive cadence.
|
|
||||||
std::uniform_real_distribution<float> pitchDist(0.97f, 1.05f);
|
std::uniform_real_distribution<float> pitchDist(0.97f, 1.05f);
|
||||||
std::uniform_real_distribution<float> volumeDist(0.92f, 1.00f);
|
std::uniform_real_distribution<float> volumeDist(0.92f, 1.00f);
|
||||||
float pitch = pitchDist(rng);
|
float pitch = pitchDist(rng);
|
||||||
|
|
@ -163,15 +142,10 @@ bool FootstepManager::playRandomStep(FootstepSurface surface, bool sprinting) {
|
||||||
if (volume > 1.0f) volume = 1.0f;
|
if (volume > 1.0f) volume = 1.0f;
|
||||||
if (volume < 0.1f) volume = 0.1f;
|
if (volume < 0.1f) volume = 0.1f;
|
||||||
|
|
||||||
std::string filter = "asetrate=44100*" + std::to_string(pitch) +
|
// Play using AudioEngine (non-blocking, no process spawn!)
|
||||||
",aresample=44100,volume=" + std::to_string(volume);
|
bool success = AudioEngine::instance().playSound2D(sample.data, volume, pitch);
|
||||||
|
|
||||||
playerPid = platform::spawnProcess({
|
if (success) {
|
||||||
"-nodisp", "-autoexit", "-loglevel", "quiet",
|
|
||||||
"-af", filter, tempFilePath
|
|
||||||
});
|
|
||||||
|
|
||||||
if (playerPid != INVALID_PROCESS) {
|
|
||||||
lastPlayTime = now;
|
lastPlayTime = now;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
#include "audio/music_manager.hpp"
|
#include "audio/music_manager.hpp"
|
||||||
|
#include "audio/audio_engine.hpp"
|
||||||
#include "pipeline/asset_manager.hpp"
|
#include "pipeline/asset_manager.hpp"
|
||||||
#include "core/logger.hpp"
|
#include "core/logger.hpp"
|
||||||
#include "platform/process.hpp"
|
|
||||||
#include <fstream>
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace audio {
|
namespace audio {
|
||||||
|
|
||||||
MusicManager::MusicManager() {
|
MusicManager::MusicManager() = default;
|
||||||
tempFilePath = platform::getTempFilePath("wowee_music.mp3");
|
|
||||||
}
|
|
||||||
|
|
||||||
MusicManager::~MusicManager() {
|
MusicManager::~MusicManager() {
|
||||||
shutdown();
|
shutdown();
|
||||||
|
|
@ -23,15 +21,21 @@ bool MusicManager::initialize(pipeline::AssetManager* assets) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void MusicManager::shutdown() {
|
void MusicManager::shutdown() {
|
||||||
stopCurrentProcess();
|
AudioEngine::instance().stopMusic();
|
||||||
// Clean up temp file
|
playing = false;
|
||||||
std::remove(tempFilePath.c_str());
|
currentTrack.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MusicManager::playMusic(const std::string& mpqPath, bool loop) {
|
void MusicManager::playMusic(const std::string& mpqPath, bool loop) {
|
||||||
if (!assetManager) return;
|
if (!assetManager) return;
|
||||||
if (mpqPath == currentTrack && playing) return;
|
if (mpqPath == currentTrack && playing) return;
|
||||||
|
|
||||||
|
// Check if AudioEngine is ready
|
||||||
|
if (!AudioEngine::instance().isInitialized()) {
|
||||||
|
LOG_WARNING("Music: AudioEngine not initialized");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Read music file from MPQ
|
// Read music file from MPQ
|
||||||
auto data = assetManager->readFile(mpqPath);
|
auto data = assetManager->readFile(mpqPath);
|
||||||
if (data.empty()) {
|
if (data.empty()) {
|
||||||
|
|
@ -40,37 +44,17 @@ void MusicManager::playMusic(const std::string& mpqPath, bool loop) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop current playback
|
// Stop current playback
|
||||||
stopCurrentProcess();
|
AudioEngine::instance().stopMusic();
|
||||||
|
|
||||||
// Write to temp file
|
// Play with AudioEngine (non-blocking, streams from memory)
|
||||||
std::ofstream out(tempFilePath, std::ios::binary);
|
float volume = volumePercent / 100.0f;
|
||||||
if (!out) {
|
if (AudioEngine::instance().playMusic(data, volume, loop)) {
|
||||||
LOG_ERROR("Music: Could not write temp file");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
out.write(reinterpret_cast<const char*>(data.data()), data.size());
|
|
||||||
out.close();
|
|
||||||
|
|
||||||
// Play with ffplay in background
|
|
||||||
std::vector<std::string> args;
|
|
||||||
args.push_back("-nodisp");
|
|
||||||
args.push_back("-autoexit");
|
|
||||||
if (loop) {
|
|
||||||
args.push_back("-loop");
|
|
||||||
args.push_back("0");
|
|
||||||
}
|
|
||||||
args.push_back("-volume");
|
|
||||||
args.push_back(std::to_string(volumePercent));
|
|
||||||
args.push_back(tempFilePath);
|
|
||||||
|
|
||||||
playerPid = platform::spawnProcess(args);
|
|
||||||
if (playerPid != INVALID_PROCESS) {
|
|
||||||
playing = true;
|
playing = true;
|
||||||
currentTrack = mpqPath;
|
currentTrack = mpqPath;
|
||||||
currentTrackIsFile = false;
|
currentTrackIsFile = false;
|
||||||
LOG_INFO("Music: Playing ", mpqPath);
|
LOG_INFO("Music: Playing ", mpqPath);
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR("Music: Failed to spawn ffplay process");
|
LOG_ERROR("Music: Failed to play music via AudioEngine");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,33 +66,46 @@ void MusicManager::playFilePath(const std::string& filePath, bool loop) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
stopCurrentProcess();
|
// Check if AudioEngine is ready
|
||||||
|
if (!AudioEngine::instance().isInitialized()) {
|
||||||
std::vector<std::string> args;
|
LOG_WARNING("Music: AudioEngine not initialized");
|
||||||
args.push_back("-nodisp");
|
return;
|
||||||
args.push_back("-autoexit");
|
|
||||||
if (loop) {
|
|
||||||
args.push_back("-loop");
|
|
||||||
args.push_back("0");
|
|
||||||
}
|
}
|
||||||
args.push_back("-volume");
|
|
||||||
args.push_back(std::to_string(volumePercent));
|
|
||||||
args.push_back(filePath);
|
|
||||||
|
|
||||||
playerPid = platform::spawnProcess(args);
|
// Read file into memory
|
||||||
if (playerPid != INVALID_PROCESS) {
|
std::ifstream file(filePath, std::ios::binary | std::ios::ate);
|
||||||
|
if (!file) {
|
||||||
|
LOG_ERROR("Music: Could not open file: ", filePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::streamsize size = file.tellg();
|
||||||
|
file.seekg(0, std::ios::beg);
|
||||||
|
|
||||||
|
std::vector<uint8_t> data(static_cast<size_t>(size));
|
||||||
|
if (!file.read(reinterpret_cast<char*>(data.data()), size)) {
|
||||||
|
LOG_ERROR("Music: Could not read file: ", filePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop current playback
|
||||||
|
AudioEngine::instance().stopMusic();
|
||||||
|
|
||||||
|
// Play with AudioEngine
|
||||||
|
float volume = volumePercent / 100.0f;
|
||||||
|
if (AudioEngine::instance().playMusic(data, volume, loop)) {
|
||||||
playing = true;
|
playing = true;
|
||||||
currentTrack = filePath;
|
currentTrack = filePath;
|
||||||
currentTrackIsFile = true;
|
currentTrackIsFile = true;
|
||||||
LOG_INFO("Music: Playing file ", filePath);
|
LOG_INFO("Music: Playing file ", filePath);
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR("Music: Failed to spawn ffplay process");
|
LOG_ERROR("Music: Failed to play music via AudioEngine");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MusicManager::stopMusic(float fadeMs) {
|
void MusicManager::stopMusic(float fadeMs) {
|
||||||
(void)fadeMs; // ffplay doesn't support fade easily
|
(void)fadeMs; // Fade not implemented yet
|
||||||
stopCurrentProcess();
|
AudioEngine::instance().stopMusic();
|
||||||
playing = false;
|
playing = false;
|
||||||
currentTrack.clear();
|
currentTrack.clear();
|
||||||
currentTrackIsFile = false;
|
currentTrackIsFile = false;
|
||||||
|
|
@ -119,42 +116,31 @@ void MusicManager::setVolume(int volume) {
|
||||||
if (volume > 100) volume = 100;
|
if (volume > 100) volume = 100;
|
||||||
if (volumePercent == volume) return;
|
if (volumePercent == volume) return;
|
||||||
volumePercent = volume;
|
volumePercent = volume;
|
||||||
if (playing && !currentTrack.empty()) {
|
|
||||||
std::string track = currentTrack;
|
// Update AudioEngine music volume directly (no restart needed!)
|
||||||
bool isFile = currentTrackIsFile;
|
float vol = volumePercent / 100.0f;
|
||||||
stopCurrentProcess();
|
AudioEngine::instance().setMusicVolume(vol);
|
||||||
playing = false;
|
|
||||||
currentTrack.clear();
|
|
||||||
currentTrackIsFile = false;
|
|
||||||
if (isFile) {
|
|
||||||
playFilePath(track, true);
|
|
||||||
} else {
|
|
||||||
playMusic(track, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MusicManager::crossfadeTo(const std::string& mpqPath, float fadeMs) {
|
void MusicManager::crossfadeTo(const std::string& mpqPath, float fadeMs) {
|
||||||
if (mpqPath == currentTrack && playing) return;
|
if (mpqPath == currentTrack && playing) return;
|
||||||
|
|
||||||
// Simple implementation: stop and start (no actual crossfade with subprocess)
|
// Simple implementation: stop and start (no actual crossfade yet)
|
||||||
if (fadeMs > 0 && playing) {
|
if (fadeMs > 0 && playing) {
|
||||||
crossfading = true;
|
crossfading = true;
|
||||||
pendingTrack = mpqPath;
|
pendingTrack = mpqPath;
|
||||||
fadeTimer = 0.0f;
|
fadeTimer = 0.0f;
|
||||||
fadeDuration = fadeMs / 1000.0f;
|
fadeDuration = fadeMs / 1000.0f;
|
||||||
stopCurrentProcess();
|
AudioEngine::instance().stopMusic();
|
||||||
} else {
|
} else {
|
||||||
playMusic(mpqPath);
|
playMusic(mpqPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MusicManager::update(float deltaTime) {
|
void MusicManager::update(float deltaTime) {
|
||||||
// Check if player process is still running
|
// Check if music is still playing
|
||||||
if (playerPid != INVALID_PROCESS) {
|
if (playing && !AudioEngine::instance().isMusicPlaying()) {
|
||||||
if (!platform::isProcessRunning(playerPid)) {
|
playing = false;
|
||||||
playing = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle crossfade
|
// Handle crossfade
|
||||||
|
|
@ -169,12 +155,5 @@ void MusicManager::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MusicManager::stopCurrentProcess() {
|
|
||||||
if (playerPid != INVALID_PROCESS) {
|
|
||||||
platform::killProcess(playerPid);
|
|
||||||
playing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace audio
|
} // namespace audio
|
||||||
} // namespace wowee
|
} // namespace wowee
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
#include "core/logger.hpp"
|
#include "core/logger.hpp"
|
||||||
#include "game/world.hpp"
|
#include "game/world.hpp"
|
||||||
#include "game/zone_manager.hpp"
|
#include "game/zone_manager.hpp"
|
||||||
|
#include "audio/audio_engine.hpp"
|
||||||
#include "audio/music_manager.hpp"
|
#include "audio/music_manager.hpp"
|
||||||
#include "audio/footstep_manager.hpp"
|
#include "audio/footstep_manager.hpp"
|
||||||
#include "audio/activity_sound_manager.hpp"
|
#include "audio/activity_sound_manager.hpp"
|
||||||
|
|
@ -322,6 +323,11 @@ bool Renderer::initialize(core::Window* win) {
|
||||||
zoneManager = std::make_unique<game::ZoneManager>();
|
zoneManager = std::make_unique<game::ZoneManager>();
|
||||||
zoneManager->initialize();
|
zoneManager->initialize();
|
||||||
|
|
||||||
|
// Initialize AudioEngine (singleton)
|
||||||
|
if (!audio::AudioEngine::instance().initialize()) {
|
||||||
|
LOG_WARNING("Failed to initialize AudioEngine - audio will be disabled");
|
||||||
|
}
|
||||||
|
|
||||||
// Create music manager (initialized later with asset manager)
|
// Create music manager (initialized later with asset manager)
|
||||||
musicManager = std::make_unique<audio::MusicManager>();
|
musicManager = std::make_unique<audio::MusicManager>();
|
||||||
footstepManager = std::make_unique<audio::FootstepManager>();
|
footstepManager = std::make_unique<audio::FootstepManager>();
|
||||||
|
|
@ -443,6 +449,10 @@ void Renderer::shutdown() {
|
||||||
activitySoundManager->shutdown();
|
activitySoundManager->shutdown();
|
||||||
activitySoundManager.reset();
|
activitySoundManager.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown AudioEngine singleton
|
||||||
|
audio::AudioEngine::instance().shutdown();
|
||||||
|
|
||||||
if (underwaterOverlayVAO) {
|
if (underwaterOverlayVAO) {
|
||||||
glDeleteVertexArrays(1, &underwaterOverlayVAO);
|
glDeleteVertexArrays(1, &underwaterOverlayVAO);
|
||||||
underwaterOverlayVAO = 0;
|
underwaterOverlayVAO = 0;
|
||||||
|
|
@ -1072,13 +1082,26 @@ audio::FootstepSurface Renderer::resolveFootstepSurface() const {
|
||||||
|
|
||||||
const glm::vec3& p = characterPosition;
|
const glm::vec3& p = characterPosition;
|
||||||
|
|
||||||
|
// Cache footstep surface to avoid expensive queries every step
|
||||||
|
// Only update if moved >1.5 units or timer expired (0.5s)
|
||||||
|
float distSq = glm::dot(p - cachedFootstepPosition, p - cachedFootstepPosition);
|
||||||
|
if (distSq < 2.25f && cachedFootstepUpdateTimer < 0.5f) {
|
||||||
|
return cachedFootstepSurface;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
cachedFootstepPosition = p;
|
||||||
|
cachedFootstepUpdateTimer = 0.0f;
|
||||||
|
|
||||||
if (cameraController->isSwimming()) {
|
if (cameraController->isSwimming()) {
|
||||||
|
cachedFootstepSurface = audio::FootstepSurface::WATER;
|
||||||
return audio::FootstepSurface::WATER;
|
return audio::FootstepSurface::WATER;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (waterRenderer) {
|
if (waterRenderer) {
|
||||||
auto waterH = waterRenderer->getWaterHeightAt(p.x, p.y);
|
auto waterH = waterRenderer->getWaterHeightAt(p.x, p.y);
|
||||||
if (waterH && p.z < (*waterH + 0.25f)) {
|
if (waterH && p.z < (*waterH + 0.25f)) {
|
||||||
|
cachedFootstepSurface = audio::FootstepSurface::WATER;
|
||||||
return audio::FootstepSurface::WATER;
|
return audio::FootstepSurface::WATER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1087,25 +1110,30 @@ audio::FootstepSurface Renderer::resolveFootstepSurface() const {
|
||||||
auto wmoFloor = wmoRenderer->getFloorHeight(p.x, p.y, p.z + 1.5f);
|
auto wmoFloor = wmoRenderer->getFloorHeight(p.x, p.y, p.z + 1.5f);
|
||||||
auto terrainFloor = terrainManager ? terrainManager->getHeightAt(p.x, p.y) : std::nullopt;
|
auto terrainFloor = terrainManager ? terrainManager->getHeightAt(p.x, p.y) : std::nullopt;
|
||||||
if (wmoFloor && (!terrainFloor || *wmoFloor >= *terrainFloor - 0.1f)) {
|
if (wmoFloor && (!terrainFloor || *wmoFloor >= *terrainFloor - 0.1f)) {
|
||||||
|
cachedFootstepSurface = audio::FootstepSurface::STONE;
|
||||||
return audio::FootstepSurface::STONE;
|
return audio::FootstepSurface::STONE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine surface type (expensive - only done when cache needs update)
|
||||||
|
audio::FootstepSurface surface = audio::FootstepSurface::STONE;
|
||||||
|
|
||||||
if (terrainManager) {
|
if (terrainManager) {
|
||||||
auto texture = terrainManager->getDominantTextureAt(p.x, p.y);
|
auto texture = terrainManager->getDominantTextureAt(p.x, p.y);
|
||||||
if (texture) {
|
if (texture) {
|
||||||
std::string t = *texture;
|
std::string t = *texture;
|
||||||
for (char& c : t) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
for (char& c : t) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||||
if (t.find("snow") != std::string::npos || t.find("ice") != std::string::npos) return audio::FootstepSurface::SNOW;
|
if (t.find("snow") != std::string::npos || t.find("ice") != std::string::npos) surface = audio::FootstepSurface::SNOW;
|
||||||
if (t.find("grass") != std::string::npos || t.find("moss") != std::string::npos || t.find("leaf") != std::string::npos) return audio::FootstepSurface::GRASS;
|
else if (t.find("grass") != std::string::npos || t.find("moss") != std::string::npos || t.find("leaf") != std::string::npos) surface = audio::FootstepSurface::GRASS;
|
||||||
if (t.find("sand") != std::string::npos || t.find("dirt") != std::string::npos || t.find("mud") != std::string::npos) return audio::FootstepSurface::DIRT;
|
else if (t.find("sand") != std::string::npos || t.find("dirt") != std::string::npos || t.find("mud") != std::string::npos) surface = audio::FootstepSurface::DIRT;
|
||||||
if (t.find("wood") != std::string::npos || t.find("timber") != std::string::npos) return audio::FootstepSurface::WOOD;
|
else if (t.find("wood") != std::string::npos || t.find("timber") != std::string::npos) surface = audio::FootstepSurface::WOOD;
|
||||||
if (t.find("metal") != std::string::npos || t.find("iron") != std::string::npos) return audio::FootstepSurface::METAL;
|
else if (t.find("metal") != std::string::npos || t.find("iron") != std::string::npos) surface = audio::FootstepSurface::METAL;
|
||||||
if (t.find("stone") != std::string::npos || t.find("rock") != std::string::npos || t.find("cobble") != std::string::npos || t.find("brick") != std::string::npos) return audio::FootstepSurface::STONE;
|
else if (t.find("stone") != std::string::npos || t.find("rock") != std::string::npos || t.find("cobble") != std::string::npos || t.find("brick") != std::string::npos) surface = audio::FootstepSurface::STONE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return audio::FootstepSurface::STONE;
|
cachedFootstepSurface = surface;
|
||||||
|
return surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::update(float deltaTime) {
|
void Renderer::update(float deltaTime) {
|
||||||
|
|
@ -1118,6 +1146,12 @@ void Renderer::update(float deltaTime) {
|
||||||
cameraController->update(deltaTime);
|
cameraController->update(deltaTime);
|
||||||
auto cameraEnd = std::chrono::steady_clock::now();
|
auto cameraEnd = std::chrono::steady_clock::now();
|
||||||
lastCameraUpdateMs = std::chrono::duration<double, std::milli>(cameraEnd - cameraStart).count();
|
lastCameraUpdateMs = std::chrono::duration<double, std::milli>(cameraEnd - cameraStart).count();
|
||||||
|
|
||||||
|
// Update 3D audio listener position/orientation to match camera
|
||||||
|
if (camera) {
|
||||||
|
audio::AudioEngine::instance().setListenerPosition(camera->getPosition());
|
||||||
|
audio::AudioEngine::instance().setListenerOrientation(camera->getForward(), camera->getUp());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
lastCameraUpdateMs = 0.0;
|
lastCameraUpdateMs = 0.0;
|
||||||
}
|
}
|
||||||
|
|
@ -1209,9 +1243,13 @@ void Renderer::update(float deltaTime) {
|
||||||
characterRenderer->update(deltaTime);
|
characterRenderer->update(deltaTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update AudioEngine (cleanup finished sounds, etc.)
|
||||||
|
audio::AudioEngine::instance().update(deltaTime);
|
||||||
|
|
||||||
// Footsteps: animation-event driven + surface query at event time.
|
// Footsteps: animation-event driven + surface query at event time.
|
||||||
if (footstepManager) {
|
if (footstepManager) {
|
||||||
footstepManager->update(deltaTime);
|
footstepManager->update(deltaTime);
|
||||||
|
cachedFootstepUpdateTimer += deltaTime; // Update surface cache timer
|
||||||
bool canPlayFootsteps = characterRenderer && characterInstanceId > 0 &&
|
bool canPlayFootsteps = characterRenderer && characterInstanceId > 0 &&
|
||||||
cameraController && cameraController->isThirdPerson() &&
|
cameraController && cameraController->isThirdPerson() &&
|
||||||
cameraController->isGrounded() && !cameraController->isSwimming();
|
cameraController->isGrounded() && !cameraController->isSwimming();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue