Implement SDL2_mixer audio for WASM builds, closes #247
Replace no-op audio stubs in SDL2Types.h with real SDL2_mixer-backed implementations of SoundBuffer, Sound, and Music. This enables audio playback in the browser with zero changes to Python bindings. - Add -sUSE_SDL_MIXER=2 to Emscripten compile/link flags (CMakeLists.txt) - Initialize Mix_OpenAudio in SDL2Renderer::init(), Mix_CloseAudio in shutdown() - SoundBuffer: Mix_LoadWAV/Mix_LoadWAV_RW with duration computation - Sound: channel-based playback with Mix_ChannelFinished tracking - Music: global channel streaming via Mix_LoadMUS/Mix_PlayMusic - Volume conversion: SFML 0-100 scale to SDL_mixer 0-128 scale Known limitations on web: Music.duration and Music.position getters return 0 (SDL_mixer 2.0.2 lacks Mix_MusicDuration/Mix_GetMusicPosition). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ef05152ea0
commit
0969f7c2f6
4 changed files with 290 additions and 29 deletions
|
|
@ -17,9 +17,11 @@
|
|||
#include <emscripten/html5.h>
|
||||
// Emscripten's USE_SDL=2 port puts headers directly in include path
|
||||
#include <SDL.h>
|
||||
#include <SDL_mixer.h>
|
||||
#include <GLES2/gl2.h>
|
||||
#else
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_mixer.h>
|
||||
#include <SDL2/SDL_opengl.h>
|
||||
// Desktop OpenGL - we'll use GL 2.1 compatible subset that matches GLES2
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
|
|
@ -132,11 +134,22 @@ bool SDL2Renderer::init() {
|
|||
if (initialized_) return true;
|
||||
|
||||
// Initialize SDL2 if not already done
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) < 0) {
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_AUDIO) < 0) {
|
||||
std::cerr << "SDL2Renderer: Failed to initialize SDL: " << SDL_GetError() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize SDL2_mixer for audio (non-fatal if it fails)
|
||||
if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) < 0) {
|
||||
std::cerr << "SDL2Renderer: Failed to initialize audio: " << Mix_GetError() << std::endl;
|
||||
std::cerr << "SDL2Renderer: Continuing without audio support" << std::endl;
|
||||
} else {
|
||||
Mix_AllocateChannels(16);
|
||||
Mix_ChannelFinished(Sound::onChannelFinished);
|
||||
audioInitialized_ = true;
|
||||
std::cout << "SDL2Renderer: Audio initialized (16 channels, 44100 Hz)" << std::endl;
|
||||
}
|
||||
|
||||
// Note: Shaders are initialized in initGL() after GL context is created
|
||||
|
||||
// Set up initial projection matrix (identity)
|
||||
|
|
@ -170,6 +183,12 @@ void SDL2Renderer::shutdown() {
|
|||
|
||||
shapeProgram_ = spriteProgram_ = textProgram_ = 0;
|
||||
|
||||
// Close audio before SDL_Quit
|
||||
if (audioInitialized_) {
|
||||
Mix_CloseAudio();
|
||||
audioInitialized_ = false;
|
||||
}
|
||||
|
||||
SDL_Quit();
|
||||
initialized_ = false;
|
||||
}
|
||||
|
|
@ -673,6 +692,9 @@ void RenderWindow::setSize(const Vector2u& size) {
|
|||
if (sdlWindow_) {
|
||||
SDL_SetWindowSize(static_cast<SDL_Window*>(sdlWindow_), size.x, size.y);
|
||||
glViewport(0, 0, size.x, size.y);
|
||||
#ifdef __EMSCRIPTEN__
|
||||
emscripten_set_canvas_element_size("#canvas", size.x, size.y);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ public:
|
|||
void shutdown();
|
||||
bool isInitialized() const { return initialized_; }
|
||||
bool isGLInitialized() const { return glInitialized_; }
|
||||
bool isAudioInitialized() const { return audioInitialized_; }
|
||||
|
||||
// Built-in shader programs
|
||||
enum class ShaderType {
|
||||
|
|
@ -100,6 +101,7 @@ private:
|
|||
|
||||
bool initialized_ = false;
|
||||
bool glInitialized_ = false;
|
||||
bool audioInitialized_ = false;
|
||||
|
||||
// Built-in shader programs
|
||||
unsigned int shapeProgram_ = 0;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
#include <vector>
|
||||
#include <functional>
|
||||
#include <chrono>
|
||||
#include <algorithm>
|
||||
|
||||
// SDL2 headers - conditionally included when actually implementing
|
||||
// For now, forward declare what we need
|
||||
|
|
@ -33,6 +34,13 @@
|
|||
#include <GLES2/gl2.h>
|
||||
#endif
|
||||
|
||||
// SDL2_mixer for audio (always needed in SDL2 builds for SoundBuffer/Sound/Music types)
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <SDL_mixer.h>
|
||||
#else
|
||||
#include <SDL2/SDL_mixer.h>
|
||||
#endif
|
||||
|
||||
namespace sf {
|
||||
|
||||
// Forward declarations (needed for RenderWindow)
|
||||
|
|
@ -922,34 +930,195 @@ public:
|
|||
};
|
||||
|
||||
// =============================================================================
|
||||
// Audio Stubs (SDL2_mixer could implement these later)
|
||||
// Audio (SDL2_mixer backed)
|
||||
// =============================================================================
|
||||
|
||||
class SoundBuffer {
|
||||
Mix_Chunk* chunk_ = nullptr;
|
||||
Time duration_;
|
||||
|
||||
public:
|
||||
SoundBuffer() = default;
|
||||
bool loadFromFile(const std::string& filename) { return true; } // Stub
|
||||
bool loadFromMemory(const void* data, size_t sizeInBytes) { return true; } // Stub
|
||||
Time getDuration() const { return Time(); }
|
||||
~SoundBuffer() {
|
||||
if (chunk_) {
|
||||
Mix_FreeChunk(chunk_);
|
||||
chunk_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// No copy (Mix_Chunk ownership)
|
||||
SoundBuffer(const SoundBuffer&) = delete;
|
||||
SoundBuffer& operator=(const SoundBuffer&) = delete;
|
||||
|
||||
// Move
|
||||
SoundBuffer(SoundBuffer&& other) noexcept
|
||||
: chunk_(other.chunk_), duration_(other.duration_) {
|
||||
other.chunk_ = nullptr;
|
||||
}
|
||||
SoundBuffer& operator=(SoundBuffer&& other) noexcept {
|
||||
if (this != &other) {
|
||||
if (chunk_) Mix_FreeChunk(chunk_);
|
||||
chunk_ = other.chunk_;
|
||||
duration_ = other.duration_;
|
||||
other.chunk_ = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool loadFromFile(const std::string& filename) {
|
||||
if (chunk_) { Mix_FreeChunk(chunk_); chunk_ = nullptr; }
|
||||
chunk_ = Mix_LoadWAV(filename.c_str());
|
||||
if (!chunk_) return false;
|
||||
computeDuration();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool loadFromMemory(const void* data, size_t sizeInBytes) {
|
||||
if (chunk_) { Mix_FreeChunk(chunk_); chunk_ = nullptr; }
|
||||
SDL_RWops* rw = SDL_RWFromConstMem(data, static_cast<int>(sizeInBytes));
|
||||
if (!rw) return false;
|
||||
chunk_ = Mix_LoadWAV_RW(rw, 1); // 1 = free RWops after load
|
||||
if (!chunk_) return false;
|
||||
computeDuration();
|
||||
return true;
|
||||
}
|
||||
|
||||
Time getDuration() const { return duration_; }
|
||||
Mix_Chunk* getChunk() const { return chunk_; }
|
||||
|
||||
private:
|
||||
void computeDuration() {
|
||||
if (!chunk_) { duration_ = Time(); return; }
|
||||
int freq = 0, channels = 0;
|
||||
Uint16 format = 0;
|
||||
Mix_QuerySpec(&freq, &format, &channels);
|
||||
if (freq == 0 || channels == 0) { duration_ = Time(); return; }
|
||||
// Compute bytes per sample based on format
|
||||
int bytesPerSample = 2; // Default 16-bit
|
||||
if (format == AUDIO_U8 || format == AUDIO_S8) bytesPerSample = 1;
|
||||
else if (format == AUDIO_S32LSB || format == AUDIO_S32MSB) bytesPerSample = 4;
|
||||
else if (format == AUDIO_F32LSB || format == AUDIO_F32MSB) bytesPerSample = 4;
|
||||
int totalSamples = chunk_->alen / (bytesPerSample * channels);
|
||||
float secs = static_cast<float>(totalSamples) / static_cast<float>(freq);
|
||||
duration_ = seconds(secs);
|
||||
}
|
||||
};
|
||||
|
||||
// Forward declare Sound for channel tracking
|
||||
class Sound;
|
||||
|
||||
// Channel tracking: maps SDL_mixer channel indices to Sound* owners
|
||||
// Defined as inline to keep header-only and avoid multiple definition issues
|
||||
inline Sound* g_channelOwners[16] = {};
|
||||
|
||||
class Sound {
|
||||
public:
|
||||
enum Status { Stopped, Paused, Playing };
|
||||
|
||||
Sound() = default;
|
||||
Sound(const SoundBuffer& buffer) {}
|
||||
Sound(const SoundBuffer& buffer) : chunk_(buffer.getChunk()) {}
|
||||
|
||||
void setBuffer(const SoundBuffer& buffer) {}
|
||||
void play() {}
|
||||
void pause() {}
|
||||
void stop() {}
|
||||
~Sound() {
|
||||
// Release our channel claim
|
||||
if (channel_ >= 0 && channel_ < 16) {
|
||||
if (g_channelOwners[channel_] == this) {
|
||||
Mix_HaltChannel(channel_);
|
||||
g_channelOwners[channel_] = nullptr;
|
||||
}
|
||||
channel_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
Status getStatus() const { return Stopped; }
|
||||
void setVolume(float volume) {}
|
||||
float getVolume() const { return 100.0f; }
|
||||
void setLoop(bool loop) {}
|
||||
bool getLoop() const { return false; }
|
||||
// No copy (channel ownership)
|
||||
Sound(const Sound&) = delete;
|
||||
Sound& operator=(const Sound&) = delete;
|
||||
|
||||
// Move
|
||||
Sound(Sound&& other) noexcept
|
||||
: chunk_(other.chunk_), channel_(other.channel_),
|
||||
volume_(other.volume_), loop_(other.loop_) {
|
||||
if (channel_ >= 0 && channel_ < 16) {
|
||||
g_channelOwners[channel_] = this;
|
||||
}
|
||||
other.channel_ = -1;
|
||||
other.chunk_ = nullptr;
|
||||
}
|
||||
Sound& operator=(Sound&& other) noexcept {
|
||||
if (this != &other) {
|
||||
stop();
|
||||
chunk_ = other.chunk_;
|
||||
channel_ = other.channel_;
|
||||
volume_ = other.volume_;
|
||||
loop_ = other.loop_;
|
||||
if (channel_ >= 0 && channel_ < 16) {
|
||||
g_channelOwners[channel_] = this;
|
||||
}
|
||||
other.channel_ = -1;
|
||||
other.chunk_ = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
void setBuffer(const SoundBuffer& buffer) { chunk_ = buffer.getChunk(); }
|
||||
|
||||
void play() {
|
||||
if (!chunk_) return;
|
||||
channel_ = Mix_PlayChannel(-1, chunk_, loop_ ? -1 : 0);
|
||||
if (channel_ >= 0 && channel_ < 16) {
|
||||
// Clear any previous owner on this channel
|
||||
if (g_channelOwners[channel_] && g_channelOwners[channel_] != this) {
|
||||
g_channelOwners[channel_]->channel_ = -1;
|
||||
}
|
||||
g_channelOwners[channel_] = this;
|
||||
Mix_Volume(channel_, static_cast<int>(volume_ * 128.f / 100.f));
|
||||
}
|
||||
}
|
||||
|
||||
void pause() {
|
||||
if (channel_ >= 0) Mix_Pause(channel_);
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (channel_ >= 0) {
|
||||
Mix_HaltChannel(channel_);
|
||||
if (channel_ < 16 && g_channelOwners[channel_] == this) {
|
||||
g_channelOwners[channel_] = nullptr;
|
||||
}
|
||||
channel_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
Status getStatus() const {
|
||||
if (channel_ < 0) return Stopped;
|
||||
if (Mix_Paused(channel_)) return Paused;
|
||||
if (Mix_Playing(channel_)) return Playing;
|
||||
return Stopped;
|
||||
}
|
||||
|
||||
void setVolume(float vol) {
|
||||
volume_ = std::clamp(vol, 0.f, 100.f);
|
||||
if (channel_ >= 0) {
|
||||
Mix_Volume(channel_, static_cast<int>(volume_ * 128.f / 100.f));
|
||||
}
|
||||
}
|
||||
float getVolume() const { return volume_; }
|
||||
|
||||
void setLoop(bool loop) { loop_ = loop; }
|
||||
bool getLoop() const { return loop_; }
|
||||
|
||||
// Called by Mix_ChannelFinished callback
|
||||
static void onChannelFinished(int channel) {
|
||||
if (channel >= 0 && channel < 16 && g_channelOwners[channel]) {
|
||||
g_channelOwners[channel]->channel_ = -1;
|
||||
g_channelOwners[channel] = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Mix_Chunk* chunk_ = nullptr; // Borrowed from SoundBuffer
|
||||
int channel_ = -1;
|
||||
float volume_ = 100.f;
|
||||
bool loop_ = false;
|
||||
};
|
||||
|
||||
class Music {
|
||||
|
|
@ -957,20 +1126,83 @@ public:
|
|||
enum Status { Stopped, Paused, Playing };
|
||||
|
||||
Music() = default;
|
||||
bool openFromFile(const std::string& filename) { return true; } // Stub
|
||||
~Music() {
|
||||
if (music_) {
|
||||
Mix_FreeMusic(music_);
|
||||
music_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void play() {}
|
||||
void pause() {}
|
||||
void stop() {}
|
||||
// No copy (global music channel)
|
||||
Music(const Music&) = delete;
|
||||
Music& operator=(const Music&) = delete;
|
||||
|
||||
Status getStatus() const { return Stopped; }
|
||||
void setVolume(float volume) {}
|
||||
float getVolume() const { return 100.0f; }
|
||||
void setLoop(bool loop) {}
|
||||
bool getLoop() const { return false; }
|
||||
// Move
|
||||
Music(Music&& other) noexcept
|
||||
: music_(other.music_), volume_(other.volume_), loop_(other.loop_) {
|
||||
other.music_ = nullptr;
|
||||
}
|
||||
Music& operator=(Music&& other) noexcept {
|
||||
if (this != &other) {
|
||||
if (music_) Mix_FreeMusic(music_);
|
||||
music_ = other.music_;
|
||||
volume_ = other.volume_;
|
||||
loop_ = other.loop_;
|
||||
other.music_ = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool openFromFile(const std::string& filename) {
|
||||
if (music_) { Mix_FreeMusic(music_); music_ = nullptr; }
|
||||
music_ = Mix_LoadMUS(filename.c_str());
|
||||
return music_ != nullptr;
|
||||
}
|
||||
|
||||
void play() {
|
||||
if (!music_) return;
|
||||
Mix_PlayMusic(music_, loop_ ? -1 : 0);
|
||||
Mix_VolumeMusic(static_cast<int>(volume_ * 128.f / 100.f));
|
||||
}
|
||||
|
||||
void pause() {
|
||||
Mix_PauseMusic();
|
||||
}
|
||||
|
||||
void stop() {
|
||||
Mix_HaltMusic();
|
||||
}
|
||||
|
||||
Status getStatus() const {
|
||||
if (Mix_PausedMusic()) return Paused;
|
||||
if (Mix_PlayingMusic()) return Playing;
|
||||
return Stopped;
|
||||
}
|
||||
|
||||
void setVolume(float vol) {
|
||||
volume_ = std::clamp(vol, 0.f, 100.f);
|
||||
Mix_VolumeMusic(static_cast<int>(volume_ * 128.f / 100.f));
|
||||
}
|
||||
float getVolume() const { return volume_; }
|
||||
|
||||
void setLoop(bool loop) { loop_ = loop; }
|
||||
bool getLoop() const { return loop_; }
|
||||
|
||||
// Duration not available in Emscripten's SDL_mixer 2.0.2
|
||||
Time getDuration() const { return Time(); }
|
||||
|
||||
// Playing offset getter not available in Emscripten's SDL_mixer 2.0.2
|
||||
Time getPlayingOffset() const { return Time(); }
|
||||
void setPlayingOffset(Time offset) {}
|
||||
|
||||
// Setter works for OGG via Mix_SetMusicPosition
|
||||
void setPlayingOffset(Time offset) {
|
||||
if (music_) Mix_SetMusicPosition(static_cast<double>(offset.asSeconds()));
|
||||
}
|
||||
|
||||
private:
|
||||
Mix_Music* music_ = nullptr;
|
||||
float volume_ = 100.f;
|
||||
bool loop_ = false;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue