Files
RetroArch/audio/drivers/audioworklet.c
T
Anthony SchemelandClaude Opus 5.5 67c857e163 misc: fix crashes and leaks in wayland, drm, replay index and drivers (#19630)
Small, independent fixes for crashes and leaks on reachable paths:

- wayland: gfx_ctx_wl_get_video_size_common dereferenced a NULL output
  when no window output was set and all_outputs was empty. The DnD
  drop handler leaked the received buffer when fmemopen failed, and
  always leaked the line getline allocated.
- drm: drm_plane_setup logged a NULL drmModeGetPlaneResources result
  and then dereferenced it. init_drm read `connector` uninitialised
  when the card reported no connectors.
- uint32s_index (replay statestream): uint32s_bucket_free left the
  freed pointer and length in the bucket. RHMAP_CLEAR keeps value
  slots, so a later RHMAP_PTR on the same slot could see them and free
  the vector again. uint32s_index_pop now also refuses an empty index
  and checks RHMAP_HAS before RHMAP_PTR, which would otherwise create
  a bucket for a missing hash.
- ozone: ozone_context_reset_horizontal_list allocated a node for an
  entry without one but never stored it in the list, so it leaked.
- discord: discord_json_next_strdup leaked the previous string when a
  key repeated.
- core_backup: core_backup_add_entry leaked an empty filename.
- audioworklet, caca, sixel, vga: the init functions leaked their
  struct when the second allocation or the font renderer failed.

Tested on Linux: full build. The touched Linux files show no new
warnings; the C89 build of the Wayland and DRM files fails on this
host in system headers and existing lines, with no diagnostic on a
changed line. drm_gfx.c was compiled although HAVE_PLAIN_DRM is off.
samples/tasks/core_backup passes. The audioworklet, caca, sixel and
vga drivers are not buildable here and were not compiled.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-09-26 16:18:37 +02:00

653 lines
22 KiB
C

/* RetroArch - A frontend for libretro.
* 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-
* ation, either version 3 of the License, or (at your option) any later version.
*
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with RetroArch.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <malloc.h>
#include <boolean.h>
#include <retro_atomic.h>
#include <emscripten/wasm_worker.h>
#include <emscripten/webaudio.h>
#include <emscripten/atomic.h>
#include "../../frontend/drivers/platform_emscripten.h"
#include <retro_spsc.h>
#include <retro_timers.h>
#include "../audio_driver.h"
#include "../../verbosity.h"
#define WORKLET_STACK_SIZE 4096
/* additional buffer size (for EMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK only) */
/* if this is too small, frames may be dropped and content could run too fast. */
/* very large slow-motion rate values may be too large for this; avoid anything higher than 6 or 7. */
#define EXTERNAL_BLOCK_BUFFER_MS 128
typedef struct audioworklet_data
{
uint8_t *worklet_stack;
size_t visible_buffer_size;
#ifdef EMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK
size_t write_avail_diff;
#endif
#ifdef PROXY_TO_PTHREAD
emscripten_lock_t trywrite_lock;
emscripten_condvar_t trywrite_cond;
#endif
/* Lifetime only: taken by the worklet around its pull and by
* audioworklet_free() around tearing the ring down, so the worklet
* never reads a freed ring. The writer no longer takes it: the
* ring below is SPSC, so the main thread (producer) and the worklet
* (consumer) do not need to exclude each other, and a worklet that
* failed to get the lock - and played a period of silence - was a
* real symptom on the old fifo. */
emscripten_lock_t buffer_lock;
EMSCRIPTEN_WEBAUDIO_T context;
float *tmpbuf;
/* Outgoing samples: main thread writes, the worklet pulls.
* retro_spsc rounds capacity up to a power of two; ring_size is the
* size asked for (visible buffer plus the external-block reserve)
* and the producer never fills past it, so room is what the fifo
* of that size reported. A ring that failed to init has
* ring_init false and the driver does not come up. */
retro_spsc_t ring;
size_t ring_size;
/* Quanta the worklet had to fill with silence because the ring came
* up short, or because it could not get at the ring at all. Counted
* here and not said: this is the render callback, and a line from
* it costs part of the quantum it is reporting on. */
retro_atomic_size_t underruns;
bool ring_init;
unsigned rate;
unsigned latency;
bool nonblock;
bool initing;
#ifdef EMSCRIPTEN_AUDIO_FAKE_BLOCK
bool block_requested;
#endif
volatile bool running; /* currently only used by RetroArch */
volatile bool driver_running; /* whether the driver is running (buffer allocated) */
volatile bool context_running; /* whether the AudioContext is running */
volatile bool init_done;
volatile bool init_error;
} audioworklet_data_t;
/* We only ever want to create 1 worklet, so we need to keep its data even if the driver is inactive. */
static audioworklet_data_t *audioworklet_static_data = NULL;
/* Room in the ring against the size asked for. Producer-side query -
* write(), write_avail() and the external-block waits all run on the
* main thread - so it is exact for the caller: the worklet only ever
* frees space, never takes it. */
static size_t audioworklet_room(const audioworklet_data_t *audioworklet)
{
size_t room = retro_spsc_write_avail(&audioworklet->ring);
size_t excess = audioworklet->ring.capacity - audioworklet->ring_size;
return room > excess ? room - excess : 0;
}
/* Note that we cannot allocate any heap in here. */
static bool audioworklet_process_cb(int numInputs, const AudioSampleFrame *inputs,
int numOutputs, AudioSampleFrame *outputs,
int numParams, const AudioParamFrame *params,
void *data)
{
audioworklet_data_t *audioworklet = (audioworklet_data_t*)data;
size_t max_read;
unsigned writing_frames = 0;
int i;
/* TODO: do we need to pay attention to audioworklet->running here too? */
if (audioworklet->driver_running)
{
/* can't use Atomics.wait in AudioWorklet */
/* busyspin is safe as of emscripten 4.0.4 */
if (!emscripten_lock_busyspin_wait_acquire(&audioworklet->buffer_lock, 2.5))
{
/* The output arrays arrive zeroed, so this quantum goes out
* as silence. */
retro_atomic_fetch_add_size(&audioworklet->underruns, 1);
return true;
}
/* The lock is only against free(); the producer never holds it,
* so this no longer waits on a write in progress. */
max_read = retro_spsc_read(&audioworklet->ring, audioworklet->tmpbuf,
outputs[0].samplesPerChannel * 2 * sizeof(float));
emscripten_lock_release(&audioworklet->buffer_lock);
#ifdef PROXY_TO_PTHREAD
emscripten_condvar_signal(&audioworklet->trywrite_cond, 1);
#endif
writing_frames = max_read / 2 / sizeof(float);
for (i = 0; i < writing_frames; i++)
{
outputs[0].data[i] = audioworklet->tmpbuf[i * 2];
outputs[0].data[outputs[0].samplesPerChannel + i] = audioworklet->tmpbuf[i * 2 + 1];
}
}
if (writing_frames < outputs[0].samplesPerChannel)
{
int zero_frames = outputs[0].samplesPerChannel - writing_frames;
/* Only where audio was expected: a driver that is not running is
* not being starved. */
if (audioworklet->driver_running)
retro_atomic_fetch_add_size(&audioworklet->underruns, 1);
memset(outputs[0].data + writing_frames, 0, zero_frames * sizeof(float));
memset(outputs[0].data + writing_frames + outputs[0].samplesPerChannel, 0, zero_frames * sizeof(float));
}
return true;
}
static void audioworklet_processor_inited_cb(EMSCRIPTEN_WEBAUDIO_T context, bool success, void *data)
{
audioworklet_data_t *audioworklet = (audioworklet_data_t*)data;
int outputChannelCounts[1] = { 2 };
EmscriptenAudioWorkletNodeCreateOptions opts = { 0, 1, outputChannelCounts };
EMSCRIPTEN_AUDIO_WORKLET_NODE_T worklet_node;
if (!success)
{
RARCH_ERR("[AudioWorklet] Failed to init AudioWorkletProcessor.\n");
audioworklet->init_error = true;
audioworklet->init_done = true;
return;
}
worklet_node = emscripten_create_wasm_audio_worklet_node(context, "retroarch", &opts, audioworklet_process_cb, audioworklet);
emscripten_audio_node_connect(worklet_node, context, 0, 0);
audioworklet->init_done = true;
}
static void audioworklet_thread_inited_cb(EMSCRIPTEN_WEBAUDIO_T context, bool success, void *data)
{
audioworklet_data_t *audioworklet = (audioworklet_data_t*)data;
WebAudioWorkletProcessorCreateOptions opts = { "retroarch", 0 };
if (success)
emscripten_create_wasm_audio_worklet_processor_async(context, &opts,
audioworklet_processor_inited_cb, audioworklet);
else
{
RARCH_ERR("[AudioWorklet] Failed to init worklet thread! Is the worklet file in the right place?\n");
audioworklet->init_error = true;
audioworklet->init_done = true;
}
}
static void audioworklet_ctx_statechange_cb(void *data, bool state)
{
audioworklet_data_t *audioworklet = (audioworklet_data_t*)data;
audioworklet->context_running = state;
}
static void audioworklet_ctx_create(void *data)
{
audioworklet_data_t *audioworklet = (audioworklet_data_t*)data;
audioworklet->context = emscripten_create_audio_context(0);
audioworklet->tmpbuf = memalign(16, emscripten_audio_context_quantum_size(audioworklet->context) * 2 * sizeof(float));
audioworklet->rate = EM_ASM_INT({
return emscriptenGetAudioObject($0).sampleRate;
}, audioworklet->context);
audioworklet->context_running = EM_ASM_INT({
let ac = emscriptenGetAudioObject($0);
ac.addEventListener("statechange", function() {
getWasmTableEntry($2)($1, ac.state == "running");
});
return ac.state == "running";
}, audioworklet->context, audioworklet, audioworklet_ctx_statechange_cb);
emscripten_start_wasm_audio_worklet_thread_async(audioworklet->context,
audioworklet->worklet_stack, WORKLET_STACK_SIZE, audioworklet_thread_inited_cb, audioworklet);
}
static void audioworklet_alloc_buffer(void *data)
{
size_t buffer_size;
audioworklet_data_t *audioworklet = (audioworklet_data_t*)data;
audioworklet->visible_buffer_size = (audioworklet->latency * audioworklet->rate * 2 * sizeof(float)) / 1000;
buffer_size = audioworklet->visible_buffer_size;
#ifdef EMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK
audioworklet->write_avail_diff = (EXTERNAL_BLOCK_BUFFER_MS * audioworklet->rate * 2 * sizeof(float)) / 1000;
buffer_size += audioworklet->write_avail_diff;
#endif
audioworklet->ring_size = buffer_size;
audioworklet->ring_init = retro_spsc_init(&audioworklet->ring, buffer_size);
if (!audioworklet->ring_init)
RARCH_ERR("[AudioWorklet] Failed to allocate the %lu-byte buffer.\n",
(unsigned long)buffer_size);
RARCH_LOG("[AudioWorklet] Buffer size: %lu bytes.\n", audioworklet->visible_buffer_size);
}
static void audioworklet_init_error(void *data)
{
audioworklet_data_t *audioworklet = (audioworklet_data_t*)data;
RARCH_ERR("[AudioWorklet] Failed to initialize driver.\n");
free(audioworklet->worklet_stack);
free(audioworklet->tmpbuf);
free(audioworklet);
}
static bool audioworklet_resume_ctx(void *data)
{
audioworklet_data_t *audioworklet = (audioworklet_data_t*)data;
if (!audioworklet->context_running)
{
MAIN_THREAD_ASYNC_EM_ASM({
emscriptenGetAudioObject($0).resume();
}, audioworklet->context);
}
return audioworklet->context_running;
}
/* Bounds. The write retries its wait this many times before dropping
* the rest; the busy-wait build spins at most this many resume attempts
* per retry; init waits this long for the worklet module. */
#define AUDIOWORKLET_WAIT_LAPS 8
#define AUDIOWORKLET_BUSYWAIT_SPINS 100000
#define AUDIOWORKLET_INIT_WAIT_MS 30000
#define AUDIOWORKLET_BLOCK_WAIT_MS 2000
static void *audioworklet_init(const char *device, unsigned rate,
unsigned latency, unsigned *new_rate)
{
audioworklet_data_t *audioworklet;
if (audioworklet_static_data)
{
if (audioworklet_static_data->driver_running || audioworklet_static_data->initing)
{
RARCH_ERR("[AudioWorklet] Tried to start already running driver.\n");
return NULL;
}
RARCH_LOG("[AudioWorklet] Reusing old context.\n");
audioworklet = audioworklet_static_data;
audioworklet->latency = latency;
*new_rate = audioworklet->rate;
RARCH_LOG("[AudioWorklet] Device rate: %d Hz.\n", *new_rate);
audioworklet_alloc_buffer(audioworklet);
if (!audioworklet->ring_init)
return NULL;
audioworklet_resume_ctx(audioworklet);
audioworklet->driver_running = true;
return audioworklet;
}
audioworklet = (audioworklet_data_t*)calloc(1, sizeof(audioworklet_data_t));
if (!audioworklet)
return NULL;
audioworklet->worklet_stack = memalign(16, WORKLET_STACK_SIZE);
if (!audioworklet->worklet_stack)
{
free(audioworklet);
return NULL;
}
audioworklet_static_data = audioworklet;
audioworklet->latency = latency;
platform_emscripten_run_on_browser_thread_sync(audioworklet_ctx_create, audioworklet);
*new_rate = audioworklet->rate;
RARCH_LOG("[AudioWorklet] Device rate: %d Hz.\n", *new_rate);
audioworklet->initing = true;
audioworklet_alloc_buffer(audioworklet);
/* No ring, no driver: reported the way a worklet that failed to
* load is, so both the waiting and the external-block init paths
* below take their existing error exits. */
if (!audioworklet->ring_init)
audioworklet->init_error = true;
retro_atomic_size_init(&audioworklet->underruns, 0);
emscripten_lock_init(&audioworklet->buffer_lock);
#ifdef PROXY_TO_PTHREAD
emscripten_lock_init(&audioworklet->trywrite_lock);
emscripten_condvar_init(&audioworklet->trywrite_cond);
#endif
#ifndef EMSCRIPTEN_AUDIO_EXTERNAL_BLOCK
/* TODO: can MIN_ASYNCIFY block here too? */
{
/* The worklet reports done or error once its module has loaded.
* Generous, since that is a fetch, but not unbounded: a module
* that never loads and never errors is an init failure, not a
* frontend that never starts. */
unsigned waited_ms = 0;
while (!audioworklet->init_done)
{
retro_sleep(1);
if (++waited_ms >= AUDIOWORKLET_INIT_WAIT_MS)
{
RARCH_ERR("[AudioWorklet] The worklet did not initialize within %u ms.\n",
AUDIOWORKLET_INIT_WAIT_MS);
audioworklet->init_error = true;
break;
}
}
}
audioworklet->initing = false;
if (audioworklet->init_error)
{
audioworklet_init_error(audioworklet);
return NULL;
}
audioworklet->driver_running = true;
#elif defined(EMSCRIPTEN_AUDIO_FAKE_BLOCK)
audioworklet->block_requested = true;
platform_emscripten_enter_fake_block(1);
#endif
/* external block: will be handled later */
return audioworklet;
}
static ssize_t audioworklet_write(void *data, const void *s, size_t ss)
{
size_t avail;
size_t max_write;
size_t to_write_frames;
size_t to_write_bytes;
size_t _len = 0;
audioworklet_data_t *audioworklet = (audioworklet_data_t*)data;
const float *samples = (const float*)s;
size_t num_frames = ss / 2 / sizeof(float);
int laps = AUDIOWORKLET_WAIT_LAPS;
/* too early! might happen with external blocking */
if (!audioworklet->driver_running)
return 0;
/* don't write audio if the context isn't running, just try to start it */
if (!audioworklet_resume_ctx(audioworklet))
return 0;
while (num_frames)
{
/* No lock: SPSC, and this thread is the producer. */
avail = audioworklet_room(audioworklet);
max_write = avail;
#ifdef EMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK
/* make sure we don't write into the blocking buffer for nonblock */
if (audioworklet->nonblock)
{
if (max_write > audioworklet->write_avail_diff)
max_write -= audioworklet->write_avail_diff;
else
max_write = 0;
}
#endif
to_write_frames = MIN(num_frames, max_write / 2 / sizeof(float));
if (to_write_frames)
{
to_write_bytes = to_write_frames * 2 * sizeof(float);
avail -= to_write_bytes;
retro_spsc_write(&audioworklet->ring, samples, to_write_bytes);
num_frames -= to_write_frames;
samples += (to_write_frames * 2);
_len += to_write_frames;
}
#ifdef EMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK
#ifdef EMSCRIPTEN_AUDIO_FAKE_BLOCK
/* see if we're over the threshold to go to fake block */
if (avail < audioworklet->write_avail_diff)
{
audioworklet->block_requested = true;
platform_emscripten_enter_fake_block(1);
}
#endif
if (num_frames && !audioworklet->nonblock)
RARCH_WARN("[AudioWorklet] Dropping %lu frames.\n", num_frames);
break;
#endif
if (audioworklet->nonblock || !num_frames)
break;
/* Bounded overall, whichever way the wait below is made: a
* context that will not run - suspended by the autoplay policy
* until the page is clicked, or by the browser for a background
* tab - frees nothing, and this returns what went rather than
* holding the thread. On the busy-wait build that thread is the
* page's, and holding it froze the tab. */
if (--laps < 0)
{
RARCH_WARN("[AudioWorklet] Dropping %lu frames: the context is not taking audio.\n",
(unsigned long)num_frames);
break;
}
#if defined(PROXY_TO_PTHREAD)
emscripten_condvar_wait(&audioworklet->trywrite_cond, &audioworklet->trywrite_lock, 3000000);
#elif defined(EMSCRIPTEN_FULL_ASYNCIFY)
retro_sleep(1);
#else /* equivalent to defined(EMSCRIPTEN_AUDIO_BUSYWAIT) */
{
/* The spin cannot sleep on this thread; it can at least end. */
unsigned spins = AUDIOWORKLET_BUSYWAIT_SPINS;
while ( audioworklet_room(audioworklet) < 2 * sizeof(float)
&& spins--)
audioworklet_resume_ctx(audioworklet);
}
#endif
/* try resuming, on the off chance that the context was interrupted while blocking */
audioworklet_resume_ctx(audioworklet);
}
return _len;
}
#ifdef EMSCRIPTEN_AUDIO_EXTERNAL_BLOCK
/* returns true if fake block should continue */
bool audioworklet_external_block(void)
{
audioworklet_data_t *audioworklet = audioworklet_static_data;
if (audioworklet)
{
#ifdef EMSCRIPTEN_AUDIO_FAKE_BLOCK
if (!audioworklet->block_requested)
return false;
#endif
#ifdef EMSCRIPTEN_AUDIO_ASYNC_BLOCK
{
unsigned waited_ms = 0;
while (audioworklet->initing && !audioworklet->init_done)
{
retro_sleep(1);
if (++waited_ms >= AUDIOWORKLET_INIT_WAIT_MS)
{
RARCH_ERR("[AudioWorklet] The worklet did not initialize within %u ms.\n",
AUDIOWORKLET_INIT_WAIT_MS);
audioworklet->init_error = true;
audioworklet->init_done = true;
break;
}
}
}
#else
while (audioworklet->initing && !audioworklet->init_done)
return true;
#endif
if (audioworklet->init_done && !audioworklet->driver_running)
{
audioworklet->initing = false;
if (audioworklet->init_error)
{
audioworklet_init_error(audioworklet);
abort();
return false;
}
audioworklet->driver_running = true;
}
#ifdef EMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK
if (!audioworklet->driver_running)
return false;
#ifdef EMSCRIPTEN_AUDIO_ASYNC_BLOCK
{
/* Bounded: a context that will not run frees nothing, and the
* block ends with the audio dropped rather than never. */
unsigned waited_ms = 0;
while (audioworklet_room(audioworklet) < audioworklet->write_avail_diff)
{
audioworklet_resume_ctx(audioworklet);
retro_sleep(1);
if (++waited_ms >= AUDIOWORKLET_BLOCK_WAIT_MS)
break;
}
}
#else
while (audioworklet_room(audioworklet) < audioworklet->write_avail_diff)
{
audioworklet_resume_ctx(audioworklet);
return true;
}
#endif
#endif
#ifdef EMSCRIPTEN_AUDIO_FAKE_BLOCK
audioworklet->block_requested = false;
platform_emscripten_exit_fake_block();
return true; /* return to RAF if needed */
#endif
}
return false;
}
#endif
/* called on program exit */
void audioworklet_close(void)
{
audioworklet_data_t *audioworklet = audioworklet_static_data;
if (!audioworklet)
return;
MAIN_THREAD_EM_ASM({
emscriptenGetAudioObject($0).close();
}, audioworklet->context);
}
static bool audioworklet_stop(void *data)
{
audioworklet_data_t *audioworklet = (audioworklet_data_t*)data;
audioworklet->running = false;
return true;
}
static bool audioworklet_start(void *data, bool is_shutdown)
{
audioworklet_data_t *audioworklet = (audioworklet_data_t*)data;
audioworklet->running = true;
return true;
}
static bool audioworklet_alive(void *data)
{
audioworklet_data_t *audioworklet = (audioworklet_data_t*)data;
return audioworklet->running;
}
static void audioworklet_set_nonblock_state(void *data, bool state)
{
audioworklet_data_t *audioworklet = (audioworklet_data_t*)data;
audioworklet->nonblock = state;
}
static void audioworklet_free(void *data)
{
audioworklet_data_t *audioworklet = (audioworklet_data_t*)data;
/* that's not good... this shouldn't happen? */
if (!audioworklet->driver_running)
{
RARCH_ERR("[AudioWorklet] Tried to free before done initing.\n");
return;
}
#ifdef PROXY_TO_PTHREAD
if (!emscripten_lock_wait_acquire(&audioworklet->buffer_lock, 10000000))
#else
if (!emscripten_lock_busyspin_wait_acquire(&audioworklet->buffer_lock, 10))
#endif
{
RARCH_ERR("[AudioWorklet] Main thread: could not acquire lock to free buffer.\n");
return;
}
audioworklet->driver_running = false;
if (audioworklet->ring_init)
retro_spsc_free(&audioworklet->ring);
audioworklet->ring_init = false;
emscripten_lock_release(&audioworklet->buffer_lock);
MAIN_THREAD_ASYNC_EM_ASM({
emscriptenGetAudioObject($0).suspend();
}, audioworklet->context);
}
static size_t audioworklet_write_avail(void *data)
{
audioworklet_data_t *audioworklet = (audioworklet_data_t*)data;
size_t avail = audioworklet_room(audioworklet);
#ifdef EMSCRIPTEN_AUDIO_EXTERNAL_WRITE_BLOCK
if (avail > audioworklet->write_avail_diff)
return avail - audioworklet->write_avail_diff;
return 0;
#else
return avail;
#endif
}
static size_t audioworklet_buffer_size(void *data)
{
audioworklet_data_t *audioworklet = (audioworklet_data_t*)data;
return audioworklet->visible_buffer_size;
}
static bool audioworklet_use_float(void *data) { return true; }
static size_t audioworklet_underruns(void *data)
{
audioworklet_data_t *audioworklet = (audioworklet_data_t*)data;
return audioworklet
? retro_atomic_load_acquire_size(&audioworklet->underruns) : 0;
}
audio_driver_t audio_audioworklet = {
audioworklet_init,
audioworklet_write,
audioworklet_stop,
audioworklet_start,
audioworklet_alive,
audioworklet_set_nonblock_state,
audioworklet_free,
audioworklet_use_float,
"audioworklet",
NULL,
NULL,
audioworklet_write_avail,
audioworklet_buffer_size,
NULL, /* write_raw */
NULL, /* wait_writable */
NULL, /* frames_consumed */
audioworklet_underruns
};