- **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.
323 lines
12 KiB
C++
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;
|
|
}
|