Skip to content

Commit 8f61b61

Browse files
committed
webm_core: play Opus and Vorbis audio tracks
Feed the audio track through the audio_transfer demuxed path: at load, the existing frame-rate pre-scan also counts audio packets, a second scan copies them into one contiguous blob with per-packet sizes (they alias the file buffer but are interleaved with video), and the track's CodecPrivate becomes the decoder setup blob -- OpusHead for ropus, the xiph-laced headers for rvorbis. Emission is paced by the presented video frame's timestamp plus one nominal frame of lead, which keeps A/V in sync from a single clock and self-corrects instead of accumulating drift from the estimated frame rate. At video EOF the audio tail drains one frame interval per run so the frontend's audio pacing keeps throttling, and shutdown waits for the drain. Mono decodes are upmixed to the stereo callback; the reported sample rate follows the track (Opus is always 48 kHz). For Opus the exact playable length is computed as the TOC packet- duration sum minus the OpusHead pre-skip (dropped inside the decoder arm) minus the container's DiscardPadding, and emission is clamped to it; decoded output was verified byte-exact against a libopus 1.4 FIXED_POINT build over an ffmpeg-muxed VP9+Opus file, including the trimmed tail. Vorbis output was verified within 1 LSB of libvorbis (rvorbis rounds differently), at the track's own rate. retro_reset rewinds the audio path too and replays byte-identically; files without a usable audio track keep the silence fallback, and builds without the decoders compile out the whole path.
1 parent f91eb12 commit 8f61b61

1 file changed

Lines changed: 243 additions & 4 deletions

File tree

cores/libretro-webm/webm_core.c

Lines changed: 243 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@
66
* or MPV -- consoles and other minimal targets -- where it acts as the
77
* built-in media player fallback for .webm content.
88
*
9-
* Video only for now: audio tracks are ignored and silence is output
10-
* to keep frame pacing driven by the audio path. VP9 superframes and
11-
* invisible (non-shown) VP8/VP9 frames are handled.
9+
* Audio tracks (Opus via ropus, Vorbis via rvorbis) play through the
10+
* audio_transfer demuxed path when the decoders are built in; emission
11+
* is paced by the presented video frame's timestamp, which keeps A/V
12+
* in sync without a separate clock. Files without a supported audio
13+
* track fall back to silence to keep frame pacing driven by the audio
14+
* callback. VP9 superframes and invisible (non-shown) VP8/VP9 frames
15+
* are handled.
1216
*/
1317

1418
#include <stdio.h>
@@ -22,6 +26,11 @@
2226
#include <formats/rwebm.h>
2327
#include <formats/rvp9.h>
2428

29+
#if defined(HAVE_ROPUS) || defined(HAVE_RVORBIS)
30+
#define WEBM_HAVE_AUDIO 1
31+
#include <formats/audio.h>
32+
#endif
33+
2534
#ifdef RARCH_INTERNAL
2635
#include "internal_cores.h"
2736
#define WEBM_CORE_PREFIX(s) libretro_webm_##s
@@ -35,6 +44,32 @@
3544

3645
#define WEBM_AUDIO_RATE 48000
3746

47+
#ifdef WEBM_HAVE_AUDIO
48+
/* Opus packet duration in 48 kHz frames from the TOC (RFC 6716 s3):
49+
* used to compute the exact decodable total so container end trimming
50+
* (DiscardPadding) can clamp emission. */
51+
static int64_t webm_opus_pkt_frames(const uint8_t *d, size_t n)
52+
{
53+
static const int16_t fs[32] = {
54+
480, 960, 1920, 2880, 480, 960, 1920, 2880, /* SILK NB/MB */
55+
480, 960, 1920, 2880, /* SILK WB */
56+
480, 960, 480, 960, /* hybrid */
57+
120, 240, 480, 960, 120, 240, 480, 960, /* CELT NB/WB */
58+
120, 240, 480, 960, 120, 240, 480, 960 /* CELT SWB/FB */
59+
};
60+
int count;
61+
if (!n)
62+
return 0;
63+
switch (d[0] & 3)
64+
{
65+
case 0: count = 1; break;
66+
case 3: count = n >= 2 ? (d[1] & 0x3F) : 0; break;
67+
default: count = 2; break;
68+
}
69+
return (int64_t)fs[d[0] >> 3] * count;
70+
}
71+
#endif
72+
3873
static retro_log_printf_t WEBM_CORE_PREFIX(log_cb);
3974
static retro_video_refresh_t WEBM_CORE_PREFIX(video_cb);
4075
static retro_audio_sample_t WEBM_CORE_PREFIX(audio_cb);
@@ -60,6 +95,19 @@ typedef struct
6095
int eof;
6196
int16_t *silence;
6297
size_t silence_frames;
98+
#ifdef WEBM_HAVE_AUDIO
99+
void *actx; /* audio_transfer context */
100+
enum audio_type_enum atype;
101+
int atrack; /* audio track index, -1 = none */
102+
uint8_t *apkts; /* concatenated audio packets */
103+
uint32_t *asizes;
104+
unsigned ach; /* decoded channels (1 or 2) */
105+
unsigned arate;
106+
int64_t apos; /* frames emitted so far */
107+
int64_t atotal; /* emit clamp; <0 = no clamp */
108+
int aeof;
109+
int64_t vpts_ns; /* pts of the last presented frame */
110+
#endif
63111
} webm_player_t;
64112

65113
static webm_player_t webm_player;
@@ -226,6 +274,12 @@ static void webm_free_player(webm_player_t *p)
226274
#endif
227275
if (p->webm)
228276
rwebm_close(p->webm);
277+
#ifdef WEBM_HAVE_AUDIO
278+
if (p->actx)
279+
audio_transfer_free(p->actx, p->atype);
280+
free(p->apkts);
281+
free(p->asizes);
282+
#endif
229283
free(p->fb);
230284
free(p->silence);
231285
free(p->file_buf);
@@ -268,6 +322,10 @@ void WEBM_CORE_PREFIX(retro_get_system_av_info)(
268322
memset(info, 0, sizeof(*info));
269323
info->timing.fps = p->fps;
270324
info->timing.sample_rate = WEBM_AUDIO_RATE;
325+
#ifdef WEBM_HAVE_AUDIO
326+
if (webm_player.actx)
327+
info->timing.sample_rate = (double)webm_player.arate;
328+
#endif
271329
info->geometry.base_width = p->width;
272330
info->geometry.base_height = p->height;
273331
info->geometry.max_width = p->width;
@@ -325,6 +383,13 @@ void WEBM_CORE_PREFIX(retro_reset)(void)
325383
rvp8_video_close(p->vp8);
326384
p->vp8 = rvp8_video_open();
327385
}
386+
#endif
387+
#ifdef WEBM_HAVE_AUDIO
388+
if (p->actx)
389+
audio_transfer_seek(p->actx, p->atype, 0);
390+
p->apos = 0;
391+
p->aeof = 0;
392+
p->vpts_ns = 0;
328393
#endif
329394
p->eof = 0;
330395
}
@@ -355,18 +420,75 @@ void WEBM_CORE_PREFIX(retro_run)(void)
355420
p->eof = 1;
356421
break;
357422
}
423+
#ifdef WEBM_HAVE_AUDIO
424+
if (presented > 0 && pkt.timestamp > 0)
425+
p->vpts_ns = pkt.timestamp;
426+
#endif
358427
}
359428
if (!presented)
360429
WEBM_CORE_PREFIX(video_cb)(p->fb, p->width, p->height,
361430
p->width * sizeof(uint32_t));
362431
}
363432
else
364433
{
434+
int audio_done = 1;
435+
#ifdef WEBM_HAVE_AUDIO
436+
if (p->actx && !p->aeof)
437+
audio_done = 0;
438+
#endif
365439
WEBM_CORE_PREFIX(video_cb)(p->fb, p->width, p->height,
366440
p->width * sizeof(uint32_t));
367-
WEBM_CORE_PREFIX(environ_cb)(RETRO_ENVIRONMENT_SHUTDOWN, NULL);
441+
if (audio_done)
442+
WEBM_CORE_PREFIX(environ_cb)(RETRO_ENVIRONMENT_SHUTDOWN, NULL);
368443
}
369444

445+
#ifdef WEBM_HAVE_AUDIO
446+
if (p->actx && !p->aeof)
447+
{
448+
/* Emit decoded audio up to the presented frame's timestamp plus
449+
* one nominal frame of lead; at video EOF, drain about a frame
450+
* interval per run so the frontend's audio pacing continues to
451+
* throttle us through the tail. */
452+
int16_t buf[1024 * 2];
453+
int16_t st[1024 * 2];
454+
int64_t target = (int64_t)(((double)p->vpts_ns * 1e-9 + 1.0 / p->fps)
455+
* (double)p->arate);
456+
if (p->eof)
457+
target = p->apos + (int64_t)((double)p->arate / p->fps) + 1;
458+
if (p->atotal >= 0 && target > p->atotal)
459+
target = p->atotal;
460+
if (p->atotal >= 0 && p->apos >= p->atotal)
461+
p->aeof = 1;
462+
while (p->apos < target)
463+
{
464+
size_t want = (size_t)(target - p->apos);
465+
size_t got = 0;
466+
int r;
467+
if (want > 1024)
468+
want = 1024;
469+
r = audio_transfer_read_s16(p->actx, p->atype, buf, want, &got);
470+
if (got)
471+
{
472+
const int16_t *out = buf;
473+
if (p->ach == 1)
474+
{
475+
size_t i2;
476+
for (i2 = 0; i2 < got; i2++)
477+
st[2 * i2] = st[2 * i2 + 1] = buf[i2];
478+
out = st;
479+
}
480+
WEBM_CORE_PREFIX(audio_batch_cb)(out, got);
481+
p->apos += (int64_t)got;
482+
}
483+
if (r != AUDIO_PROCESS_NEXT || !got)
484+
{
485+
p->aeof = 1;
486+
break;
487+
}
488+
}
489+
}
490+
else
491+
#endif
370492
WEBM_CORE_PREFIX(audio_batch_cb)(p->silence, p->silence_frames);
371493
}
372494

@@ -421,6 +543,30 @@ bool WEBM_CORE_PREFIX(retro_load_game)(const struct retro_game_info *info)
421543
}
422544
#endif
423545
}
546+
#ifdef WEBM_HAVE_AUDIO
547+
p->atrack = -1;
548+
for (i = 0; i < rwebm_num_tracks(p->webm); i++)
549+
{
550+
const rwebm_track *t = rwebm_get_track(p->webm, i);
551+
if (t->type != RWEBM_TRACK_AUDIO || p->atrack >= 0)
552+
continue;
553+
#ifdef HAVE_ROPUS
554+
if (t->codec == RWEBM_CODEC_OPUS && t->codec_private_size)
555+
{
556+
p->atrack = i;
557+
p->atype = AUDIO_TYPE_OPUS;
558+
}
559+
#endif
560+
#ifdef HAVE_RVORBIS
561+
if (t->codec == RWEBM_CODEC_VORBIS && t->codec_private_size
562+
&& p->atrack < 0)
563+
{
564+
p->atrack = i;
565+
p->atype = AUDIO_TYPE_VORBIS;
566+
}
567+
#endif
568+
}
569+
#endif
424570
if (p->vtrack < 0 || !vt || !vt->width || !vt->height)
425571
{
426572
if (WEBM_CORE_PREFIX(log_cb))
@@ -440,8 +586,19 @@ bool WEBM_CORE_PREFIX(retro_load_game)(const struct retro_game_info *info)
440586
nvpkts = 0;
441587
{
442588
rwebm_packet pkt;
589+
#ifdef WEBM_HAVE_AUDIO
590+
size_t napkts = 0, abytes = 0;
591+
#endif
443592
while (rwebm_read_packet(p->webm, &pkt) == 1)
444593
{
594+
#ifdef WEBM_HAVE_AUDIO
595+
if (pkt.track == p->atrack)
596+
{
597+
napkts++;
598+
abytes += pkt.size;
599+
continue;
600+
}
601+
#endif
445602
if (pkt.track != p->vtrack)
446603
continue;
447604
if (p->codec == RWEBM_CODEC_VP8
@@ -450,6 +607,88 @@ bool WEBM_CORE_PREFIX(retro_load_game)(const struct retro_game_info *info)
450607
nvpkts++;
451608
}
452609
rwebm_rewind(p->webm);
610+
#ifdef WEBM_HAVE_AUDIO
611+
/* Second pass: copy the audio packets into one contiguous blob
612+
* for the audio_transfer demuxed contract (the packets alias the
613+
* file buffer but are interleaved with video, so they are not
614+
* contiguous in place). */
615+
if (p->atrack >= 0 && napkts)
616+
{
617+
size_t k = 0, off = 0;
618+
p->apkts = (uint8_t*)malloc(abytes ? abytes : 1);
619+
p->asizes = (uint32_t*)malloc(napkts * sizeof(uint32_t));
620+
if (p->apkts && p->asizes)
621+
{
622+
int64_t toc_frames = 0, discard_ns = 0;
623+
while (rwebm_read_packet(p->webm, &pkt) == 1)
624+
{
625+
if (pkt.track != p->atrack)
626+
continue;
627+
memcpy(p->apkts + off, pkt.data, pkt.size);
628+
p->asizes[k++] = (uint32_t)pkt.size;
629+
off += pkt.size;
630+
if (p->atype == AUDIO_TYPE_OPUS)
631+
toc_frames += webm_opus_pkt_frames(pkt.data, pkt.size);
632+
if (pkt.discard_padding > 0)
633+
discard_ns += pkt.discard_padding;
634+
}
635+
rwebm_rewind(p->webm);
636+
/* Exact playable length: decoded total minus the pre-skip
637+
* (dropped inside the decoder arm) minus container end
638+
* trimming. Only computable for Opus, whose TOC encodes
639+
* packet durations; Vorbis emission is left unclamped. */
640+
p->atotal = -1;
641+
if (p->atype == AUDIO_TYPE_OPUS && toc_frames > 0)
642+
{
643+
const rwebm_track *at0 =
644+
rwebm_get_track(p->webm, p->atrack);
645+
int64_t preskip = 0;
646+
if (at0->codec_private_size >= 19)
647+
preskip = at0->codec_private[10]
648+
| ((int64_t)at0->codec_private[11] << 8);
649+
p->atotal = toc_frames - preskip
650+
- (discard_ns * 48000 + 500000000) / 1000000000;
651+
if (p->atotal < 0)
652+
p->atotal = 0;
653+
}
654+
{
655+
const rwebm_track *at = rwebm_get_track(p->webm, p->atrack);
656+
p->actx = audio_transfer_new(p->atype);
657+
if (p->actx
658+
&& audio_transfer_set_demuxed_ptr(p->actx, p->atype,
659+
at->codec_private, at->codec_private_size,
660+
p->apkts, off, p->asizes, k)
661+
&& audio_transfer_start(p->actx, p->atype)
662+
&& audio_transfer_info(p->actx, p->atype,
663+
&p->ach, &p->arate, NULL)
664+
&& p->ach >= 1 && p->ach <= 2 && p->arate)
665+
{
666+
if (WEBM_CORE_PREFIX(log_cb))
667+
WEBM_CORE_PREFIX(log_cb)(RETRO_LOG_INFO,
668+
"[webm] audio: %s, %u Hz, %u ch, %u packets.\n",
669+
p->atype == AUDIO_TYPE_OPUS ? "Opus" : "Vorbis",
670+
p->arate, p->ach, (unsigned)k);
671+
}
672+
else
673+
{
674+
if (WEBM_CORE_PREFIX(log_cb))
675+
WEBM_CORE_PREFIX(log_cb)(RETRO_LOG_WARN,
676+
"[webm] audio track unusable; playing silent.\n");
677+
if (p->actx)
678+
audio_transfer_free(p->actx, p->atype);
679+
p->actx = NULL;
680+
}
681+
}
682+
}
683+
if (!p->actx)
684+
{
685+
free(p->apkts);
686+
free(p->asizes);
687+
p->apkts = NULL;
688+
p->asizes = NULL;
689+
}
690+
}
691+
#endif
453692
}
454693
dur_ns = rwebm_duration_ns(p->webm);
455694
if (nvpkts > 1 && dur_ns > 0)

0 commit comments

Comments
 (0)