Browse Source

Add QML integration and UI controls for audio settings

Co-authored-by: djeada <[email protected]>
copilot-swe-agent[bot] 1 month ago
parent
commit
b79e433a85

+ 2 - 0
CMakeLists.txt

@@ -71,6 +71,7 @@ if(QT_VERSION_MAJOR EQUAL 6)
         main.cpp
         main.cpp
         app/core/game_engine.cpp
         app/core/game_engine.cpp
         app/core/language_manager.cpp
         app/core/language_manager.cpp
+        app/models/audio_system_proxy.cpp
         app/models/cursor_manager.cpp
         app/models/cursor_manager.cpp
         app/models/hover_tracker.cpp
         app/models/hover_tracker.cpp
         app/models/selected_units_model.cpp
         app/models/selected_units_model.cpp
@@ -85,6 +86,7 @@ else()
         main.cpp
         main.cpp
         app/core/game_engine.cpp
         app/core/game_engine.cpp
         app/core/language_manager.cpp
         app/core/language_manager.cpp
+        app/models/audio_system_proxy.cpp
         app/models/cursor_manager.cpp
         app/models/cursor_manager.cpp
         app/models/hover_tracker.cpp
         app/models/hover_tracker.cpp
         app/models/selected_units_model.cpp
         app/models/selected_units_model.cpp

+ 9 - 3
app/core/game_engine.cpp

@@ -2,6 +2,7 @@
 
 
 #include "../controllers/action_vfx.h"
 #include "../controllers/action_vfx.h"
 #include "../controllers/command_controller.h"
 #include "../controllers/command_controller.h"
+#include "../models/audio_system_proxy.h"
 #include "../models/cursor_manager.h"
 #include "../models/cursor_manager.h"
 #include "../models/hover_tracker.h"
 #include "../models/hover_tracker.h"
 #include "../utils/json_vec_utils.h"
 #include "../utils/json_vec_utils.h"
@@ -163,6 +164,8 @@ GameEngine::GameEngine() {
     qWarning() << "Failed to initialize AudioSystem";
     qWarning() << "Failed to initialize AudioSystem";
   }
   }
 
 
+  m_audioSystemProxy = std::make_unique<App::Models::AudioSystemProxy>(this);
+
   m_audioEventHandler =
   m_audioEventHandler =
       std::make_unique<Game::Audio::AudioEventHandler>(m_world.get());
       std::make_unique<Game::Audio::AudioEventHandler>(m_world.get());
   if (m_audioEventHandler->initialize()) {
   if (m_audioEventHandler->initialize()) {
@@ -1651,7 +1654,8 @@ void GameEngine::loadAudioResources() {
 
 
   if (audioSys.loadSound(
   if (audioSys.loadSound(
           "archer_voice",
           "archer_voice",
-          (basePath + "voices/archer_voice.wav").toStdString())) {
+          (basePath + "voices/archer_voice.wav").toStdString(),
+          AudioCategory::VOICE)) {
     qInfo() << "Loaded archer voice";
     qInfo() << "Loaded archer voice";
   } else {
   } else {
     qWarning() << "Failed to load archer voice";
     qWarning() << "Failed to load archer voice";
@@ -1659,7 +1663,8 @@ void GameEngine::loadAudioResources() {
 
 
   if (audioSys.loadSound(
   if (audioSys.loadSound(
           "knight_voice",
           "knight_voice",
-          (basePath + "voices/knight_voice.wav").toStdString())) {
+          (basePath + "voices/knight_voice.wav").toStdString(),
+          AudioCategory::VOICE)) {
     qInfo() << "Loaded knight voice";
     qInfo() << "Loaded knight voice";
   } else {
   } else {
     qWarning() << "Failed to load knight voice";
     qWarning() << "Failed to load knight voice";
@@ -1667,7 +1672,8 @@ void GameEngine::loadAudioResources() {
 
 
   if (audioSys.loadSound(
   if (audioSys.loadSound(
           "spearman_voice",
           "spearman_voice",
-          (basePath + "voices/spearman_voice.wav").toStdString())) {
+          (basePath + "voices/spearman_voice.wav").toStdString(),
+          AudioCategory::VOICE)) {
     qInfo() << "Loaded spearman voice";
     qInfo() << "Loaded spearman voice";
   } else {
   } else {
     qWarning() << "Failed to load spearman voice";
     qWarning() << "Failed to load spearman voice";

+ 7 - 0
app/core/game_engine.h

@@ -70,6 +70,9 @@ namespace App {
 namespace Controllers {
 namespace Controllers {
 class CommandController;
 class CommandController;
 }
 }
+namespace Models {
+class AudioSystemProxy;
+}
 } // namespace App
 } // namespace App
 
 
 class QQuickWindow;
 class QQuickWindow;
@@ -104,6 +107,7 @@ public:
   Q_PROPERTY(int selectedPlayerId READ selectedPlayerId WRITE
   Q_PROPERTY(int selectedPlayerId READ selectedPlayerId WRITE
                  setSelectedPlayerId NOTIFY selectedPlayerIdChanged)
                  setSelectedPlayerId NOTIFY selectedPlayerIdChanged)
   Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
   Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
+  Q_PROPERTY(QObject *audioSystem READ audioSystem CONSTANT)
 
 
   Q_INVOKABLE void onMapClicked(qreal sx, qreal sy);
   Q_INVOKABLE void onMapClicked(qreal sx, qreal sy);
   Q_INVOKABLE void onRightClick(qreal sx, qreal sy);
   Q_INVOKABLE void onRightClick(qreal sx, qreal sy);
@@ -185,6 +189,8 @@ public:
   Q_INVOKABLE void exitGame();
   Q_INVOKABLE void exitGame();
   Q_INVOKABLE QVariantList getOwnerInfo() const;
   Q_INVOKABLE QVariantList getOwnerInfo() const;
 
 
+  QObject *audioSystem() { return m_audioSystemProxy.get(); }
+
   void setWindow(QQuickWindow *w) { m_window = w; }
   void setWindow(QQuickWindow *w) { m_window = w; }
 
 
   void ensureInitialized();
   void ensureInitialized();
@@ -278,6 +284,7 @@ private:
   std::unique_ptr<App::Controllers::CommandController> m_commandController;
   std::unique_ptr<App::Controllers::CommandController> m_commandController;
   std::unique_ptr<Game::Map::MapCatalog> m_mapCatalog;
   std::unique_ptr<Game::Map::MapCatalog> m_mapCatalog;
   std::unique_ptr<Game::Audio::AudioEventHandler> m_audioEventHandler;
   std::unique_ptr<Game::Audio::AudioEventHandler> m_audioEventHandler;
+  std::unique_ptr<App::Models::AudioSystemProxy> m_audioSystemProxy;
   QQuickWindow *m_window = nullptr;
   QQuickWindow *m_window = nullptr;
   RuntimeState m_runtime;
   RuntimeState m_runtime;
   ViewportState m_viewport;
   ViewportState m_viewport;

+ 42 - 0
app/models/audio_system_proxy.cpp

@@ -0,0 +1,42 @@
+#include "audio_system_proxy.h"
+#include "game/audio/AudioSystem.h"
+
+namespace App {
+namespace Models {
+
+AudioSystemProxy::AudioSystemProxy(QObject *parent) : QObject(parent) {}
+
+void AudioSystemProxy::setMasterVolume(float volume) {
+  AudioSystem::getInstance().setMasterVolume(volume);
+}
+
+void AudioSystemProxy::setMusicVolume(float volume) {
+  AudioSystem::getInstance().setMusicVolume(volume);
+}
+
+void AudioSystemProxy::setSoundVolume(float volume) {
+  AudioSystem::getInstance().setSoundVolume(volume);
+}
+
+void AudioSystemProxy::setVoiceVolume(float volume) {
+  AudioSystem::getInstance().setVoiceVolume(volume);
+}
+
+float AudioSystemProxy::getMasterVolume() const {
+  return AudioSystem::getInstance().getMasterVolume();
+}
+
+float AudioSystemProxy::getMusicVolume() const {
+  return AudioSystem::getInstance().getMusicVolume();
+}
+
+float AudioSystemProxy::getSoundVolume() const {
+  return AudioSystem::getInstance().getSoundVolume();
+}
+
+float AudioSystemProxy::getVoiceVolume() const {
+  return AudioSystem::getInstance().getVoiceVolume();
+}
+
+} // namespace Models
+} // namespace App

+ 26 - 0
app/models/audio_system_proxy.h

@@ -0,0 +1,26 @@
+#pragma once
+
+#include <QObject>
+
+namespace App {
+namespace Models {
+
+class AudioSystemProxy : public QObject {
+  Q_OBJECT
+public:
+  explicit AudioSystemProxy(QObject *parent = nullptr);
+  ~AudioSystemProxy() override = default;
+
+  Q_INVOKABLE void setMasterVolume(float volume);
+  Q_INVOKABLE void setMusicVolume(float volume);
+  Q_INVOKABLE void setSoundVolume(float volume);
+  Q_INVOKABLE void setVoiceVolume(float volume);
+
+  Q_INVOKABLE float getMasterVolume() const;
+  Q_INVOKABLE float getMusicVolume() const;
+  Q_INVOKABLE float getSoundVolume() const;
+  Q_INVOKABLE float getVoiceVolume() const;
+};
+
+} // namespace Models
+} // namespace App

+ 147 - 0
game/audio/TESTING.md

@@ -0,0 +1,147 @@
+# Audio System Testing Guide
+
+This document outlines the testing procedures for the enhanced audio system.
+
+## Manual Testing Checklist
+
+### Volume Controls
+- [ ] **Master Volume**: Adjust master volume slider in settings - all audio should be affected
+- [ ] **Music Volume**: Adjust music volume slider - only background music should be affected
+- [ ] **SFX Volume**: Adjust SFX volume slider - only sound effects should be affected
+- [ ] **Voice Volume**: Adjust voice volume slider - only unit voices should be affected
+- [ ] **Volume Persistence**: All volume settings should persist between game sessions
+
+### Channel Management
+- [ ] **Max Channels**: Play multiple sounds simultaneously (up to 32 default)
+- [ ] **Priority Eviction**: When channels are full, lower priority sounds should be stopped for higher priority ones
+- [ ] **Channel Tracking**: Active channel count should be accurate
+
+### Resource Management
+- [ ] **Load Sound**: Load sounds with different categories (SFX, VOICE)
+- [ ] **Load Music**: Load background music tracks
+- [ ] **Unload Sound**: Unload specific sound - should free memory and not crash
+- [ ] **Unload Music**: Unload specific music track - should free memory and not crash
+- [ ] **Unload All Sounds**: Bulk unload all sounds - should complete without errors
+- [ ] **Unload All Music**: Bulk unload all music - should complete without errors
+- [ ] **Reload Resources**: Load previously unloaded resources - should work correctly
+
+### Thread Safety & Stability
+- [ ] **Concurrent Playback**: Play multiple sounds from different threads simultaneously
+- [ ] **Shutdown**: Application should close cleanly without segfaults
+- [ ] **Rapid Volume Changes**: Rapidly adjust volume sliders - no crashes or audio glitches
+- [ ] **Resource Loading While Playing**: Load/unload resources while audio is playing
+- [ ] **Stop During Playback**: Stop sounds/music while they're playing - no crashes
+
+### Memory Safety
+- [ ] **Memory Leaks**: Run valgrind or similar tools to check for memory leaks
+- [ ] **Null Checks**: All QCoreApplication::instance() calls should be null-checked
+- [ ] **Thread Cleanup**: Audio thread should be properly joined on shutdown
+- [ ] **Resource Cleanup**: All unique_ptrs should be properly released
+
+### Performance
+- [ ] **No Audio Stutters**: Audio should play smoothly without interruptions
+- [ ] **Low Latency**: Sound effects should play immediately when triggered
+- [ ] **CPU Usage**: Audio system should not consume excessive CPU
+- [ ] **Memory Usage**: Memory usage should be reasonable and not grow over time
+
+## Automated Testing (Future)
+
+### Unit Tests
+```cpp
+// Test volume controls
+TEST(AudioSystem, VolumeControls) {
+    auto& audio = AudioSystem::getInstance();
+    audio.setMasterVolume(0.5f);
+    EXPECT_FLOAT_EQ(audio.getMasterVolume(), 0.5f);
+    
+    audio.setSoundVolume(0.7f);
+    EXPECT_FLOAT_EQ(audio.getSoundVolume(), 0.7f);
+    
+    audio.setMusicVolume(0.6f);
+    EXPECT_FLOAT_EQ(audio.getMusicVolume(), 0.6f);
+    
+    audio.setVoiceVolume(0.8f);
+    EXPECT_FLOAT_EQ(audio.getVoiceVolume(), 0.8f);
+}
+
+// Test channel management
+TEST(AudioSystem, ChannelManagement) {
+    auto& audio = AudioSystem::getInstance();
+    audio.setMaxChannels(5);
+    
+    // Load test sounds
+    for (int i = 0; i < 10; i++) {
+        audio.loadSound("sound" + std::to_string(i), "test.wav");
+    }
+    
+    // Play more sounds than max channels
+    for (int i = 0; i < 10; i++) {
+        audio.playSound("sound" + std::to_string(i), 1.0f, false, i);
+    }
+    
+    // Should not exceed max channels
+    EXPECT_LE(audio.getActiveChannelCount(), 5);
+}
+
+// Test resource management
+TEST(AudioSystem, ResourceManagement) {
+    auto& audio = AudioSystem::getInstance();
+    
+    EXPECT_TRUE(audio.loadSound("test_sfx", "test.wav", AudioCategory::SFX));
+    EXPECT_TRUE(audio.loadSound("test_voice", "test.wav", AudioCategory::VOICE));
+    EXPECT_TRUE(audio.loadMusic("test_music", "test.wav"));
+    
+    audio.unloadSound("test_sfx");
+    audio.unloadSound("test_voice");
+    audio.unloadMusic("test_music");
+    
+    // Resources should be properly freed
+}
+
+// Test thread safety
+TEST(AudioSystem, ThreadSafety) {
+    auto& audio = AudioSystem::getInstance();
+    
+    std::vector<std::thread> threads;
+    for (int i = 0; i < 10; i++) {
+        threads.emplace_back([&audio, i]() {
+            audio.playSound("sound" + std::to_string(i), 1.0f);
+            audio.setMasterVolume(0.5f);
+        });
+    }
+    
+    for (auto& thread : threads) {
+        thread.join();
+    }
+    
+    // Should complete without crashes
+}
+```
+
+## Performance Benchmarks
+
+### Target Metrics
+- **Audio Latency**: < 50ms from playSound() to actual sound playback
+- **Channel Switch Time**: < 10ms to evict and play new sound
+- **Memory Overhead**: < 100MB for 100 loaded sounds
+- **CPU Usage**: < 5% CPU when playing 32 simultaneous sounds
+
+### Stress Tests
+- Load 1000 sounds and verify memory usage
+- Play 100 sounds simultaneously for 1 hour
+- Rapidly load/unload resources for 10 minutes
+- Adjust volumes 1000 times per second for 1 minute
+
+## Known Issues
+
+### Current Limitations
+- Maximum 32 simultaneous sounds (configurable)
+- No 3D positional audio yet
+- No audio occlusion
+- Crossfade between music tracks uses simple stop/play
+
+### Future Improvements
+- Add fade-in/fade-out effects
+- Implement 3D audio with HRTF
+- Add audio compression for large files
+- Implement voice line queuing system