Files
spacetris/src/audio/Audio.cpp
Gregor Klevze 66099809e0 feat: implement textured line clear effects and refine UI alignment
- **Visual Effects**: Upgraded line clear particles to use the game's block texture instead of simple circles, matching the reference web game's aesthetic.
- **Particle Physics**: Tuned particle velocity, gravity, and fade rates for a more dynamic explosion effect.
- **Rendering Integration**: Updated [main.cpp](cci:7://file:///d:/Sites/Work/tetris/src/main.cpp:0:0-0:0) and `GameRenderer` to pass the block texture to the effect system and correctly trigger animations upon line completion.
- **Menu UI**: Fixed [MenuState](cci:1://file:///d:/Sites/Work/tetris/src/states/MenuState.cpp:19:0-19:55) layout calculations to use fixed logical dimensions (1200x1000), ensuring consistent centering and alignment of the logo, buttons, and settings icon across different window sizes.
- **Code Cleanup**: Refactored `PlayingState` to delegate effect triggering to the rendering layer where correct screen coordinates are available.
2025-11-21 21:19:14 +01:00

323 lines
12 KiB
C++

// 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;
#ifdef max
#undef max
#endif
#ifdef min
#undef min
#endif
#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;
}
// Ensure the device is running so SFX can be heard even before music starts
SDL_ResumeAudioStreamDevice(audioStream);
return true;
}
void Audio::start(){
if(!ensureStream()) return;
// If no track is selected yet, try to select one now (in case tracks loaded after initial start)
if(current < 0) {
nextTrack();
}
SDL_ResumeAudioStreamDevice(audioStream);
playing = true;
}
void Audio::toggleMute(){ muted=!muted; }
void Audio::nextTrack(){
if(tracks.empty()) { current = -1; return; }
// Try every track once to find a decodable one
int start = current;
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 a previous loading thread exists but has finished, join it so we can start anew
if (loadingThread.joinable()) {
if (loadingComplete) {
loadingThread.join();
} else {
// Already running
return;
}
}
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
while (true) {
std::string path;
{
std::lock_guard<std::mutex> lock(pendingTracksMutex);
if (pendingTracks.empty()) break;
path = std::move(pendingTracks.front());
pendingTracks.erase(pendingTracks.begin());
}
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
}
// IAudioSystem interface implementation
void Audio::playSound(const std::string& name) {
// This is a simplified implementation - in a full implementation,
// you would load sound effects by name from assets
// For now, we'll just trigger a generic sound effect
// In practice, this would load a sound file and play it via playSfx
}
void Audio::playMusic(const std::string& name) {
// This is a simplified implementation - in a full implementation,
// you would load music tracks by name
// For now, we'll just start the current playlist
if (!tracks.empty() && !playing) {
start();
}
}
void Audio::stopMusic() {
playing = false;
}
void Audio::setMasterVolume(float volume) {
m_masterVolume = std::max(0.0f, std::min(1.0f, volume));
}
void Audio::setMusicVolume(float volume) {
m_musicVolume = std::max(0.0f, std::min(1.0f, volume));
}
void Audio::setSoundVolume(float volume) {
m_sfxVolume = std::max(0.0f, std::min(1.0f, volume));
}
bool Audio::isMusicPlaying() const {
return playing;
}