diff --git a/include/audio/activity_sound_manager.hpp b/include/audio/activity_sound_manager.hpp index 8de9963a..360f34fd 100644 --- a/include/audio/activity_sound_manager.hpp +++ b/include/audio/activity_sound_manager.hpp @@ -30,6 +30,8 @@ public: void playWaterEnter(); void playWaterExit(); void playMeleeSwing(); + void setVolumeScale(float scale) { volumeScale = scale; } + float getVolumeScale() const { return volumeScale; } private: struct Sample { @@ -66,6 +68,7 @@ private: std::chrono::steady_clock::time_point lastMeleeSwingAt{}; bool meleeSwingWarned = false; std::string voiceProfileKey; + float volumeScale = 1.0f; void preloadCandidates(std::vector& out, const std::vector& candidates); void preloadLandingSet(FootstepSurface surface, const std::string& material); diff --git a/include/audio/footstep_manager.hpp b/include/audio/footstep_manager.hpp index e051b201..4365be5b 100644 --- a/include/audio/footstep_manager.hpp +++ b/include/audio/footstep_manager.hpp @@ -32,6 +32,8 @@ public: void update(float deltaTime); void playFootstep(FootstepSurface surface, bool sprinting); + void setVolumeScale(float scale) { volumeScale = scale; } + float getVolumeScale() const { return volumeScale; } bool isInitialized() const { return assetManager != nullptr; } bool hasAnySamples() const { return sampleCount > 0; } @@ -61,6 +63,7 @@ private: std::chrono::steady_clock::time_point lastPlayTime = std::chrono::steady_clock::time_point{}; std::mt19937 rng; + float volumeScale = 1.0f; }; } // namespace audio diff --git a/include/audio/music_manager.hpp b/include/audio/music_manager.hpp index 5305f208..2806c599 100644 --- a/include/audio/music_manager.hpp +++ b/include/audio/music_manager.hpp @@ -22,6 +22,8 @@ public: void stopMusic(float fadeMs = 2000.0f); void crossfadeTo(const std::string& mpqPath, float fadeMs = 3000.0f); void update(float deltaTime); + void setVolume(int volume); + int getVolume() const { return volumePercent; } bool isPlaying() const { return playing; } bool isInitialized() const { return assetManager != nullptr; } @@ -32,9 +34,11 @@ private: pipeline::AssetManager* assetManager = nullptr; std::string currentTrack; + bool currentTrackIsFile = false; std::string tempFilePath; ProcessHandle playerPid = INVALID_PROCESS; bool playing = false; + int volumePercent = 30; // Crossfade state bool crossfading = false; diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 169928ef..dd513a0a 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -131,6 +131,8 @@ public: double getLastWMORenderMs() const { return lastWMORenderMs; } double getLastM2RenderMs() const { return lastM2RenderMs; } audio::MusicManager* getMusicManager() { return musicManager.get(); } + audio::FootstepManager* getFootstepManager() { return footstepManager.get(); } + audio::ActivitySoundManager* getActivitySoundManager() { return activitySoundManager.get(); } private: core::Window* window = nullptr; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 9a754a92..a9030238 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -61,6 +61,8 @@ private: bool pendingVsync = false; int pendingResIndex = 0; bool pendingShadows = true; + int pendingMusicVolume = 30; + int pendingSfxVolume = 100; /** * Render player info window diff --git a/src/audio/activity_sound_manager.cpp b/src/audio/activity_sound_manager.cpp index 1560652b..6a44fd31 100644 --- a/src/audio/activity_sound_manager.cpp +++ b/src/audio/activity_sound_manager.cpp @@ -218,6 +218,7 @@ bool ActivitySoundManager::playOneShot(const std::vector& clips, float v std::uniform_real_distribution pitchDist(pitchLo, pitchHi); float pitch = pitchDist(rng); + volume *= volumeScale; if (volume < 0.1f) volume = 0.1f; if (volume > 1.2f) volume = 1.2f; std::string filter = "asetrate=44100*" + std::to_string(pitch) + @@ -241,7 +242,7 @@ void ActivitySoundManager::startSwimLoop() { out.write(reinterpret_cast(sample.data.data()), static_cast(sample.data.size())); out.close(); - float volume = swimMoving ? 0.85f : 0.65f; + float volume = (swimMoving ? 0.85f : 0.65f) * volumeScale; std::string filter = "volume=" + std::to_string(volume); swimLoopPid = platform::spawnProcess({ diff --git a/src/audio/footstep_manager.cpp b/src/audio/footstep_manager.cpp index 0849f4ce..38c48ff3 100644 --- a/src/audio/footstep_manager.cpp +++ b/src/audio/footstep_manager.cpp @@ -159,7 +159,7 @@ bool FootstepManager::playRandomStep(FootstepSurface surface, bool sprinting) { std::uniform_real_distribution pitchDist(0.97f, 1.05f); std::uniform_real_distribution volumeDist(0.92f, 1.00f); float pitch = pitchDist(rng); - float volume = volumeDist(rng) * (sprinting ? 1.0f : 0.88f); + float volume = volumeDist(rng) * (sprinting ? 1.0f : 0.88f) * volumeScale; if (volume > 1.0f) volume = 1.0f; if (volume < 0.1f) volume = 0.1f; diff --git a/src/audio/music_manager.cpp b/src/audio/music_manager.cpp index 42554087..10c9bd6f 100644 --- a/src/audio/music_manager.cpp +++ b/src/audio/music_manager.cpp @@ -60,13 +60,14 @@ void MusicManager::playMusic(const std::string& mpqPath, bool loop) { args.push_back("0"); } args.push_back("-volume"); - args.push_back("30"); + args.push_back(std::to_string(volumePercent)); args.push_back(tempFilePath); playerPid = platform::spawnProcess(args); if (playerPid != INVALID_PROCESS) { playing = true; currentTrack = mpqPath; + currentTrackIsFile = false; LOG_INFO("Music: Playing ", mpqPath); } else { LOG_ERROR("Music: Failed to spawn ffplay process"); @@ -91,13 +92,14 @@ void MusicManager::playFilePath(const std::string& filePath, bool loop) { args.push_back("0"); } args.push_back("-volume"); - args.push_back("30"); + args.push_back(std::to_string(volumePercent)); args.push_back(filePath); playerPid = platform::spawnProcess(args); if (playerPid != INVALID_PROCESS) { playing = true; currentTrack = filePath; + currentTrackIsFile = true; LOG_INFO("Music: Playing file ", filePath); } else { LOG_ERROR("Music: Failed to spawn ffplay process"); @@ -109,6 +111,27 @@ void MusicManager::stopMusic(float fadeMs) { stopCurrentProcess(); playing = false; currentTrack.clear(); + currentTrackIsFile = false; +} + +void MusicManager::setVolume(int volume) { + if (volume < 0) volume = 0; + if (volume > 100) volume = 100; + if (volumePercent == volume) return; + volumePercent = volume; + if (playing && !currentTrack.empty()) { + std::string track = currentTrack; + bool isFile = currentTrackIsFile; + stopCurrentProcess(); + playing = false; + currentTrack.clear(); + currentTrackIsFile = false; + if (isFile) { + playFilePath(track, true); + } else { + playMusic(track, true); + } + } } void MusicManager::crossfadeTo(const std::string& mpqPath, float fadeMs) { diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 6e710a3d..e5e08902 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -1807,6 +1807,22 @@ void GameScreen::renderSettingsWindow() { pendingFullscreen = window->isFullscreen(); pendingVsync = window->isVsyncEnabled(); pendingShadows = renderer ? renderer->areShadowsEnabled() : true; + if (renderer) { + if (auto* music = renderer->getMusicManager()) { + pendingMusicVolume = music->getVolume(); + } + if (auto* footstep = renderer->getFootstepManager()) { + float scale = footstep->getVolumeScale(); + pendingSfxVolume = static_cast(scale * 100.0f + 0.5f); + if (pendingSfxVolume < 0) pendingSfxVolume = 0; + if (pendingSfxVolume > 100) pendingSfxVolume = 100; + } else if (auto* activity = renderer->getActivitySoundManager()) { + float scale = activity->getVolumeScale(); + pendingSfxVolume = static_cast(scale * 100.0f + 0.5f); + if (pendingSfxVolume < 0) pendingSfxVolume = 0; + if (pendingSfxVolume > 100) pendingSfxVolume = 100; + } + } pendingResIndex = 0; int curW = window->getWidth(); int curH = window->getHeight(); @@ -1822,7 +1838,7 @@ void GameScreen::renderSettingsWindow() { ImGuiIO& io = ImGui::GetIO(); float screenW = io.DisplaySize.x; float screenH = io.DisplaySize.y; - ImVec2 size(360.0f, 240.0f); + ImVec2 size(380.0f, 320.0f); ImVec2 pos((screenW - size.x) * 0.5f, (screenH - size.y) * 0.5f); ImGui::SetNextWindowPos(pos, ImGuiCond_Always); @@ -1848,6 +1864,16 @@ void GameScreen::renderSettingsWindow() { } ImGui::Combo(resLabel, &pendingResIndex, resItems, kResCount); + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + ImGui::Text("Audio"); + ImGui::SliderInt("Music Volume", &pendingMusicVolume, 0, 100, "%d"); + ImGui::SliderInt("SFX Volume", &pendingSfxVolume, 0, 100, "%d"); + + ImGui::Spacing(); + ImGui::Separator(); ImGui::Spacing(); if (ImGui::Button("Apply", ImVec2(-1, 0))) { window->setVsync(pendingVsync); @@ -1855,6 +1881,16 @@ void GameScreen::renderSettingsWindow() { window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]); if (renderer) { renderer->setShadowsEnabled(pendingShadows); + if (auto* music = renderer->getMusicManager()) { + music->setVolume(pendingMusicVolume); + } + float sfxScale = static_cast(pendingSfxVolume) / 100.0f; + if (auto* footstep = renderer->getFootstepManager()) { + footstep->setVolumeScale(sfxScale); + } + if (auto* activity = renderer->getActivitySoundManager()) { + activity->setVolumeScale(sfxScale); + } } } ImGui::Spacing();