From c2d7366ea1f7217dfb6fc858c7a5ef3cf17b4096 Mon Sep 17 00:00:00 2001 From: BinBashBanana <51469593+BinBashBanana@users.noreply.github.com> Date: Wed, 21 May 2025 18:05:05 -0700 Subject: [PATCH 1/2] Rewrite RWebAudio driver --- CHANGES.md | 1 + Makefile.emscripten | 60 ++++--- audio/drivers/rwebaudio.c | 210 +++++++++++++++++++++---- config.def.h | 4 +- configuration.c | 2 +- emscripten/library_rwebaudio.js | 194 ++++++++--------------- frontend/drivers/platform_emscripten.c | 10 ++ frontend/drivers/platform_emscripten.h | 14 ++ retroarch.c | 29 +++- 9 files changed, 328 insertions(+), 196 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1cb87c7e44..f238d0e75c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ - EMSCRIPTEN/RWEBCAM: Fix camera driver - EMSCRIPTEN/RWEBINPUT: Add accelerometer/gyroscope support - EMSCRIPTEN/RWEBPAD: Add rumble support +- EMSCRIPTEN/RWEBAUDIO: Rewrite driver, set as default audio driver # 1.21.0 - 3DS: Fix unique IDs for newer cores diff --git a/Makefile.emscripten b/Makefile.emscripten index 7f3fc7b437..6d7cab21dc 100644 --- a/Makefile.emscripten +++ b/Makefile.emscripten @@ -27,7 +27,7 @@ HAVE_GLSL = 1 HAVE_SCREENSHOTS = 1 HAVE_REWIND = 1 HAVE_AUDIOMIXER = 1 -HAVE_CC_RESAMPLER = 1 +HAVE_CC_RESAMPLER ?= 1 HAVE_EGL ?= 0 HAVE_OPENGLES = 1 HAVE_RJPEG = 0 @@ -54,8 +54,6 @@ HAVE_BSV_MOVIE = 1 HAVE_CHD ?= 0 HAVE_NETPLAYDISCOVERY ?= 0 -HAVE_AL ?= 1 - # enables pthreads, requires special headers on the web server: # see https://web.dev/articles/coop-coep HAVE_THREADS ?= 0 @@ -63,18 +61,14 @@ HAVE_THREADS ?= 0 # requires HAVE_THREADS HAVE_AUDIOWORKLET ?= 0 -# WARNING -- READ BEFORE ENABLING -# The rwebaudio driver is known to have several audio bugs, such as -# minor crackling, or the entire page freezing/crashing. -# It works perfectly on chrome, but even firefox has really bad audio quality. -# I should also note, the driver on iOS is completely broken (crashes the page). -# You have been warned. -HAVE_RWEBAUDIO ?= 0 +# doesn't work on PROXY_TO_PTHREAD +HAVE_RWEBAUDIO ?= 1 -# whether the browser thread is allowed to block to wait for audio to play, -# may lead to the issues mentioned above. -# currently this variable is only used by audioworklet; -# rwebaudio will always busywait and openal will never busywait. +# requires ASYNC or PROXY_TO_PTHREAD +HAVE_AL ?= 0 + +# whether the browser thread is allowed to block to wait for audio to play, not CPU usage-friendly! +# currently this variable is only used by rwebaudio and audioworklet; openal will never busywait. ALLOW_AUDIO_BUSYWAIT ?= 0 # minimal asyncify; better performance than full asyncify, @@ -196,6 +190,9 @@ endif ifeq ($(HAVE_RWEBAUDIO), 1) LDFLAGS += --js-library emscripten/library_rwebaudio.js DEFINES += -DHAVE_RWEBAUDIO + ifeq ($(PROXY_TO_PTHREAD), 1) + $(error ERROR: RWEBAUDIO is incompatible with PROXY_TO_PTHREAD) + endif endif ifeq ($(HAVE_AUDIOWORKLET), 1) @@ -205,18 +202,24 @@ ifeq ($(HAVE_AUDIOWORKLET), 1) ifeq ($(HAVE_THREADS), 0) $(error ERROR: AUDIOWORKLET requires HAVE_THREADS) endif - ifeq ($(PROXY_TO_PTHREAD), 1) - else ifeq ($(ASYNC), 1) +endif + +ifeq ($(HAVE_AL), 1) + LDFLAGS += -lopenal + DEFINES += -DHAVE_AL +endif + +ifeq ($(PROXY_TO_PTHREAD), 1) +else ifeq ($(ASYNC), 1) +else + DEFINES += -DEMSCRIPTEN_AUDIO_EXTERNAL_BLOCK + ifeq ($(MIN_ASYNC), 1) + DEFINES += -DEMSCRIPTEN_AUDIO_ASYNC_BLOCK else - DEFINES += -DEMSCRIPTEN_AUDIO_EXTERNAL_BLOCK - ifeq ($(MIN_ASYNC), 1) - DEFINES += -DEMSCRIPTEN_AUDIO_ASYNC_BLOCK - else - DEFINES += -DEMSCRIPTEN_AUDIO_FAKE_BLOCK - endif - ifneq ($(ALLOW_AUDIO_BUSYWAIT), 1) - DEFINES += -DEMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK - endif + DEFINES += -DEMSCRIPTEN_AUDIO_FAKE_BLOCK + endif + ifneq ($(ALLOW_AUDIO_BUSYWAIT), 1) + DEFINES += -DEMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK endif endif @@ -227,16 +230,11 @@ endif # explanation of some of these defines: # EMSCRIPTEN_AUDIO_EXTERNAL_BLOCK: audio blocking occurs in the main loop instead of in the audio driver functions. # EMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK: along with above, enables external blocking in the write function. -# ALLOW_AUDIO_BUSYWAIT: write function will busywait. init function may still use an external block. +# EMSCRIPTEN_AUDIO_BUSYWAIT: write function will busywait. init function may still use an external block. # EMSCRIPTEN_AUDIO_ASYNC_BLOCK: external block uses emscripten_sleep (requires MIN_ASYNC). # EMSCRIPTEN_AUDIO_FAKE_BLOCK: external block uses main loop timing (doesn't require asyncify). # when building with either PROXY_TO_PTHREAD or ASYNC (full asyncify), none of the above are required. -ifeq ($(HAVE_AL), 1) - LDFLAGS += -lopenal - DEFINES += -DHAVE_AL -endif - ifeq ($(HAVE_THREADS), 1) LDFLAGS += -pthread -s PTHREAD_POOL_SIZE=$(PTHREAD_POOL_SIZE) CFLAGS += -pthread -s SHARED_MEMORY diff --git a/audio/drivers/rwebaudio.c b/audio/drivers/rwebaudio.c index 486398781b..189da9f29f 100644 --- a/audio/drivers/rwebaudio.c +++ b/audio/drivers/rwebaudio.c @@ -1,6 +1,7 @@ /* RetroArch - A frontend for libretro. * Copyright (C) 2010-2015 - Michael Lelli * Copyright (C) 2011-2017 - Daniel De Matteis + * Copyright (C) 2025 - OlyB * * RetroArch is free software: you can redistribute it and/or modify it under the terms * of the GNU General Public License as published by the Free Software Found- @@ -17,81 +18,230 @@ #include #include #include +#include #include "../audio_driver.h" +#include "../../verbosity.h" +#include "../../frontend/drivers/platform_emscripten.h" + +#define RWEBAUDIO_BUFFER_SIZE_MS 10 /* forward declarations */ unsigned RWebAudioSampleRate(void); void *RWebAudioInit(unsigned latency); -ssize_t RWebAudioWrite(const void *s, size_t len); +ssize_t RWebAudioQueueBuffer(size_t num_frames, float *left, float *right); bool RWebAudioStop(void); bool RWebAudioStart(void); void RWebAudioSetNonblockState(bool state); void RWebAudioFree(void); -size_t RWebAudioWriteAvail(void); -size_t RWebAudioBufferSize(void); +size_t RWebAudioWriteAvailFrames(void); +size_t RWebAudioBufferSizeFrames(void); +void RWebAudioRecalibrateTime(void); +bool RWebAudioResumeCtx(void); -typedef struct rweb_audio +typedef struct rwebaudio_data { - bool is_paused; -} rweb_audio_t; + size_t tmpbuf_frames; + size_t tmpbuf_offset; + float *tmpbuf_left; + float *tmpbuf_right; + bool nonblock; + bool running; +#ifdef EMSCRIPTEN_AUDIO_FAKE_BLOCK + bool block_requested; +#endif +} rwebaudio_data_t; + +static rwebaudio_data_t *rwebaudio_static_data = NULL; static void rwebaudio_free(void *data) { + rwebaudio_data_t *rwebaudio = (rwebaudio_data_t*)data; + if (!rwebaudio) + return; + RWebAudioFree(); - free(data); + if (rwebaudio->tmpbuf_left) + free(rwebaudio->tmpbuf_left); + if (rwebaudio->tmpbuf_right) + free(rwebaudio->tmpbuf_right); + free(rwebaudio); + rwebaudio_static_data = NULL; } static void *rwebaudio_init(const char *device, unsigned rate, unsigned latency, unsigned block_frames, unsigned *new_rate) { - rweb_audio_t *rwebaudio = (rweb_audio_t*)calloc(1, sizeof(rweb_audio_t)); + rwebaudio_data_t *rwebaudio; + if (rwebaudio_static_data) + { + RARCH_ERR("[RWebAudio] Tried to start already running driver!\n"); + return NULL; + } + + rwebaudio = (rwebaudio_data_t*)calloc(1, sizeof(rwebaudio_data_t)); if (!rwebaudio) return NULL; - if (RWebAudioInit(latency)) - *new_rate = RWebAudioSampleRate(); + if (!RWebAudioInit(latency)) + { + RARCH_ERR("[RWebAudio] Failed to initialize driver!\n"); + return NULL; + } + rwebaudio_static_data = rwebaudio; + *new_rate = RWebAudioSampleRate(); + rwebaudio->tmpbuf_frames = RWEBAUDIO_BUFFER_SIZE_MS * *new_rate / 1000; + rwebaudio->tmpbuf_left = memalign(sizeof(float), rwebaudio->tmpbuf_frames * sizeof(float)); + rwebaudio->tmpbuf_right = memalign(sizeof(float), rwebaudio->tmpbuf_frames * sizeof(float)); + RARCH_LOG("[RWebAudio] Device rate: %d Hz.\n", *new_rate); + RARCH_LOG("[RWebAudio] Buffer size: %lu bytes.\n", RWebAudioBufferSizeFrames() * 2 * sizeof(float)); return rwebaudio; } static ssize_t rwebaudio_write(void *data, const void *s, size_t len) { - return RWebAudioWrite(s, len); + rwebaudio_data_t *rwebaudio = (rwebaudio_data_t*)data; + const float *samples = (const float*)s; + size_t num_frames = len / 2 / sizeof(float); + size_t written = 0; + if (!rwebaudio) + return -1; + + while (num_frames) + { + rwebaudio->tmpbuf_left[rwebaudio->tmpbuf_offset] = *(samples++); + rwebaudio->tmpbuf_right[rwebaudio->tmpbuf_offset] = *(samples++); + num_frames--; + if (++rwebaudio->tmpbuf_offset == rwebaudio->tmpbuf_frames) + { + size_t queued = RWebAudioQueueBuffer(rwebaudio->tmpbuf_frames, rwebaudio->tmpbuf_left, rwebaudio->tmpbuf_right); + rwebaudio->tmpbuf_offset = 0; + /* fast-forward or context is suspended */ + if (queued < rwebaudio->tmpbuf_frames) + break; + written += queued; + } + } + + if (rwebaudio->nonblock) + return written; + +#ifdef EMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK +#ifdef EMSCRIPTEN_AUDIO_FAKE_BLOCK + if (RWebAudioWriteAvailFrames() == 0) + { + rwebaudio->block_requested = true; + platform_emscripten_enter_fake_block(1); + } +#endif + /* async external block doesn't need to do anything else */ +#else + while (RWebAudioWriteAvailFrames() == 0) + { +#ifdef EMSCRIPTEN_FULL_ASYNCIFY + retro_sleep(1); +#endif + RWebAudioResumeCtx(); + } +#endif + + return written; +} + +#ifdef EMSCRIPTEN_AUDIO_EXTERNAL_BLOCK +/* returns true if fake block should continue */ +bool rwebaudio_external_block(void) +{ + rwebaudio_data_t *rwebaudio = rwebaudio_static_data; + + if (!rwebaudio) + return false; + +#ifdef EMSCRIPTEN_AUDIO_FAKE_BLOCK + if (!rwebaudio->block_requested) + return false; +#endif + +#ifdef EMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK + while (!rwebaudio->nonblock && RWebAudioWriteAvailFrames() == 0) + { + RWebAudioResumeCtx(); +#ifdef EMSCRIPTEN_AUDIO_ASYNC_BLOCK + retro_sleep(1); +#else + return true; +#endif + } +#endif + +#ifdef EMSCRIPTEN_AUDIO_FAKE_BLOCK + rwebaudio->block_requested = false; + platform_emscripten_exit_fake_block(); + return true; /* return to RAF if needed */ +#endif + return false; +} +#endif + +void rwebaudio_recalibrate_time(void) +{ + if (rwebaudio_static_data) + RWebAudioRecalibrateTime(); } static bool rwebaudio_stop(void *data) { - rweb_audio_t *rwebaudio = (rweb_audio_t*)data; + rwebaudio_data_t *rwebaudio = (rwebaudio_data_t*)data; if (!rwebaudio) return false; - rwebaudio->is_paused = true; + rwebaudio->running = false; return RWebAudioStop(); } -static void rwebaudio_set_nonblock_state(void *data, bool state) -{ - RWebAudioSetNonblockState(state); -} - -static bool rwebaudio_alive(void *data) -{ - rweb_audio_t *rwebaudio = (rweb_audio_t*)data; - if (!rwebaudio) - return false; - return !rwebaudio->is_paused; -} - static bool rwebaudio_start(void *data, bool is_shutdown) { - rweb_audio_t *rwebaudio = (rweb_audio_t*)data; + rwebaudio_data_t *rwebaudio = (rwebaudio_data_t*)data; if (!rwebaudio) return false; - rwebaudio->is_paused = false; + rwebaudio->running = true; return RWebAudioStart(); } -static size_t rwebaudio_write_avail(void *data) {return RWebAudioWriteAvail();} -static size_t rwebaudio_buffer_size(void *data) {return RWebAudioBufferSize();} +static bool rwebaudio_alive(void *data) +{ + rwebaudio_data_t *rwebaudio = (rwebaudio_data_t*)data; + if (!rwebaudio) + return false; + return rwebaudio->running; +} + +static void rwebaudio_set_nonblock_state(void *data, bool state) +{ + rwebaudio_data_t *rwebaudio = (rwebaudio_data_t*)data; + if (!rwebaudio) + return; + rwebaudio->nonblock = state; + RWebAudioSetNonblockState(state); +} + +static size_t rwebaudio_write_avail(void *data) +{ + rwebaudio_data_t *rwebaudio = (rwebaudio_data_t*)data; + size_t avail_frames; + if (!rwebaudio) + return 0; + + avail_frames = RWebAudioWriteAvailFrames(); + if (avail_frames > rwebaudio->tmpbuf_offset) + return (avail_frames - rwebaudio->tmpbuf_offset) * 2 * sizeof(float); + return 0; +} + +static size_t rwebaudio_buffer_size(void *data) +{ + return RWebAudioBufferSizeFrames() * 2 * sizeof(float); +} + static bool rwebaudio_use_float(void *data) { return true; } audio_driver_t audio_rwebaudio = { diff --git a/config.def.h b/config.def.h index dda01137e7..faee2e89ca 100644 --- a/config.def.h +++ b/config.def.h @@ -1176,7 +1176,7 @@ /* Desired audio latency in milliseconds. Might not be honored * if driver can't provide given latency. */ -#if defined(ANDROID) || defined(RETROFW) || defined(MIYOO) || (defined(EMSCRIPTEN) && !defined(HAVE_AUDIOWORKLET)) +#if defined(ANDROID) || defined(RETROFW) || defined(MIYOO) || (defined(EMSCRIPTEN) && defined(HAVE_AL)) /* For most Android devices, 64ms is way too low. */ #define DEFAULT_OUT_LATENCY 128 #define DEFAULT_IN_LATENCY 128 @@ -1682,7 +1682,7 @@ #if defined(__QNX__) || defined(_XBOX1) || defined(_XBOX360) || (defined(__MACH__) && defined(IOS)) || defined(ANDROID) || defined(WIIU) || defined(HAVE_NEON) || defined(GEKKO) || defined(__ARM_NEON__) || defined(__PS3__) #define DEFAULT_AUDIO_RESAMPLER_QUALITY_LEVEL RESAMPLER_QUALITY_LOWER -#elif defined(PSP) || defined(_3DS) || defined(VITA) || defined(PS2) || defined(DINGUX) || defined(EMSCRIPTEN) +#elif defined(PSP) || defined(_3DS) || defined(VITA) || defined(PS2) || defined(DINGUX) #define DEFAULT_AUDIO_RESAMPLER_QUALITY_LEVEL RESAMPLER_QUALITY_LOWEST #else #define DEFAULT_AUDIO_RESAMPLER_QUALITY_LEVEL RESAMPLER_QUALITY_NORMAL diff --git a/configuration.c b/configuration.c index f5a1c9a6f5..d3917d2170 100644 --- a/configuration.c +++ b/configuration.c @@ -590,7 +590,7 @@ static const enum microphone_driver_enum MICROPHONE_DEFAULT_DRIVER = MICROPHONE_ #if defined(RS90) || defined(MIYOO) static const enum audio_resampler_driver_enum AUDIO_DEFAULT_RESAMPLER_DRIVER = AUDIO_RESAMPLER_NEAREST; -#elif defined(PSP) || defined(EMSCRIPTEN) +#elif defined(PSP) || (defined(EMSCRIPTEN) && defined(HAVE_CC_RESAMPLER)) static const enum audio_resampler_driver_enum AUDIO_DEFAULT_RESAMPLER_DRIVER = AUDIO_RESAMPLER_CC; #else static const enum audio_resampler_driver_enum AUDIO_DEFAULT_RESAMPLER_DRIVER = AUDIO_RESAMPLER_SINC; diff --git a/emscripten/library_rwebaudio.js b/emscripten/library_rwebaudio.js index e7fa3dd76f..1599a50549 100644 --- a/emscripten/library_rwebaudio.js +++ b/emscripten/library_rwebaudio.js @@ -1,140 +1,79 @@ //"use strict"; var LibraryRWebAudio = { - $RA__deps: ['$Browser'], - $RA: { - BUFFER_SIZE: 2048, - + $RWA: { + /* add 10 ms of silence on start, seems to prevent underrun on start/unpausing (terrible crackling in firefox) */ + MIN_START_OFFSET_SEC: 0.01, + /* firefox needs more latency (transparent to audio driver) */ + EXTRA_LATENCY_FIREFOX_SEC: 0.01, + PLATFORM_EMSCRIPTEN_BROWSER_FIREFOX: 2, context: null, - buffers: [], - numBuffers: 0, - bufIndex: 0, - bufOffset: 0, - startTime: 0, + contextRunning: false, nonblock: false, - currentTimeWorkaround: false, - - setStartTime: function() { - if (RA.context.currentTime) { - RA.startTime = window['performance']['now']() - RA.context.currentTime * 1000; - Module["resumeMainLoop"](); - } else window['setTimeout'](RA.setStartTime, 0); - }, - - getCurrentPerfTime: function() { - if (RA.startTime) return (window['performance']['now']() - RA.startTime) / 1000; - else return 0; - }, - - process: function(queueBuffers) { - var currentTime = RA.getCurrentPerfTime(); - for (var i = 0; i < RA.bufIndex; i++) { - if (RA.buffers[i].endTime !== 0 && RA.buffers[i].endTime < currentTime) { - RA.buffers[i].endTime = 0; - var buf = RA.buffers.splice(i, 1); - RA.buffers[RA.numBuffers - 1] = buf[0]; - i--; - RA.bufIndex--; - } else if (!RA.startTime) { - RA.setStartTime(); - } - } - }, - - fillBuffer: function(buf, samples) { - var count = 0; - const leftBuffer = RA.buffers[RA.bufIndex].getChannelData(0); - const rightBuffer = RA.buffers[RA.bufIndex].getChannelData(1); - while (samples && RA.bufOffset !== RA.BUFFER_SIZE) { - leftBuffer[RA.bufOffset] = {{{ makeGetValue('buf', 'count * 8', 'float') }}}; - rightBuffer[RA.bufOffset] = {{{ makeGetValue('buf', 'count * 8 + 4', 'float') }}}; - RA.bufOffset++; - count++; - samples--; - } - - return count; - }, - - queueAudio: function() { - var index = RA.bufIndex; - - var startTime; - if (RA.bufIndex) startTime = RA.buffers[RA.bufIndex - 1].endTime; - else startTime = RA.context.currentTime; - RA.buffers[index].endTime = startTime + RA.buffers[index].duration; - - const bufferSource = RA.context.createBufferSource(); - bufferSource.buffer = RA.buffers[index]; - bufferSource.connect(RA.context.destination); - bufferSource.start(startTime); - - RA.bufIndex++; - RA.bufOffset = 0; - }, - - block: function() { - do { - RA.process(); - } while (RA.bufIndex === RA.numBuffers); - } + endTime: 0, + latency: 0, + virtualBufferFrames: 0, + currentTimeDiff: 0, + extraLatencySec: 0 }, - RWebAudioInit: function(latency) { - var ac = window['AudioContext'] || window['webkitAudioContext']; + /* AudioContext.currentTime can be inaccurate: https://bugzilla.mozilla.org/show_bug.cgi?id=901247 */ + $RWebAudioGetCurrentTime: function() { + return performance.now() / 1000 - RWA.currentTimeDiff; + }, + $RWebAudioStateChangeCB: function() { + RWA.contextRunning = RWA.context.state == "running"; + }, + + RWebAudioResumeCtx: function() { + if (!RWA.contextRunning) RWA.context.resume(); + return RWA.contextRunning; + }, + + RWebAudioInit__deps: ["$RWebAudioStateChangeCB", "platform_emscripten_get_browser"], + RWebAudioInit: function(latency) { + var ac = window.AudioContext || window.webkitAudioContext; if (!ac) return 0; - RA.context = new ac(); + RWA.context = new ac(); + RWA.currentTimeDiff = performance.now() / 1000 - RWA.context.currentTime; + RWA.nonblock = false; + RWA.endTime = 0; + RWA.latency = latency; + RWA.virtualBufferFrames = Math.round(RWA.latency * RWA.context.sampleRate / 1000); + RWA.context.addEventListener("statechange", RWebAudioStateChangeCB); + RWebAudioStateChangeCB(); + RWA.extraLatencySec = (_platform_emscripten_get_browser() == RWA.PLATFORM_EMSCRIPTEN_BROWSER_FIREFOX) ? RWA.EXTRA_LATENCY_FIREFOX_SEC : 0; - RA.numBuffers = ((latency * RA.context.sampleRate) / (1000 * RA.BUFFER_SIZE))|0; - if (RA.numBuffers < 2) RA.numBuffers = 2; - - for (var i = 0; i < RA.numBuffers; i++) { - RA.buffers[i] = RA.context.createBuffer(2, RA.BUFFER_SIZE, RA.context.sampleRate); - RA.buffers[i].endTime = 0 - } - - RA.nonblock = false; - RA.startTime = 0; - // chrome hack to get currentTime running - RA.context.createGain(); - window['setTimeout'](RA.setStartTime, 0); - Module["pauseMainLoop"](); return 1; }, RWebAudioSampleRate: function() { - return RA.context.sampleRate; + return RWA.context.sampleRate; }, - RWebAudioWrite: function (buf, size) { - RA.process(); - var samples = size / 8; - var count = 0; + RWebAudioQueueBuffer__deps: ["$RWebAudioGetCurrentTime", "RWebAudioResumeCtx", "RWebAudioWriteAvailFrames"], + RWebAudioQueueBuffer: function(num_frames, left, right) { + if (RWA.nonblock && _RWebAudioWriteAvailFrames() < num_frames) return 0; + if (!_RWebAudioResumeCtx()) return 0; - while (samples) { - if (RA.bufIndex === RA.numBuffers) { - if (RA.nonblock) break; - else RA.block(); - } + var buffer = RWA.context.createBuffer(2, num_frames, RWA.context.sampleRate); + buffer.getChannelData(0).set(HEAPF32.subarray(left >> 2, (left >> 2) + num_frames)); + buffer.getChannelData(1).set(HEAPF32.subarray(right >> 2, (right >> 2) + num_frames)); + var bufferSource = RWA.context.createBufferSource(); + bufferSource.buffer = buffer; + bufferSource.connect(RWA.context.destination); - var fill = RA.fillBuffer(buf, samples); - samples -= fill; - count += fill; - buf += fill * 8; + var currentTime = RWebAudioGetCurrentTime(); + var startTime = RWA.endTime > currentTime ? RWA.endTime : currentTime + RWA.MIN_START_OFFSET_SEC; + RWA.endTime = startTime + buffer.duration; + bufferSource.start(startTime + RWA.extraLatencySec); - if (RA.bufOffset === RA.BUFFER_SIZE) { - RA.queueAudio(); - } - } - - return count * 8; + return num_frames; }, RWebAudioStop: function() { - RA.bufIndex = 0; - RA.bufOffset = 0; return true; }, @@ -143,29 +82,32 @@ var LibraryRWebAudio = { }, RWebAudioSetNonblockState: function(state) { - RA.nonblock = state; + RWA.nonblock = state; }, + RWebAudioFree__deps: ["$RWebAudioStateChangeCB"], RWebAudioFree: function() { - RA.bufIndex = 0; - RA.bufOffset = 0; + RWA.context.removeEventListener("statechange", RWebAudioStateChangeCB); + RWA.context.close(); + RWA.contextRunning = false; }, - RWebAudioBufferSize: function() { - return RA.numBuffers * RA.BUFFER_SIZE * 8; + RWebAudioBufferSizeFrames: function() { + return RWA.virtualBufferFrames; }, - RWebAudioWriteAvail: function() { - RA.process(); - return ((RA.numBuffers - RA.bufIndex) * RA.BUFFER_SIZE - RA.bufOffset) * 8; + RWebAudioWriteAvailFrames__deps: ["$RWebAudioGetCurrentTime"], + RWebAudioWriteAvailFrames: function() { + var avail = Math.round(((RWA.latency / 1000) - RWA.endTime + RWebAudioGetCurrentTime()) * RWA.context.sampleRate); + if (avail <= 0) return 0; + if (avail >= RWA.virtualBufferFrames) return RWA.virtualBufferFrames; + return avail; }, RWebAudioRecalibrateTime: function() { - if (RA.startTime) { - RA.startTime = window['performance']['now']() - RA.context.currentTime * 1000; - } + if (RWA.contextRunning) RWA.currentTimeDiff = performance.now() / 1000 - RWA.context.currentTime; } }; -autoAddDeps(LibraryRWebAudio, '$RA'); +autoAddDeps(LibraryRWebAudio, '$RWA'); addToLibrary(LibraryRWebAudio); diff --git a/frontend/drivers/platform_emscripten.c b/frontend/drivers/platform_emscripten.c index 778622808f..f16d47829b 100644 --- a/frontend/drivers/platform_emscripten.c +++ b/frontend/drivers/platform_emscripten.c @@ -532,6 +532,16 @@ void platform_emscripten_set_canvas_size(int width, int height) PlatformEmscriptenSetCanvasSize(width, height); } +enum platform_emscripten_browser platform_emscripten_get_browser(void) +{ + return emscripten_platform_data->browser; +} + +enum platform_emscripten_os platform_emscripten_get_os(void) +{ + return emscripten_platform_data->os; +} + /* frontend driver impl */ static void frontend_emscripten_get_env(int *argc, char *argv[], diff --git a/frontend/drivers/platform_emscripten.h b/frontend/drivers/platform_emscripten.h index 58dfa27946..ae2204f57b 100644 --- a/frontend/drivers/platform_emscripten.h +++ b/frontend/drivers/platform_emscripten.h @@ -184,4 +184,18 @@ void platform_emscripten_set_wake_lock(bool state); */ void platform_emscripten_set_canvas_size(int width, int height); +/** + * Get the browser that the program is running in. + * + * @return enum platform_emscripten_browser + */ +enum platform_emscripten_browser platform_emscripten_get_browser(void); + +/** + * Get the OS that the program is running in. + * + * @return enum platform_emscripten_os + */ +enum platform_emscripten_os platform_emscripten_get_os(void); + #endif diff --git a/retroarch.c b/retroarch.c index 65f455c8fd..6bda54ec09 100644 --- a/retroarch.c +++ b/retroarch.c @@ -6008,11 +6008,17 @@ int rarch_main(int argc, char *argv[], void *data) #if defined(EMSCRIPTEN) -#if defined(EMSCRIPTEN_AUDIO_EXTERNAL_BLOCK) && defined(HAVE_AUDIOWORKLET) +#ifdef EMSCRIPTEN_AUDIO_EXTERNAL_BLOCK +#ifdef HAVE_AUDIOWORKLET bool audioworklet_external_block(void); #endif #ifdef HAVE_RWEBAUDIO -void RWebAudioRecalibrateTime(void); +bool rwebaudio_external_block(void); +#endif +#endif + +#ifdef HAVE_RWEBAUDIO +void rwebaudio_recalibrate_time(void); #endif void emscripten_mainloop(void) @@ -6037,13 +6043,19 @@ void emscripten_mainloop(void) if (platform_emscripten_should_drop_iter()) return; -#if defined(EMSCRIPTEN_AUDIO_FAKE_BLOCK) && defined(HAVE_AUDIOWORKLET) +#ifdef HAVE_RWEBAUDIO + rwebaudio_recalibrate_time(); +#endif + +#ifdef EMSCRIPTEN_AUDIO_FAKE_BLOCK +#ifdef HAVE_AUDIOWORKLET if (audioworklet_external_block()) return; #endif - #ifdef HAVE_RWEBAUDIO - RWebAudioRecalibrateTime(); + if (rwebaudio_external_block()) + return; +#endif #endif emscripten_frame_count++; @@ -6068,8 +6080,13 @@ void emscripten_mainloop(void) ret = runloop_iterate(); -#if defined(EMSCRIPTEN_AUDIO_ASYNC_BLOCK) && defined(HAVE_AUDIOWORKLET) +#ifdef EMSCRIPTEN_AUDIO_ASYNC_BLOCK +#ifdef HAVE_AUDIOWORKLET audioworklet_external_block(); +#endif +#ifdef HAVE_RWEBAUDIO + rwebaudio_external_block(); +#endif #endif task_queue_check(); From 86308ef670c53c6b433b689165b15ae894d6d15f Mon Sep 17 00:00:00 2001 From: BinBashBanana <51469593+BinBashBanana@users.noreply.github.com> Date: Sun, 25 May 2025 12:42:41 -0700 Subject: [PATCH 2/2] RWebAudio scheduling adjustments --- emscripten/library_rwebaudio.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/emscripten/library_rwebaudio.js b/emscripten/library_rwebaudio.js index 1599a50549..d97bfead0b 100644 --- a/emscripten/library_rwebaudio.js +++ b/emscripten/library_rwebaudio.js @@ -4,9 +4,10 @@ var LibraryRWebAudio = { $RWA: { /* add 10 ms of silence on start, seems to prevent underrun on start/unpausing (terrible crackling in firefox) */ MIN_START_OFFSET_SEC: 0.01, - /* firefox needs more latency (transparent to audio driver) */ - EXTRA_LATENCY_FIREFOX_SEC: 0.01, - PLATFORM_EMSCRIPTEN_BROWSER_FIREFOX: 2, + /* firefox and safari need more latency (transparent to audio driver) */ + EXTRA_LATENCY_SEC_NONCHROME: 0.01, + EXTRA_LATENCY_SEC_CHROME: 0, + PLATFORM_EMSCRIPTEN_BROWSER_CHROMIUM: 1, context: null, contextRunning: false, nonblock: false, @@ -44,7 +45,7 @@ var LibraryRWebAudio = { RWA.virtualBufferFrames = Math.round(RWA.latency * RWA.context.sampleRate / 1000); RWA.context.addEventListener("statechange", RWebAudioStateChangeCB); RWebAudioStateChangeCB(); - RWA.extraLatencySec = (_platform_emscripten_get_browser() == RWA.PLATFORM_EMSCRIPTEN_BROWSER_FIREFOX) ? RWA.EXTRA_LATENCY_FIREFOX_SEC : 0; + RWA.extraLatencySec = (_platform_emscripten_get_browser() == RWA.PLATFORM_EMSCRIPTEN_BROWSER_CHROMIUM) ? RWA.EXTRA_LATENCY_SEC_CHROME : RWA.EXTRA_LATENCY_SEC_NONCHROME; return 1; }, @@ -66,7 +67,8 @@ var LibraryRWebAudio = { bufferSource.connect(RWA.context.destination); var currentTime = RWebAudioGetCurrentTime(); - var startTime = RWA.endTime > currentTime ? RWA.endTime : currentTime + RWA.MIN_START_OFFSET_SEC; + /* when empty, start rounded up to nearest 1 ms, add MIN_START_OFFSET_SEC */ + var startTime = RWA.endTime > currentTime ? RWA.endTime : Math.ceil(currentTime * 1000) / 1000 + RWA.MIN_START_OFFSET_SEC; RWA.endTime = startTime + buffer.duration; bufferSource.start(startTime + RWA.extraLatencySec);