diff --git a/ChangeLog.md b/ChangeLog.md index 3bdde1241ba74..92a62983f929f 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -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) 5.0.4 - 03/23/26 ---------------- diff --git a/src/settings.js b/src/settings.js index 2aa74c60e20d1..161e0a5af332d 100644 --- a/src/settings.js +++ b/src/settings.js @@ -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", "mp3"]; + // 1 = use sqlite3 from emscripten-ports // Alternate syntax: --use-port=sqlite3 // [compile+link] diff --git a/test/browser/test_sdl3_mixer.c b/test/browser/test_sdl3_mixer.c new file mode 100644 index 0000000000000..f14eca9b77f25 --- /dev/null +++ b/test/browser/test_sdl3_mixer.c @@ -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 +#include +#include +#include +#include + +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; +} diff --git a/test/test_browser.py b/test/test_browser.py index 7f43824793515..1d0cf5063bb93 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -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): diff --git a/test/test_other.py b/test/test_other.py index f5fab92327817..53cd30f17dd6f 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -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 diff --git a/tools/ports/sdl3_mixer.py b/tools/ports/sdl3_mixer.py new file mode 100644 index 0000000000000..aab1914106f7e --- /dev/null +++ b/tools/ports/sdl3_mixer.py @@ -0,0 +1,124 @@ +# 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 + +from typing import Dict, Set + +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' + 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)' \ No newline at end of file diff --git a/tools/settings.py b/tools/settings.py index ce511e75eb1d5..8782fc223f9fc 100644 --- a/tools/settings.py +++ b/tools/settings.py @@ -53,6 +53,7 @@ 'USE_FREETYPE', 'SDL2_MIXER_FORMATS', 'SDL2_IMAGE_FORMATS', + 'SDL3_MIXER_FORMATS', 'USE_SQLITE3', }