Skip to content
Open
1 change: 1 addition & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ See docs/process.md for more on how version tagging works.
- `emcmake` no longer automatically injects `--experimental-wasm-threads` and
`--experimental-wasm-bulk-memory` flags when used with versions of node older
than v16. (#26560)
- Added sdl3_mixer port. (#26571)
- SDL3 port updated from 3.2.30 to 3.4.2 (#26572)
- Fixed a race condition in syscall proxying that caused some hangs and ASan
errors (#26582)
Expand Down
4 changes: 4 additions & 0 deletions src/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -1580,6 +1580,10 @@ var SDL2_IMAGE_FORMATS = [];
// [compile+link]
var SDL2_MIXER_FORMATS = ["ogg"];

// Formats to support in SDL3_mixer. Valid values: ogg, mp3
// [compile+link]
var SDL3_MIXER_FORMATS = ["ogg"];

// 1 = use sqlite3 from emscripten-ports
// Alternate syntax: --use-port=sqlite3
// [compile+link]
Expand Down
81 changes: 81 additions & 0 deletions test/browser/test_sdl3_mixer.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright 2025 The Emscripten Authors. All rights reserved.
* Emscripten is available under two separate licenses, the MIT license and the
* University of Illinois/NCSA Open Source License. Both these licenses can be
* found in the LICENSE file.
*/

#include <stdio.h>
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3_mixer/SDL_mixer.h>
#include <emscripten.h>

SDL_Window *window = NULL;
SDL_Renderer *renderer = NULL;
MIX_Audio *audio = NULL;
MIX_Track *track = NULL;
MIX_Mixer *mixer = NULL;

#define WIDTH 640
#define HEIGHT 480

#ifndef SOUND_PATH
#error "must define SOUND_PATH"
#endif

void sound_loop_then_quit() {
if (MIX_TrackPlaying(track))
return;

MIX_DestroyAudio(audio);
MIX_DestroyTrack(track);
MIX_DestroyMixer(mixer);

emscripten_cancel_main_loop();
printf("Shutting down\n");
exit(0);
}

int main(int argc, char *argv[]) {
SDL_Init(SDL_INIT_VIDEO);

if (!MIX_Init()) {
printf("MIX_Init failed: %s\n", SDL_GetError());
return 1;
}

if (!SDL_CreateWindowAndRenderer("SDL3 MIXER", WIDTH, HEIGHT, 0, &window, &renderer)) {
printf("SDL_CreateWindowAndRenderer: %s\n", SDL_GetError());
return 1;
}

mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, NULL);
if (!mixer) {
printf("Couldn't create mixer on default device: %s", SDL_GetError());
return 1;
}

audio = MIX_LoadAudio(mixer, SOUND_PATH, false);
if (!audio) {
printf("MIX_LoadAudio: %s\n", SDL_GetError());
return 1;
}

track = MIX_CreateTrack(mixer);
if (!track) {
printf("MIX_CreateTrack: %s\n", SDL_GetError());
return 1;
}

MIX_SetTrackAudio(track, audio);
SDL_PropertiesID props = SDL_CreateProperties();
SDL_SetNumberProperty(props, MIX_PROP_PLAY_LOOPS_NUMBER, 0);

printf("Starting sound play loop\n");
MIX_PlayTrack(track, props);

emscripten_set_main_loop(sound_loop_then_quit, 0, 1);

return 0;
}
27 changes: 27 additions & 0 deletions test/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3205,6 +3205,33 @@ def test_sdl3_canvas_write(self):
self.cflags.append('-Wno-experimental')
self.btest_exit('test_sdl3_canvas_write.c', cflags=['-sUSE_SDL=3'])

@parameterized({
'': (['-sUSE_SDL=3', '-sUSE_SDL_MIXER=3'],),
'dash_l': (['-lSDL3', '-lSDL3_mixer'],),
})
@requires_sound_hardware
def test_sdl3_mixer_wav(self, flags):
copy_asset('sounds/the_entertainer.wav', 'sound.wav')
self.cflags.append('-Wno-experimental')
self.btest_exit('test_sdl3_mixer.c', cflags=['--preload-file', 'sound.wav', '-DSOUND_PATH="sound.wav"'] + flags)

@parameterized({
'ogg': (['ogg'], 'alarmvictory_1.ogg',),
'mp3': (['mp3'], 'pudinha.mp3'),
})
@requires_sound_hardware
def test_sdl3_mixer_music(self, formats, music_name):
copy_asset(f'sounds/{music_name}')
self.cflags.append('-Wno-experimental')
args = [
'--preload-file', music_name,
'-DSOUND_PATH="%s"' % music_name,
'-sUSE_SDL=3',
'-sUSE_SDL_MIXER=3',
'-sSDL3_MIXER_FORMATS=' + ','.join(formats),
]
self.btest_exit('test_sdl3_mixer.c', cflags=args)

@requires_graphics_hardware
@no_wasm64('cocos2d ports does not compile with wasm64')
def test_cocos2d_hello(self):
Expand Down
6 changes: 6 additions & 0 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -2649,6 +2649,12 @@ def test_sdl3_ttf(self):
self.emcc(test_file('browser/test_sdl3_ttf.c'), args=['-Wno-experimental', '-sUSE_SDL=3', '-sUSE_SDL_TTF=3'])
self.emcc(test_file('browser/test_sdl3_ttf.c'), args=['-Wno-experimental', '--use-port=sdl3', '--use-port=sdl3_ttf'])

@requires_network
def test_sdl3_mixer(self):
self.emcc('browser/test_sdl3_mixer.c', ['-Wno-experimental', '-DSOUND_PATH="sound.wav"', '-sUSE_SDL=3', '-sUSE_SDL_MIXER=3', '-o', 'a.out.js'])
self.emcc('browser/test_sdl3_mixer.c', ['-Wno-experimental', '-DSOUND_PATH="sound.wav"', '--use-port=sdl3_mixer', '-o', 'a.out.js'])
self.emcc('browser/test_sdl3_mixer.c', ['-Wno-experimental', '-DSOUND_PATH="sound.wav"', '--use-port=sdl3_mixer:formats=ogg', '-o', 'a.out.js'])

@requires_network
def test_contrib_ports(self):
# Verify that contrib ports can be used (using the only contrib port available ATM, but can be replaced
Expand Down
122 changes: 122 additions & 0 deletions tools/ports/sdl3_mixer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Copyright 2025 The Emscripten Authors. All rights reserved.
# Emscripten is available under two separate licenses, the MIT license and the
# University of Illinois/NCSA Open Source License. Both these licenses can be
# found in the LICENSE file.

import os

VERSION = '3.2.0'
TAG = f'release-{VERSION}'
HASH = '96f374b3ca96202973fca84228e7775db3d6e38888888573d0ba0d045bc1d3cc6f876984e50dcce1b65875c80f8e263b5ff687570f4b4c720f48ca3cfaff0648'
SUBDIR = f'SDL3_mixer-{TAG}'

deps = ['sdl3']

variants = {
'sdl3_mixer-ogg': {'SDL3_MIXER_FORMATS': ['ogg']},
'sdl3_mixer-none': {'SDL3_MIXER_FORMATS': []},
'sdl3_mixer-ogg-mt': {'SDL3_MIXER_FORMATS': ['ogg'], 'PTHREADS': 1},
'sdl3_mixer-none-mt': {'SDL3_MIXER_FORMATS': [], 'PTHREADS': 1},
}

OPTIONS = {
'formats': 'A comma separated list of formats (ex: --use-port=sdl3_mixer:formats=ogg,mp3)',
}

SUPPORTED_FORMATS = {'ogg', 'mp3'}

# user options (from --use-port)
opts: dict[str, set] = {
'formats': set(),
}


def needed(settings):
return settings.USE_SDL_MIXER == 3


def get_formats(settings):
return opts['formats'].union(settings.SDL3_MIXER_FORMATS)


def get_lib_name(settings):
formats = '-'.join(sorted(get_formats(settings)))

libname = 'libSDL3_mixer'
if formats != '':
libname += '-' + formats
if settings.PTHREADS:
libname += '-mt'
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this library actually use threading (or atomics)?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've copied this part from sdl2_mixer.py
not sure.
there are no -pthreads flag check in cmakelists.txt.
only set(WAVPACK_ENABLE_THREADS FALSE) for wavpack which is not supported.
Should we remove this option?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think maybe because SDL3 itself has a threaded variant its required that all the sub-libraries do it.. but I can't remember the exact rationale.

Maybe leave it as is.

libname += '.a'

return libname


def get(ports, settings, shared):
ports.fetch_project('sdl3_mixer', f'https://github.com/libsdl-org/SDL_mixer/archive/{TAG}.zip', sha512hash=HASH)
libname = get_lib_name(settings)

def create(final):
src_root = ports.get_dir('sdl3_mixer', 'SDL_mixer-' + TAG)
ports.install_header_dir(os.path.join(src_root, 'include'), target='.')
srcs = [
'src/SDL_mixer.c',
'src/SDL_mixer_metadata_tags.c',
'src/SDL_mixer_spatialization.c',
'src/decoder_raw.c',
'src/decoder_sinewave.c',
'src/decoder_wav.c',
]

flags = ['-sUSE_SDL=3', '-DDECODER_WAV','-Wno-format-security', '-Wno-experimental']

if settings.PTHREADS:
flags += ['-pthread']

formats = get_formats(settings)

if 'ogg' in formats:
flags += [
'-sUSE_VORBIS',
'-DDECODER_OGGVORBIS_VORBISFILE',
]
srcs += ['src/decoder_vorbis.c',]

if 'mp3' in formats:
flags += [
'-sUSE_MPG123',
'-DDECODER_MP3_MPG123',
]
srcs += ['src/decoder_mpg123.c',]

ports.build_port(src_root, final, 'sdl3_mixer', flags=flags, srcs=srcs)
return [shared.cache.get_lib(libname, create, what='port')]


def clear(ports, settings, shared):
shared.cache.erase_lib(get_lib_name(settings))


def process_dependencies(settings):
settings.USE_SDL = 3
formats = get_formats(settings)
if 'ogg' in formats:
deps.append('vorbis')
settings.USE_VORBIS = 1
if 'mp3' in formats:
deps.append('mpg123')
settings.USE_MPG123 = 1


def handle_options(options, error_handler):
formats = options['formats'].split(',')
for format in formats:
format = format.lower().strip()
if format not in SUPPORTED_FORMATS:
error_handler(f'{format} is not a supported format')
else:
opts['formats'].add(format)


def show():
return 'sdl3_mixer (-sUSE_SDL_MIXER=3 or --use-port=sdl3_mixer; zlib license)'
1 change: 1 addition & 0 deletions tools/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
'USE_FREETYPE',
'SDL2_MIXER_FORMATS',
'SDL2_IMAGE_FORMATS',
'SDL3_MIXER_FORMATS',
'USE_SQLITE3',
}

Expand Down
Loading