Audio update

This commit is contained in:
2025-12-25 19:17:36 +01:00
parent 938988c876
commit 68b35ea57b
15 changed files with 122 additions and 62 deletions

View File

@ -57,6 +57,7 @@ set(TETRIS_SOURCES
src/graphics/renderers/SyncLineRenderer.cpp src/graphics/renderers/SyncLineRenderer.cpp
src/graphics/renderers/UIRenderer.cpp src/graphics/renderers/UIRenderer.cpp
src/audio/Audio.cpp src/audio/Audio.cpp
src/audio/AudioManager.cpp
src/renderer/SDLRenderer.cpp src/renderer/SDLRenderer.cpp
src/gameplay/effects/LineEffect.cpp src/gameplay/effects/LineEffect.cpp
src/audio/SoundEffect.cpp src/audio/SoundEffect.cpp

View File

@ -118,6 +118,7 @@ static bool decodeMP3(const std::string& path, std::vector<int16_t>& outPCM, int
outCh = static_cast<int>(clientFormat.mChannelsPerFrame); outCh = static_cast<int>(clientFormat.mChannelsPerFrame);
return !outPCM.empty(); return !outPCM.empty();
} }
#else #else
static bool decodeMP3(const std::string& path, std::vector<int16_t>& outPCM, int& outRate, int& outCh){ static bool decodeMP3(const std::string& path, std::vector<int16_t>& outPCM, int& outRate, int& outCh){
(void)outPCM; (void)outRate; (void)outCh; (void)path; (void)outPCM; (void)outRate; (void)outCh; (void)path;
@ -184,6 +185,8 @@ void Audio::skipToNextTrack(){
void Audio::toggleMute(){ muted=!muted; } void Audio::toggleMute(){ muted=!muted; }
void Audio::setMuted(bool m){ muted=m; } void Audio::setMuted(bool m){ muted=m; }
bool Audio::isMuted() const { return muted; }
void Audio::nextTrack(){ void Audio::nextTrack(){
if(tracks.empty()) { current = -1; return; } if(tracks.empty()) { current = -1; return; }
// Try every track once to find a decodable one // Try every track once to find a decodable one

View File

@ -32,29 +32,27 @@ public:
void setSoundVolume(float volume) override; void setSoundVolume(float volume) override;
bool isMusicPlaying() const override; bool isMusicPlaying() const override;
// Existing Audio class methods // Additional IAudioSystem methods (forwarded to concrete implementation)
bool init(); // initialize backend (MF on Windows) bool init() override;
void addTrack(const std::string& path); // decode MP3 -> PCM16 stereo 44100 void shutdown() override;
void addTrackAsync(const std::string& path); // add track for background loading void addTrack(const std::string& path) override;
void startBackgroundLoading(); // start background thread for loading void addTrackAsync(const std::string& path) override;
void waitForLoadingComplete(); // wait for all tracks to finish loading void startBackgroundLoading() override;
bool isLoadingComplete() const; // check if background loading is done bool isLoadingComplete() const override;
int getLoadedTrackCount() const; // get number of tracks loaded so far int getLoadedTrackCount() const override;
void shuffle(); // randomize order void start() override;
void start(); // begin playback void skipToNextTrack() override;
void skipToNextTrack(); // advance to the next music track void shuffle() override;
void toggleMute(); void toggleMute() override;
bool isMuted() const override;
void setMuted(bool m); void setMuted(bool m);
bool isMuted() const { return muted; } void setMenuTrack(const std::string& path) override;
void playMenuMusic() override;
// Menu music support void playGameMusic() override;
void setMenuTrack(const std::string& path); void playSfx(const std::vector<int16_t>& pcm, int channels, int rate, float volume) override;
void playMenuMusic();
void playGameMusic(); // Existing Audio class helper methods
void waitForLoadingComplete(); // wait for all tracks to finish loading
// Queue a sound effect to mix over the music (pcm can be mono/stereo, any rate; will be converted)
void playSfx(const std::vector<int16_t>& pcm, int channels, int rate, float volume);
void shutdown();
private: private:
Audio()=default; ~Audio()=default; Audio(const Audio&)=delete; Audio& operator=(const Audio&)=delete; Audio()=default; ~Audio()=default; Audio(const Audio&)=delete; Audio& operator=(const Audio&)=delete;
static void SDLCALL streamCallback(void* userdata, SDL_AudioStream* stream, int additional, int total); static void SDLCALL streamCallback(void* userdata, SDL_AudioStream* stream, int additional, int total);

View File

@ -0,0 +1,15 @@
#include "AudioManager.h"
#include "Audio.h"
static IAudioSystem* g_audioSystem = nullptr;
IAudioSystem* AudioManager::get() {
if (!g_audioSystem) {
g_audioSystem = &Audio::instance();
}
return g_audioSystem;
}
void AudioManager::set(IAudioSystem* sys) {
g_audioSystem = sys;
}

11
src/audio/AudioManager.h Normal file
View File

@ -0,0 +1,11 @@
#pragma once
#include "../core/interfaces/IAudioSystem.h"
class AudioManager {
public:
// Get the currently registered audio system (may return Audio::instance())
static IAudioSystem* get();
// Replace the audio system (for tests or different backends)
static void set(IAudioSystem* sys);
};

View File

@ -2,6 +2,7 @@
#include "SoundEffect.h" #include "SoundEffect.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include "audio/Audio.h" #include "audio/Audio.h"
#include "audio/AudioManager.h"
#include <cstdio> #include <cstdio>
#include <algorithm> #include <algorithm>
#include <random> #include <random>
@ -93,7 +94,9 @@ void SimpleAudioPlayer::playSound(const std::vector<int16_t>& pcmData, int chann
return; return;
} }
// Route through shared Audio mixer so SFX always play over music // Route through shared Audio mixer so SFX always play over music
Audio::instance().playSfx(pcmData, channels, sampleRate, volume); if (auto sys = AudioManager::get()) {
sys->playSfx(pcmData, channels, sampleRate, volume);
}
} }
bool SoundEffect::loadWAV(const std::string& filePath) { bool SoundEffect::loadWAV(const std::string& filePath) {

View File

@ -7,6 +7,7 @@
#include "../interfaces/IInputHandler.h" #include "../interfaces/IInputHandler.h"
#include <filesystem> #include <filesystem>
#include "../../audio/Audio.h" #include "../../audio/Audio.h"
#include "../../audio/AudioManager.h"
#include "../../audio/SoundEffect.h" #include "../../audio/SoundEffect.h"
#include "../../persistence/Scores.h" #include "../../persistence/Scores.h"
#include "../../states/State.h" #include "../../states/State.h"
@ -267,7 +268,7 @@ void ApplicationManager::shutdown() {
m_running = false; m_running = false;
// Stop audio systems before tearing down SDL to avoid aborts/asserts // Stop audio systems before tearing down SDL to avoid aborts/asserts
Audio::instance().shutdown(); if (auto sys = ::AudioManager::get()) sys->shutdown();
SoundEffectManager::instance().shutdown(); SoundEffectManager::instance().shutdown();
// Cleanup in reverse order of initialization // Cleanup in reverse order of initialization
@ -381,11 +382,11 @@ bool ApplicationManager::initializeManagers() {
// M: Toggle/mute music; start playback if unmuting and not started yet // M: Toggle/mute music; start playback if unmuting and not started yet
if (!consume && sc == SDL_SCANCODE_M) { if (!consume && sc == SDL_SCANCODE_M) {
Audio::instance().toggleMute(); if (auto sys = ::AudioManager::get()) sys->toggleMute();
m_musicEnabled = !m_musicEnabled; m_musicEnabled = !m_musicEnabled;
if (m_musicEnabled && !m_musicStarted && Audio::instance().getLoadedTrackCount() > 0) { if (m_musicEnabled && !m_musicStarted && ::AudioManager::get() && ::AudioManager::get()->getLoadedTrackCount() > 0) {
Audio::instance().shuffle(); ::AudioManager::get()->shuffle();
Audio::instance().start(); ::AudioManager::get()->start();
m_musicStarted = true; m_musicStarted = true;
} }
consume = true; consume = true;
@ -393,11 +394,7 @@ bool ApplicationManager::initializeManagers() {
// N: Skip to next song in the playlist (or restart menu track) // N: Skip to next song in the playlist (or restart menu track)
if (!consume && sc == SDL_SCANCODE_N) { if (!consume && sc == SDL_SCANCODE_N) {
Audio::instance().skipToNextTrack(); if (auto sys = ::AudioManager::get()) { sys->skipToNextTrack(); if (!m_musicStarted && sys->getLoadedTrackCount() > 0) { m_musicStarted = true; m_musicEnabled = true; } }
if (!m_musicStarted && Audio::instance().getLoadedTrackCount() > 0) {
m_musicStarted = true;
m_musicEnabled = true;
}
consume = true; consume = true;
} }
@ -515,13 +512,13 @@ void ApplicationManager::registerServices() {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IInputHandler service"); SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IInputHandler service");
} }
// Register Audio system singleton // Register Audio system singleton (via AudioManager)
auto& audioInstance = Audio::instance(); IAudioSystem* audioInstance = AudioManager::get();
auto audioPtr = std::shared_ptr<Audio>(&audioInstance, [](Audio*) { if (audioInstance) {
// Custom deleter that does nothing since Audio is a singleton std::shared_ptr<IAudioSystem> audioPtr(audioInstance, [](IAudioSystem*){});
}); m_serviceContainer.registerSingleton<IAudioSystem>(audioPtr);
m_serviceContainer.registerSingleton<IAudioSystem>(audioPtr); SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IAudioSystem service");
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IAudioSystem service"); }
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Service registration completed successfully"); SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Service registration completed successfully");
} }
@ -618,7 +615,7 @@ bool ApplicationManager::initializeGame() {
// as lambdas that reference members here. // as lambdas that reference members here.
// Start background music loading similar to main.cpp: Audio init + file discovery // Start background music loading similar to main.cpp: Audio init + file discovery
Audio::instance().init(); if (auto sys = ::AudioManager::get()) sys->init();
// Discover available tracks (up to 100) and queue for background loading // Discover available tracks (up to 100) and queue for background loading
m_totalTracks = 0; m_totalTracks = 0;
std::vector<std::string> trackPaths; std::vector<std::string> trackPaths;
@ -634,15 +631,15 @@ bool ApplicationManager::initializeGame() {
} }
m_totalTracks = static_cast<int>(trackPaths.size()); m_totalTracks = static_cast<int>(trackPaths.size());
for (const auto& path : trackPaths) { for (const auto& path : trackPaths) {
Audio::instance().addTrackAsync(path); if (auto sys = ::AudioManager::get()) sys->addTrackAsync(path);
} }
if (m_totalTracks > 0) { if (m_totalTracks > 0) {
Audio::instance().startBackgroundLoading(); if (auto sys = ::AudioManager::get()) sys->startBackgroundLoading();
// Kick off playback now; Audio will pick a track once decoded. // Kick off playback now; Audio will pick a track once decoded.
// Do not mark as started yet; we'll flip the flag once a track is actually loaded. // Do not mark as started yet; we'll flip the flag once a track is actually loaded.
if (m_musicEnabled) { if (m_musicEnabled) {
Audio::instance().shuffle(); if (auto sys = ::AudioManager::get()) { sys->shuffle(); sys->start(); }
Audio::instance().start(); m_musicStarted = true;
} }
m_currentTrackLoading = 1; // mark started m_currentTrackLoading = 1; // mark started
} }
@ -941,15 +938,15 @@ void ApplicationManager::setupStateHandlers() {
// Start music as soon as at least one track has decoded (dont wait for all) // Start music as soon as at least one track has decoded (dont wait for all)
// Start music as soon as at least one track has decoded (don't wait for all) // Start music as soon as at least one track has decoded (don't wait for all)
if (m_musicEnabled && !m_musicStarted) { if (m_musicEnabled && !m_musicStarted) {
if (Audio::instance().getLoadedTrackCount() > 0) { if (auto sys = ::AudioManager::get()) {
Audio::instance().shuffle(); if (sys->getLoadedTrackCount() > 0) { sys->shuffle(); sys->start(); m_musicStarted = true; }
Audio::instance().start();
m_musicStarted = true;
} }
} }
// Track completion status for UI // Track completion status for UI
if (!m_musicLoaded && Audio::instance().isLoadingComplete()) { if (!m_musicLoaded) {
m_musicLoaded = true; if (auto sys = ::AudioManager::get()) {
if (sys->isLoadingComplete()) m_musicLoaded = true;
}
} }
}); });

View File

@ -1,6 +1,7 @@
#include "AssetManager.h" #include "AssetManager.h"
#include "../../graphics/ui/Font.h" #include "../../graphics/ui/Font.h"
#include "../../audio/Audio.h" #include "../../audio/Audio.h"
#include "../../audio/AudioManager.h"
#include "../../audio/SoundEffect.h" #include "../../audio/SoundEffect.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h> #include <SDL3_image/SDL_image.h>
@ -40,7 +41,7 @@ bool AssetManager::initialize(SDL_Renderer* renderer) {
m_renderer = renderer; m_renderer = renderer;
// Get references to singleton systems // Get references to singleton systems
m_audioSystem = &Audio::instance(); m_audioSystem = ::AudioManager::get();
m_soundSystem = &SoundEffectManager::instance(); m_soundSystem = &SoundEffectManager::instance();
m_initialized = true; m_initialized = true;

View File

@ -7,12 +7,12 @@
#include <memory> #include <memory>
#include <functional> #include <functional>
#include "../interfaces/IAssetLoader.h" #include "../interfaces/IAssetLoader.h"
#include "../interfaces/IAssetLoader.h"
// Forward declarations // Forward declarations
class FontAtlas; class FontAtlas;
class Audio; class Audio;
class SoundEffectManager; class SoundEffectManager;
class IAudioSystem;
/** /**
* AssetManager - Centralized resource management following SOLID principles * AssetManager - Centralized resource management following SOLID principles
@ -121,7 +121,7 @@ private:
// System references // System references
SDL_Renderer* m_renderer; SDL_Renderer* m_renderer;
Audio* m_audioSystem; // Pointer to singleton IAudioSystem* m_audioSystem; // Pointer to audio system (IAudioSystem)
SoundEffectManager* m_soundSystem; // Pointer to singleton SoundEffectManager* m_soundSystem; // Pointer to singleton
// Configuration // Configuration

View File

@ -1,6 +1,8 @@
#pragma once #pragma once
#include <string> #include <string>
#include <vector>
#include <cstdint>
/** /**
* @brief Abstract interface for audio system operations * @brief Abstract interface for audio system operations
@ -52,4 +54,28 @@ public:
* @return true if music is playing, false otherwise * @return true if music is playing, false otherwise
*/ */
virtual bool isMusicPlaying() const = 0; virtual bool isMusicPlaying() const = 0;
// Extended control methods used by the application
virtual bool init() = 0;
virtual void shutdown() = 0;
virtual void addTrack(const std::string& path) = 0;
virtual void addTrackAsync(const std::string& path) = 0;
virtual void startBackgroundLoading() = 0;
virtual bool isLoadingComplete() const = 0;
virtual int getLoadedTrackCount() const = 0;
virtual void start() = 0;
virtual void skipToNextTrack() = 0;
virtual void shuffle() = 0;
virtual void toggleMute() = 0;
virtual bool isMuted() const = 0;
virtual void setMenuTrack(const std::string& path) = 0;
virtual void playMenuMusic() = 0;
virtual void playGameMusic() = 0;
// Low-level SFX path (raw PCM) used by internal SFX mixer
virtual void playSfx(const std::vector<int16_t>& pcm, int channels, int rate, float volume) = 0;
}; };

View File

@ -3,6 +3,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include "audio/Audio.h" #include "audio/Audio.h"
#include "audio/AudioManager.h"
#ifndef M_PI #ifndef M_PI
#define M_PI 3.14159265358979323846 #define M_PI 3.14159265358979323846
@ -266,6 +267,6 @@ void LineEffect::playLineClearSound(int lineCount) {
const std::vector<int16_t>* sample = (lineCount == 4) ? &tetrisSample : &lineClearSample; const std::vector<int16_t>* sample = (lineCount == 4) ? &tetrisSample : &lineClearSample;
if (sample && !sample->empty()) { if (sample && !sample->empty()) {
// Mix via shared Audio device so it layers with music // Mix via shared Audio device so it layers with music
Audio::instance().playSfx(*sample, 2, 44100, (lineCount == 4) ? 0.9f : 0.7f); if (auto sys = ::AudioManager::get()) sys->playSfx(*sample, 2, 44100, (lineCount == 4) ? 0.9f : 0.7f);
} }
} }

View File

@ -3,6 +3,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include "audio/Audio.h" #include "audio/Audio.h"
#include "audio/AudioManager.h"
#include "gameplay/core/Game.h" #include "gameplay/core/Game.h"
#ifndef M_PI #ifndef M_PI
@ -461,7 +462,7 @@ void LineEffect::playLineClearSound(int lineCount) {
const std::vector<int16_t>* sample = (lineCount == 4) ? &tetrisSample : &lineClearSample; const std::vector<int16_t>* sample = (lineCount == 4) ? &tetrisSample : &lineClearSample;
if (sample && !sample->empty()) { if (sample && !sample->empty()) {
// Mix via shared Audio device so it layers with music // Mix via shared Audio device so it layers with music
Audio::instance().playSfx(*sample, 2, 44100, (lineCount == 4) ? 0.9f : 0.7f); if (auto sys = ::AudioManager::get()) sys->playSfx(*sample, 2, 44100, (lineCount == 4) ? 0.9f : 0.7f);
} }
} }

View File

@ -8,6 +8,7 @@
#include "../core/Settings.h" #include "../core/Settings.h"
#include "../core/state/StateManager.h" #include "../core/state/StateManager.h"
#include "../audio/Audio.h" #include "../audio/Audio.h"
#include "../audio/AudioManager.h"
#include "../audio/SoundEffect.h" #include "../audio/SoundEffect.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3/SDL_render.h> #include <SDL3/SDL_render.h>
@ -180,7 +181,7 @@ void MenuState::showCoopSetupPanel(bool show, bool resumeMusic) {
coopSetupStep = CoopSetupStep::ChoosePartner; coopSetupStep = CoopSetupStep::ChoosePartner;
// Resume menu music only when requested (ESC should pass resumeMusic=false) // Resume menu music only when requested (ESC should pass resumeMusic=false)
if (resumeMusic && ctx.musicEnabled && *ctx.musicEnabled) { if (resumeMusic && ctx.musicEnabled && *ctx.musicEnabled) {
Audio::instance().playMenuMusic(); if (auto sys = ::AudioManager::get()) sys->playMenuMusic();
} }
} }
} }

View File

@ -2,6 +2,7 @@
#include "../core/state/StateManager.h" #include "../core/state/StateManager.h"
#include "../graphics/ui/Font.h" #include "../graphics/ui/Font.h"
#include "../audio/Audio.h" #include "../audio/Audio.h"
#include "../audio/AudioManager.h"
#include "../audio/SoundEffect.h" #include "../audio/SoundEffect.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <algorithm> #include <algorithm>
@ -220,7 +221,7 @@ void OptionsState::toggleFullscreen() {
} }
void OptionsState::toggleMusic() { void OptionsState::toggleMusic() {
Audio::instance().toggleMute(); if (auto sys = ::AudioManager::get()) sys->toggleMute();
// If muted, music is disabled. If not muted, music is enabled. // If muted, music is disabled. If not muted, music is enabled.
// Note: Audio::instance().isMuted() returns true if muted. // Note: Audio::instance().isMuted() returns true if muted.
// But Audio class doesn't expose isMuted directly in header usually? // But Audio class doesn't expose isMuted directly in header usually?

View File

@ -3,6 +3,7 @@
#include "../video/VideoPlayer.h" #include "../video/VideoPlayer.h"
#include "../audio/Audio.h" #include "../audio/Audio.h"
#include "../audio/AudioManager.h"
#include "../core/state/StateManager.h" #include "../core/state/StateManager.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
@ -104,7 +105,7 @@ void VideoState::startAudioIfReady() {
if (m_audioPcm.empty()) return; if (m_audioPcm.empty()) return;
// Use the existing audio output path (same device as music/SFX). // Use the existing audio output path (same device as music/SFX).
Audio::instance().playSfx(m_audioPcm, m_audioChannels, m_audioRate, 1.0f); if (auto sys = ::AudioManager::get()) sys->playSfx(m_audioPcm, m_audioChannels, m_audioRate, 1.0f);
m_audioStarted = true; m_audioStarted = true;
} }