Updated game structure
This commit is contained in:
252
src/audio/Audio.cpp
Normal file
252
src/audio/Audio.cpp
Normal file
@ -0,0 +1,252 @@
|
||||
// Audio.cpp - Windows Media Foundation MP3 decoding
|
||||
#include "audio/Audio.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <mfapi.h>
|
||||
#include <mfidl.h>
|
||||
#include <mfreadwrite.h>
|
||||
#include <objbase.h>
|
||||
#include <wrl/client.h>
|
||||
#pragma comment(lib, "mfplat.lib")
|
||||
#pragma comment(lib, "mfreadwrite.lib")
|
||||
#pragma comment(lib, "mfuuid.lib")
|
||||
#pragma comment(lib, "ole32.lib")
|
||||
using Microsoft::WRL::ComPtr;
|
||||
#endif
|
||||
|
||||
Audio& Audio::instance(){ static Audio inst; return inst; }
|
||||
|
||||
bool Audio::init(){ if(outSpec.freq!=0) return true; outSpec.format=SDL_AUDIO_S16; outSpec.channels=outChannels; outSpec.freq=outRate;
|
||||
#ifdef _WIN32
|
||||
if(!mfStarted){ if(FAILED(MFStartup(MF_VERSION))) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] MFStartup failed"); } else mfStarted=true; }
|
||||
#endif
|
||||
return true; }
|
||||
|
||||
#ifdef _WIN32
|
||||
static bool decodeMP3(const std::string& path, std::vector<int16_t>& outPCM, int& outRate, int& outCh){
|
||||
outPCM.clear(); outRate=44100; outCh=2;
|
||||
ComPtr<IMFSourceReader> reader;
|
||||
wchar_t wpath[MAX_PATH]; int wlen = MultiByteToWideChar(CP_UTF8,0,path.c_str(),-1,wpath,MAX_PATH); if(!wlen) return false;
|
||||
if(FAILED(MFCreateSourceReaderFromURL(wpath,nullptr,&reader))) return false;
|
||||
// Request PCM output
|
||||
ComPtr<IMFMediaType> outType; MFCreateMediaType(&outType); outType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio); outType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM); outType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, 2); outType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, 44100); outType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, 4); outType->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, 44100*4); outType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, 16); outType->SetUINT32(MF_MT_AUDIO_CHANNEL_MASK, 3); reader->SetCurrentMediaType((DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, NULL, outType.Get());
|
||||
reader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE);
|
||||
while(true){ DWORD flags=0; ComPtr<IMFSample> sample; if(FAILED(reader->ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM,0,nullptr,&flags,nullptr,&sample))) break; if(flags & MF_SOURCE_READERF_ENDOFSTREAM) break; if(!sample) continue; ComPtr<IMFMediaBuffer> buffer; if(FAILED(sample->ConvertToContiguousBuffer(&buffer))) continue; BYTE* data=nullptr; DWORD maxLen=0, curLen=0; if(SUCCEEDED(buffer->Lock(&data,&maxLen,&curLen)) && curLen){ size_t samples = curLen/2; size_t oldSz = outPCM.size(); outPCM.resize(oldSz + samples); std::memcpy(outPCM.data()+oldSz, data, curLen); } if(data) buffer->Unlock(); }
|
||||
outRate=44100; outCh=2; return !outPCM.empty(); }
|
||||
#endif
|
||||
|
||||
void Audio::addTrack(const std::string& path){ AudioTrack t; t.path=path;
|
||||
#ifdef _WIN32
|
||||
if(decodeMP3(path, t.pcm, t.rate, t.channels)) t.ok=true; else SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] Failed to decode %s", path.c_str());
|
||||
#else
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] MP3 unsupported on this platform (stub): %s", path.c_str());
|
||||
#endif
|
||||
tracks.push_back(std::move(t)); }
|
||||
|
||||
void Audio::shuffle(){
|
||||
std::lock_guard<std::mutex> lock(tracksMutex);
|
||||
std::shuffle(tracks.begin(), tracks.end(), rng);
|
||||
}
|
||||
|
||||
bool Audio::ensureStream(){
|
||||
if(audioStream) return true;
|
||||
audioStream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &outSpec, &Audio::streamCallback, this);
|
||||
if(!audioStream){
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] SDL_OpenAudioDeviceStream failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Audio::start(){ if(!ensureStream()) return; if(!playing){ current=-1; nextTrack(); SDL_ResumeAudioStreamDevice(audioStream); playing=true; } }
|
||||
|
||||
void Audio::toggleMute(){ muted=!muted; }
|
||||
|
||||
void Audio::nextTrack(){ if(tracks.empty()) return; for(size_t i=0;i<tracks.size(); ++i){ current = (current + 1) % (int)tracks.size(); if(tracks[current].ok){ tracks[current].cursor=0; return; } } current=-1; }
|
||||
|
||||
void Audio::feed(Uint32 bytesWanted, SDL_AudioStream* stream){
|
||||
if(bytesWanted==0) return;
|
||||
// Prepare a buffer of int16 samples for the output device
|
||||
const size_t outSamples = bytesWanted / sizeof(int16_t);
|
||||
std::vector<int16_t> mix(outSamples, 0);
|
||||
|
||||
// 1) Mix music into buffer (if not muted)
|
||||
if(!muted && current >= 0){
|
||||
size_t cursorBytes = 0;
|
||||
while(cursorBytes < bytesWanted){
|
||||
if(current < 0) break;
|
||||
auto &trk = tracks[current];
|
||||
size_t samplesAvail = trk.pcm.size() - trk.cursor; // samples (int16)
|
||||
if(samplesAvail == 0){ nextTrack(); if(current < 0) break; continue; }
|
||||
size_t samplesNeeded = (bytesWanted - cursorBytes) / sizeof(int16_t);
|
||||
size_t toCopy = (samplesAvail < samplesNeeded) ? samplesAvail : samplesNeeded;
|
||||
if(toCopy == 0) break;
|
||||
// Mix add with clamp
|
||||
size_t startSample = cursorBytes / sizeof(int16_t);
|
||||
for(size_t i=0;i<toCopy;++i){
|
||||
int v = (int)mix[startSample+i] + (int)trk.pcm[trk.cursor+i];
|
||||
if(v>32767) v=32767; if(v<-32768) v=-32768; mix[startSample+i] = (int16_t)v;
|
||||
}
|
||||
trk.cursor += toCopy;
|
||||
cursorBytes += (Uint32)(toCopy * sizeof(int16_t));
|
||||
if(trk.cursor >= trk.pcm.size()) nextTrack();
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Mix active SFX
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(sfxMutex);
|
||||
for(size_t si=0; si<activeSfx.size(); ){
|
||||
auto &s = activeSfx[si];
|
||||
size_t samplesAvail = s.pcm.size() - s.cursor;
|
||||
if(samplesAvail == 0){ activeSfx.erase(activeSfx.begin()+si); continue; }
|
||||
size_t toCopy = (samplesAvail < outSamples) ? samplesAvail : outSamples;
|
||||
for(size_t i=0;i<toCopy;++i){
|
||||
int v = (int)mix[i] + (int)s.pcm[s.cursor+i];
|
||||
if(v>32767) v=32767; if(v<-32768) v=-32768; mix[i] = (int16_t)v;
|
||||
}
|
||||
s.cursor += toCopy;
|
||||
++si;
|
||||
}
|
||||
}
|
||||
|
||||
// Submit mixed audio
|
||||
if(!mix.empty()) SDL_PutAudioStreamData(stream, mix.data(), (int)bytesWanted);
|
||||
}
|
||||
|
||||
void Audio::playSfx(const std::vector<int16_t>& pcm, int channels, int rate, float volume){
|
||||
if(pcm.empty()) return;
|
||||
if(!ensureStream()) return;
|
||||
// Convert input to device format (S16, stereo, 44100)
|
||||
SDL_AudioSpec src{}; src.format=SDL_AUDIO_S16; src.channels=(Uint8)channels; src.freq=rate;
|
||||
SDL_AudioSpec dst{}; dst.format=SDL_AUDIO_S16; dst.channels=(Uint8)outChannels; dst.freq=outRate;
|
||||
SDL_AudioStream* cvt = SDL_CreateAudioStream(&src, &dst);
|
||||
if(!cvt) return;
|
||||
// Apply volume while copying into a temp buffer
|
||||
std::vector<int16_t> volBuf(pcm.size());
|
||||
for(size_t i=0;i<pcm.size();++i){
|
||||
int v = (int)(pcm[i] * volume);
|
||||
if(v>32767) v=32767; if(v<-32768) v=-32768; volBuf[i]=(int16_t)v;
|
||||
}
|
||||
SDL_PutAudioStreamData(cvt, volBuf.data(), (int)(volBuf.size()*sizeof(int16_t)));
|
||||
SDL_FlushAudioStream(cvt);
|
||||
int bytes = SDL_GetAudioStreamAvailable(cvt);
|
||||
if(bytes>0){
|
||||
std::vector<int16_t> out(bytes/2);
|
||||
SDL_GetAudioStreamData(cvt, out.data(), bytes);
|
||||
std::lock_guard<std::mutex> lock(sfxMutex);
|
||||
activeSfx.push_back(SfxPlay{ std::move(out), 0 });
|
||||
}
|
||||
SDL_DestroyAudioStream(cvt);
|
||||
}
|
||||
|
||||
void SDLCALL Audio::streamCallback(void* userdata, SDL_AudioStream* stream, int additional, int total){ Uint32 want = additional>0 ? (Uint32)additional : (Uint32)total; if(!want) want=4096; reinterpret_cast<Audio*>(userdata)->feed(want, stream); }
|
||||
|
||||
void Audio::addTrackAsync(const std::string& path) {
|
||||
std::lock_guard<std::mutex> lock(pendingTracksMutex);
|
||||
pendingTracks.push_back(path);
|
||||
}
|
||||
|
||||
void Audio::startBackgroundLoading() {
|
||||
if (loadingThread.joinable()) return; // Already running
|
||||
loadingComplete = false;
|
||||
loadedCount = 0;
|
||||
loadingThread = std::thread(&Audio::backgroundLoadingThread, this);
|
||||
}
|
||||
|
||||
void Audio::backgroundLoadingThread() {
|
||||
#ifdef _WIN32
|
||||
// Initialize COM and MF for this thread
|
||||
HRESULT hrCom = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
HRESULT hrMF = MFStartup(MF_VERSION);
|
||||
bool mfInitialized = SUCCEEDED(hrMF);
|
||||
|
||||
if (!mfInitialized) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] Failed to initialize MF on background thread");
|
||||
}
|
||||
#endif
|
||||
|
||||
// Copy pending tracks to avoid holding the mutex during processing
|
||||
std::vector<std::string> tracksToProcess;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pendingTracksMutex);
|
||||
tracksToProcess = pendingTracks;
|
||||
}
|
||||
|
||||
for (const std::string& path : tracksToProcess) {
|
||||
AudioTrack t;
|
||||
t.path = path;
|
||||
#ifdef _WIN32
|
||||
if (mfInitialized && decodeMP3(path, t.pcm, t.rate, t.channels)) {
|
||||
t.ok = true;
|
||||
} else {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] Failed to decode %s", path.c_str());
|
||||
}
|
||||
#else
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] MP3 unsupported on this platform (stub): %s", path.c_str());
|
||||
#endif
|
||||
|
||||
// Thread-safe addition to tracks
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(tracksMutex);
|
||||
tracks.push_back(std::move(t));
|
||||
}
|
||||
|
||||
loadedCount++;
|
||||
|
||||
// Small delay to prevent overwhelming the system
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// Cleanup MF and COM for this thread
|
||||
if (mfInitialized) {
|
||||
MFShutdown();
|
||||
}
|
||||
if (SUCCEEDED(hrCom)) {
|
||||
CoUninitialize();
|
||||
}
|
||||
#endif
|
||||
|
||||
loadingComplete = true;
|
||||
}
|
||||
|
||||
void Audio::waitForLoadingComplete() {
|
||||
if (loadingThread.joinable()) {
|
||||
loadingThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
bool Audio::isLoadingComplete() const {
|
||||
return loadingComplete;
|
||||
}
|
||||
|
||||
int Audio::getLoadedTrackCount() const {
|
||||
return loadedCount;
|
||||
}
|
||||
|
||||
void Audio::shutdown(){
|
||||
// Stop background loading thread first
|
||||
if (loadingThread.joinable()) {
|
||||
loadingThread.join();
|
||||
}
|
||||
|
||||
if(audioStream){ SDL_DestroyAudioStream(audioStream); audioStream=nullptr; }
|
||||
tracks.clear();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pendingTracksMutex);
|
||||
pendingTracks.clear();
|
||||
}
|
||||
playing=false;
|
||||
#ifdef _WIN32
|
||||
if(mfStarted){ MFShutdown(); mfStarted=false; }
|
||||
#endif
|
||||
}
|
||||
60
src/audio/Audio.h
Normal file
60
src/audio/Audio.h
Normal file
@ -0,0 +1,60 @@
|
||||
// Audio.h - MP3 playlist playback (Windows Media Foundation backend) + SDL3 stream
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <random>
|
||||
#include <cstdint>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
|
||||
struct AudioTrack {
|
||||
std::string path;
|
||||
std::vector<int16_t> pcm;
|
||||
int channels = 2;
|
||||
int rate = 44100;
|
||||
size_t cursor = 0;
|
||||
bool ok = false;
|
||||
};
|
||||
|
||||
class Audio {
|
||||
public:
|
||||
static Audio& instance();
|
||||
bool init(); // initialize backend (MF on Windows)
|
||||
void addTrack(const std::string& path); // decode MP3 -> PCM16 stereo 44100
|
||||
void addTrackAsync(const std::string& path); // add track for background loading
|
||||
void startBackgroundLoading(); // start background thread for loading
|
||||
void waitForLoadingComplete(); // wait for all tracks to finish loading
|
||||
bool isLoadingComplete() const; // check if background loading is done
|
||||
int getLoadedTrackCount() const; // get number of tracks loaded so far
|
||||
void shuffle(); // randomize order
|
||||
void start(); // begin playback
|
||||
void toggleMute();
|
||||
// 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:
|
||||
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);
|
||||
void feed(Uint32 bytesWanted, SDL_AudioStream* stream);
|
||||
void nextTrack();
|
||||
bool ensureStream();
|
||||
void backgroundLoadingThread(); // background thread function
|
||||
|
||||
std::vector<AudioTrack> tracks; int current=-1; bool playing=false; bool muted=false; std::mt19937 rng{std::random_device{}()};
|
||||
SDL_AudioStream* audioStream=nullptr; SDL_AudioSpec outSpec{}; int outChannels=2; int outRate=44100; bool mfStarted=false;
|
||||
|
||||
// Threading support
|
||||
std::vector<std::string> pendingTracks;
|
||||
std::thread loadingThread;
|
||||
std::mutex tracksMutex;
|
||||
std::mutex pendingTracksMutex;
|
||||
std::atomic<bool> loadingComplete{false};
|
||||
std::atomic<int> loadedCount{0};
|
||||
|
||||
// SFX mixing support
|
||||
struct SfxPlay { std::vector<int16_t> pcm; size_t cursor=0; };
|
||||
std::vector<SfxPlay> activeSfx;
|
||||
std::mutex sfxMutex;
|
||||
};
|
||||
22
src/audio/MenuWrappers.h
Normal file
22
src/audio/MenuWrappers.h
Normal file
@ -0,0 +1,22 @@
|
||||
// MenuWrappers.h - function prototypes for menu helper wrappers implemented in main.cpp
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <string>
|
||||
|
||||
class FontAtlas;
|
||||
|
||||
// Draw fireworks using the provided blocks texture (may be nullptr)
|
||||
void menu_drawFireworks(SDL_Renderer* renderer, SDL_Texture* blocksTex);
|
||||
void menu_updateFireworks(double frameMs);
|
||||
double menu_getLogoAnimCounter();
|
||||
int menu_getHoveredButton();
|
||||
|
||||
void menu_drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
|
||||
const std::string& label, bool isHovered, bool isSelected);
|
||||
|
||||
void menu_drawMenuButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h,
|
||||
const std::string& label, SDL_Color bgColor, SDL_Color borderColor);
|
||||
|
||||
void menu_drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, SDL_Texture* bgTex, int selectedLevel);
|
||||
void menu_drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled);
|
||||
318
src/audio/SoundEffect.cpp
Normal file
318
src/audio/SoundEffect.cpp
Normal file
@ -0,0 +1,318 @@
|
||||
// SoundEffect.cpp - Implementation of sound effects system
|
||||
#include "SoundEffect.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include "audio/Audio.h"
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <mfapi.h>
|
||||
#include <mfidl.h>
|
||||
#include <mfreadwrite.h>
|
||||
#include <objbase.h>
|
||||
#include <wrl/client.h>
|
||||
#pragma comment(lib, "mfplat.lib")
|
||||
#pragma comment(lib, "mfreadwrite.lib")
|
||||
#pragma comment(lib, "mfuuid.lib")
|
||||
#pragma comment(lib, "ole32.lib")
|
||||
using Microsoft::WRL::ComPtr;
|
||||
#endif
|
||||
|
||||
// SoundEffect implementation
|
||||
bool SoundEffect::load(const std::string& filePath) {
|
||||
// Determine file type by extension
|
||||
std::string extension = filePath.substr(filePath.find_last_of('.') + 1);
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
|
||||
|
||||
bool success = false;
|
||||
if (extension == "wav") {
|
||||
success = loadWAV(filePath);
|
||||
} else if (extension == "mp3") {
|
||||
success = loadMP3(filePath);
|
||||
} else {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] Unsupported file format: %s", extension.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
//std::printf("[SoundEffect] Loaded: %s (%d channels, %d Hz, %zu samples)\n", filePath.c_str(), channels, sampleRate, pcmData.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
void SoundEffect::play(float volume) {
|
||||
if (!loaded || pcmData.empty()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] Cannot play - loaded=%d, pcmData.size()=%zu", loaded, pcmData.size());
|
||||
return;
|
||||
}
|
||||
|
||||
//std::printf("[SoundEffect] Playing sound with %zu samples at volume %.2f\n", pcmData.size(), volume);
|
||||
|
||||
// Calculate final volume
|
||||
float finalVolume = defaultVolume * volume;
|
||||
finalVolume = (std::max)(0.0f, (std::min)(1.0f, finalVolume));
|
||||
|
||||
// Use the simple audio player to play this sound
|
||||
SimpleAudioPlayer::instance().playSound(pcmData, channels, sampleRate, finalVolume);
|
||||
}
|
||||
|
||||
void SoundEffect::setVolume(float volume) {
|
||||
defaultVolume = (std::max)(0.0f, (std::min)(1.0f, volume));
|
||||
}
|
||||
|
||||
// SimpleAudioPlayer implementation
|
||||
SimpleAudioPlayer& SimpleAudioPlayer::instance() {
|
||||
static SimpleAudioPlayer inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
bool SimpleAudioPlayer::init() {
|
||||
if (initialized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
//std::printf("[SimpleAudioPlayer] Initialized\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
void SimpleAudioPlayer::shutdown() {
|
||||
initialized = false;
|
||||
//std::printf("[SimpleAudioPlayer] Shut down\n");
|
||||
}
|
||||
|
||||
void SimpleAudioPlayer::playSound(const std::vector<int16_t>& pcmData, int channels, int sampleRate, float volume) {
|
||||
if (!initialized || pcmData.empty()) {
|
||||
return;
|
||||
}
|
||||
// Route through shared Audio mixer so SFX always play over music
|
||||
Audio::instance().playSfx(pcmData, channels, sampleRate, volume);
|
||||
}
|
||||
|
||||
bool SoundEffect::loadWAV(const std::string& filePath) {
|
||||
SDL_AudioSpec wavSpec;
|
||||
Uint8* wavBuffer;
|
||||
Uint32 wavLength;
|
||||
|
||||
if (!SDL_LoadWAV(filePath.c_str(), &wavSpec, &wavBuffer, &wavLength)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] Failed to load WAV file %s: %s",
|
||||
filePath.c_str(), SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store audio format info
|
||||
channels = wavSpec.channels;
|
||||
sampleRate = wavSpec.freq;
|
||||
|
||||
// Convert to 16-bit signed if needed
|
||||
if (wavSpec.format == SDL_AUDIO_S16) {
|
||||
// Already in the right format
|
||||
size_t samples = wavLength / sizeof(int16_t);
|
||||
pcmData.resize(samples);
|
||||
std::memcpy(pcmData.data(), wavBuffer, wavLength);
|
||||
} else {
|
||||
// Need to convert format
|
||||
SDL_AudioSpec srcSpec = wavSpec;
|
||||
SDL_AudioSpec dstSpec = wavSpec;
|
||||
dstSpec.format = SDL_AUDIO_S16;
|
||||
|
||||
SDL_AudioStream* converter = SDL_CreateAudioStream(&srcSpec, &dstSpec);
|
||||
if (converter) {
|
||||
SDL_PutAudioStreamData(converter, wavBuffer, wavLength);
|
||||
SDL_FlushAudioStream(converter);
|
||||
|
||||
int convertedLength = SDL_GetAudioStreamAvailable(converter);
|
||||
if (convertedLength > 0) {
|
||||
pcmData.resize(convertedLength / sizeof(int16_t));
|
||||
SDL_GetAudioStreamData(converter, pcmData.data(), convertedLength);
|
||||
}
|
||||
|
||||
SDL_DestroyAudioStream(converter);
|
||||
}
|
||||
}
|
||||
|
||||
SDL_free(wavBuffer);
|
||||
return !pcmData.empty();
|
||||
}
|
||||
|
||||
bool SoundEffect::loadMP3(const std::string& filePath) {
|
||||
#ifdef _WIN32
|
||||
static bool mfInitialized = false;
|
||||
if (!mfInitialized) {
|
||||
if (FAILED(MFStartup(MF_VERSION))) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] MFStartup failed");
|
||||
return false;
|
||||
}
|
||||
mfInitialized = true;
|
||||
}
|
||||
|
||||
ComPtr<IMFSourceReader> reader;
|
||||
wchar_t wpath[MAX_PATH];
|
||||
int wlen = MultiByteToWideChar(CP_UTF8, 0, filePath.c_str(), -1, wpath, MAX_PATH);
|
||||
if (!wlen) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] Failed to convert path to wide char");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (FAILED(MFCreateSourceReaderFromURL(wpath, nullptr, &reader))) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] Failed to create source reader for %s", filePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Request PCM output
|
||||
ComPtr<IMFMediaType> outType;
|
||||
MFCreateMediaType(&outType);
|
||||
outType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
|
||||
outType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM);
|
||||
outType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, 2);
|
||||
outType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, 44100);
|
||||
outType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, 4);
|
||||
outType->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, 44100 * 4);
|
||||
outType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, 16);
|
||||
outType->SetUINT32(MF_MT_AUDIO_CHANNEL_MASK, 3);
|
||||
reader->SetCurrentMediaType((DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, NULL, outType.Get());
|
||||
reader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE);
|
||||
|
||||
// Read all samples
|
||||
std::vector<int16_t> tempData;
|
||||
while (true) {
|
||||
DWORD flags = 0;
|
||||
ComPtr<IMFSample> sample;
|
||||
if (FAILED(reader->ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0, nullptr, &flags, nullptr, &sample))) {
|
||||
break;
|
||||
}
|
||||
if (flags & MF_SOURCE_READERF_ENDOFSTREAM) {
|
||||
break;
|
||||
}
|
||||
if (!sample) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ComPtr<IMFMediaBuffer> buffer;
|
||||
if (FAILED(sample->ConvertToContiguousBuffer(&buffer))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
BYTE* data = nullptr;
|
||||
DWORD maxLen = 0, curLen = 0;
|
||||
if (SUCCEEDED(buffer->Lock(&data, &maxLen, &curLen)) && curLen) {
|
||||
size_t samples = curLen / 2;
|
||||
size_t oldSize = tempData.size();
|
||||
tempData.resize(oldSize + samples);
|
||||
std::memcpy(tempData.data() + oldSize, data, curLen);
|
||||
}
|
||||
if (data) {
|
||||
buffer->Unlock();
|
||||
}
|
||||
}
|
||||
|
||||
if (tempData.empty()) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] No audio data decoded from %s", filePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
pcmData = std::move(tempData);
|
||||
channels = 2;
|
||||
sampleRate = 44100;
|
||||
return true;
|
||||
#else
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] MP3 support not available on this platform");
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// SoundEffectManager implementation
|
||||
SoundEffectManager& SoundEffectManager::instance() {
|
||||
static SoundEffectManager inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
bool SoundEffectManager::init() {
|
||||
if (initialized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Initialize the simple audio player
|
||||
SimpleAudioPlayer::instance().init();
|
||||
|
||||
initialized = true;
|
||||
//std::printf("[SoundEffectManager] Initialized\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
void SoundEffectManager::shutdown() {
|
||||
soundEffects.clear();
|
||||
SimpleAudioPlayer::instance().shutdown();
|
||||
initialized = false;
|
||||
//std::printf("[SoundEffectManager] Shut down\n");
|
||||
}
|
||||
|
||||
bool SoundEffectManager::loadSound(const std::string& id, const std::string& filePath) {
|
||||
if (!initialized) {
|
||||
std::fprintf(stderr, "[SoundEffectManager] Not initialized\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto soundEffect = std::make_unique<SoundEffect>();
|
||||
if (!soundEffect->load(filePath)) {
|
||||
std::fprintf(stderr, "[SoundEffectManager] Failed to load sound: %s\n", filePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove existing sound with the same ID
|
||||
soundEffects.erase(
|
||||
std::remove_if(soundEffects.begin(), soundEffects.end(),
|
||||
[&id](const auto& pair) { return pair.first == id; }),
|
||||
soundEffects.end());
|
||||
|
||||
soundEffects.emplace_back(id, std::move(soundEffect));
|
||||
//std::printf("[SoundEffectManager] Loaded sound '%s' from %s\n", id.c_str(), filePath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
void SoundEffectManager::playSound(const std::string& id, float volume) {
|
||||
if (!enabled || !initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = std::find_if(soundEffects.begin(), soundEffects.end(),
|
||||
[&id](const auto& pair) { return pair.first == id; });
|
||||
|
||||
if (it != soundEffects.end()) {
|
||||
it->second->play(volume * masterVolume);
|
||||
} else {
|
||||
std::fprintf(stderr, "[SoundEffectManager] Sound not found: %s\n", id.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void SoundEffectManager::playRandomSound(const std::vector<std::string>& soundIds, float volume) {
|
||||
if (!enabled || !initialized || soundIds.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<> dis(0, soundIds.size() - 1);
|
||||
|
||||
const std::string& selectedId = soundIds[dis(gen)];
|
||||
playSound(selectedId, volume);
|
||||
}
|
||||
|
||||
void SoundEffectManager::setMasterVolume(float volume) {
|
||||
masterVolume = (std::max)(0.0f, (std::min)(1.0f, volume));
|
||||
}
|
||||
|
||||
void SoundEffectManager::setEnabled(bool enabled_) {
|
||||
enabled = enabled_;
|
||||
}
|
||||
|
||||
bool SoundEffectManager::isEnabled() const {
|
||||
return enabled;
|
||||
}
|
||||
94
src/audio/SoundEffect.h
Normal file
94
src/audio/SoundEffect.h
Normal file
@ -0,0 +1,94 @@
|
||||
// SoundEffect.h - Single sound effect player using SDL3 audio
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
class SoundEffect {
|
||||
public:
|
||||
SoundEffect() = default;
|
||||
~SoundEffect() = default;
|
||||
|
||||
// Load a sound effect from file (WAV or MP3)
|
||||
bool load(const std::string& filePath);
|
||||
|
||||
// Play the sound effect
|
||||
void play(float volume = 1.0f);
|
||||
|
||||
// Set default volume for this sound effect
|
||||
void setVolume(float volume);
|
||||
|
||||
// Get PCM data for mixing
|
||||
const std::vector<int16_t>& getPCMData() const { return pcmData; }
|
||||
int getChannels() const { return channels; }
|
||||
int getSampleRate() const { return sampleRate; }
|
||||
bool isLoaded() const { return loaded; }
|
||||
|
||||
private:
|
||||
std::vector<int16_t> pcmData;
|
||||
int channels = 2;
|
||||
int sampleRate = 44100;
|
||||
bool loaded = false;
|
||||
float defaultVolume = 1.0f;
|
||||
|
||||
bool loadWAV(const std::string& filePath);
|
||||
bool loadMP3(const std::string& filePath);
|
||||
};
|
||||
|
||||
// Simple audio player for immediate playback
|
||||
class SimpleAudioPlayer {
|
||||
public:
|
||||
static SimpleAudioPlayer& instance();
|
||||
|
||||
bool init();
|
||||
void shutdown();
|
||||
|
||||
// Play a sound effect immediately
|
||||
void playSound(const std::vector<int16_t>& pcmData, int channels, int sampleRate, float volume = 1.0f);
|
||||
|
||||
private:
|
||||
SimpleAudioPlayer() = default;
|
||||
~SimpleAudioPlayer() = default;
|
||||
SimpleAudioPlayer(const SimpleAudioPlayer&) = delete;
|
||||
SimpleAudioPlayer& operator=(const SimpleAudioPlayer&) = delete;
|
||||
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
// Helper class to manage multiple sound effects
|
||||
class SoundEffectManager {
|
||||
public:
|
||||
static SoundEffectManager& instance();
|
||||
|
||||
bool init();
|
||||
void shutdown();
|
||||
|
||||
// Load a sound effect and assign it an ID
|
||||
bool loadSound(const std::string& id, const std::string& filePath);
|
||||
|
||||
// Play a sound effect by ID
|
||||
void playSound(const std::string& id, float volume = 1.0f);
|
||||
|
||||
// Play a random sound from a group
|
||||
void playRandomSound(const std::vector<std::string>& soundIds, float volume = 1.0f);
|
||||
|
||||
// Set master volume for all sound effects
|
||||
void setMasterVolume(float volume);
|
||||
|
||||
// Enable/disable sound effects
|
||||
void setEnabled(bool enabled);
|
||||
bool isEnabled() const;
|
||||
|
||||
private:
|
||||
SoundEffectManager() = default;
|
||||
~SoundEffectManager() = default;
|
||||
SoundEffectManager(const SoundEffectManager&) = delete;
|
||||
SoundEffectManager& operator=(const SoundEffectManager&) = delete;
|
||||
|
||||
std::vector<std::pair<std::string, std::unique_ptr<SoundEffect>>> soundEffects;
|
||||
float masterVolume = 1.0f;
|
||||
bool enabled = true;
|
||||
bool initialized = false;
|
||||
};
|
||||
Reference in New Issue
Block a user