319 lines
10 KiB
C++
319 lines
10 KiB
C++
// 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;
|
|
}
|