Files
spacetris/src/audio/Audio.cpp
Gregor Klevze ec2bb1bb1e feat: Add Firebase high score sync, menu music, and gameplay improvements
- Integrate Firebase Realtime Database for high score synchronization
  - Add cpr and nlohmann-json dependencies for HTTP requests
  - Implement async score loading from Firebase with local fallback
  - Submit all scores > 0 to Firebase in background thread
  - Always prompt for player name on game over if score > 0

- Add dedicated menu music system
  - Implement menu track support in Audio class with looping
  - Add "Every Block You Take.mp3" as main menu theme
  - Automatically switch between menu and game music on state transitions
  - Load menu track asynchronously to prevent startup delays

- Update level speed progression to match web version
  - Replace NES frame-based gravity with explicit millisecond values
  - Implement 20-level speed table (1000ms to 60ms)
  - Ensure consistent gameplay between C++ and web versions

- Fix startup performance issues
  - Move score loading to background thread to prevent UI freeze
  - Optimize Firebase network requests with 2s timeout
  - Add graceful fallback to local scores on network failure

Files modified:
- src/persistence/Scores.cpp/h - Firebase integration
- src/audio/Audio.cpp/h - Menu music support
- src/core/GravityManager.cpp/h - Level speed updates
- src/main.cpp - State-based music switching, async loading
- CMakeLists.txt - Add cpr and nlohmann-json dependencies
- vcpkg.json - Update dependency list
2025-11-22 09:47:46 +01:00

383 lines
14 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)
// 1) Mix music into buffer (if not muted)
if(!muted && playing){
size_t cursorBytes = 0;
while(cursorBytes < bytesWanted){
AudioTrack* trk = nullptr;
if (isMenuMusic) {
if (menuTrack.ok) trk = &menuTrack;
} else {
if (current >= 0 && current < (int)tracks.size()) trk = &tracks[current];
}
if (!trk) break;
size_t samplesAvail = trk->pcm.size() - trk->cursor; // samples (int16)
if(samplesAvail == 0){
if (isMenuMusic) {
trk->cursor = 0; // Loop menu music
continue;
} else {
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()) {
if (isMenuMusic) {
trk->cursor = 0; // Loop menu music
} else {
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::setMenuTrack(const std::string& path) {
menuTrack.path = path;
#ifdef _WIN32
// Ensure MF is started (might be redundant if init called, but safe)
if(!mfStarted){ if(FAILED(MFStartup(MF_VERSION))) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] MFStartup failed"); } else mfStarted=true; }
if (decodeMP3(path, menuTrack.pcm, menuTrack.rate, menuTrack.channels)) {
menuTrack.ok = true;
} else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] Failed to decode menu track %s", path.c_str());
}
#else
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] MP3 unsupported (stub): %s", path.c_str());
#endif
}
void Audio::playMenuMusic() {
isMenuMusic = true;
if (menuTrack.ok) {
menuTrack.cursor = 0;
}
start();
}
void Audio::playGameMusic() {
isMenuMusic = false;
// If we were playing menu music, we might want to pick a random track or resume
if (current < 0 && !tracks.empty()) {
nextTrack();
}
start();
}
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;
}