formats/wma: WMA1 and WMA2 played gapless

WMA1 and WMA2 carry no gapless signalling, and rwma played them as the
frames came: the encoder's priming frame first (FFmpeg's encoder sets
initial_padding to one frame, as WMA encoders put one in), and without
the last frame's second half, which only the next frame's overlap would
have completed. A track started one frame late - 46 ms at 44.1 kHz -
and lost its last frame. FFmpeg's decoder is no reference here: it
drops two frames at the start, its own and one of the audio.

- rwma_decode_packet()'s drain (no bytes) gives the standard decoder's
  overlap tail once, as FFmpeg's flush does, where it gave nothing;
- rwma_stream drops the first frame of a WMA1 or WMA2 stream, drains
  the tail at the end, and stops at the length the ASF header states
  (rwma_stream_frames), so the stream plays exactly the audio the
  encoder was given. WMA Pro (which signals its own trims) and XMA are
  as they were; a rewind starts the trimming again.

parallel-ps3 needs this for cellMusicDecode to play WMA tracks from
their first sample to their last.

audio_transfer_sniff_test --file gains --source, the decode against the
signal the encoder was given: from its first sample, at its length
within 2 ms (ASF's durations are in milliseconds), 20 dB above the
coding error. CI checks WMA1 and WMA2 from FFmpeg at 44.1, 22.05, 16
and 8 kHz (frames of 2048, 1024 and 512 samples); the old code was a
frame late and 575 samples long at 44.1 kHz. rwma_test checks the drain
gives the tail once, and audio_transfer_wma_test, whose fixture is a
single coded frame, now expects that frame's second half.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-11-11 20:25:00 +01:00
1 parent 100a62c152
commit a2cd6efcb4
6 files changed
+167 -11

No files matched your search

@@ -1093,6 +1093,19 @@ jobs:
-of csv=p=0 "$work/$f" | tr '\n' ' ' | awk '{ printf "%d", $1 * $2 + 0.5 }')
./audio_transfer_sniff_test --file "$work/$f" wma --frames "$frames"
done
# WMA1 and WMA2, gapless at each frame size (2048, 1024 and 512
# samples): the signal FFmpeg's encoder was given, from its
# first sample, at the length the header states
for codec in wmav1 wmav2; do
for rate in 44100 22050 16000 8000; do
src="sine=f=440:d=0.73:sample_rate=$rate"
ffmpeg -nostdin -hide_banner -loglevel error -f lavfi -i "$src" -ac 2 \
-c:a "$codec" -y "$work/g.wma"
ffmpeg -nostdin -hide_banner -loglevel error -f lavfi -i "$src" -ac 2 \
-f f32le -y "$work/g.f32"
./audio_transfer_sniff_test --file "$work/g.wma" wma --source "$work/g.f32"
done
done
six=""
n=0
for hz in 300 500 700 80 1100 1300; do
+14 -1
View File
@@ -628,14 +628,27 @@ int rwma_std_decode(void *opaque, const uint8_t *data, size_t bytes, size_t *use
unsigned step;
if (s->failed)
return RWMA_BAD_DATA;
std_roll(s);
if (!bytes)
{
/* The end of the stream: the last frame's second half, which the
* next frame's overlap would have completed, comes out once */
unsigned c, had = s->pending;
if (s->in_packet)
return RWMA_BAD_DATA;
std_roll(s);
if (had && !s->drained)
{
out->samples = s->length;
out->channels = s->config.channels;
out->sample_rate = s->config.sample_rate;
for (c = 0; c < s->config.channels; c++)
out->planes[c] = s->acc[c];
s->pending = 1;
}
s->drained = 1;
return RWMA_OK;
}
std_roll(s);
if (s->drained || !data || bytes < s->config.block_align ||
(s->in_packet && data != s->packet))
return RWMA_BAD_DATA;
+63 -1
View File
@@ -44,6 +44,10 @@ struct rwma_stream
/* the length ASF states: its play duration less its preroll, in
* 100 ns units */
uint64_t play_100ns;
/* WMA1 and WMA2 played gapless: the encoder's priming frame dropped,
* the last frame's tail drained, the end at the stated length */
unsigned gapless, primed, tail_done;
uint64_t skip_left, emitted;
};
static uint32_t stream_u32(const uint8_t *p)
@@ -284,6 +288,7 @@ int rwma_stream_open(rwma_stream **out, const uint8_t *data, size_t size, size_t
rwma_stream_close(s);
return status;
}
s->gapless = !s->xma_streams && (cfg.codec == RWMA_V1 || cfg.codec == RWMA_V2);
s->channels = cfg.channels;
s->rate = cfg.sample_rate;
s->align = cfg.block_align;
@@ -463,7 +468,7 @@ static int stream_object(rwma_stream *s)
return RWMA_STREAM_WAIT;
}
int rwma_stream_next(rwma_stream *s, rwma_frame *frame)
static int stream_next_raw(rwma_stream *s, rwma_frame *frame)
{
unsigned steps;
if (!frame)
@@ -531,6 +536,61 @@ invalid:
return RWMA_BAD_DATA;
}
/* WMA1 and WMA2 carry no gapless signalling, but their encoders put one
* frame of priming before the audio (FFmpeg's sets initial_padding to a
* frame), and the decoder's output lags by the overlap, the last frame's
* second half coming out only when the stream is drained. So the first
* frame is dropped, the tail drained, and the end cut at the length the
* ASF header states, which is the audio the encoder was given. */
int rwma_stream_next(rwma_stream *s, rwma_frame *frame)
{
for (;;)
{
int status = stream_next_raw(s, frame);
uint64_t total;
unsigned c, n;
if (!s || !s->gapless)
return status;
if (status == RWMA_STREAM_END && !s->tail_done)
{
size_t used = 0;
s->tail_done = 1;
if (rwma_decode_packet(s->decoder, NULL, 0, &used, frame) || !frame->samples)
return RWMA_STREAM_END;
status = RWMA_OK;
}
if (status)
return status;
if (!s->primed)
{
s->primed = 1;
s->skip_left = frame->samples;
}
if (s->skip_left)
{
n = s->skip_left < frame->samples ? (unsigned)s->skip_left : frame->samples;
for (c = 0; c < frame->channels; c++)
frame->planes[c] += n;
frame->samples -= n;
s->skip_left -= n;
if (!frame->samples)
continue;
}
if ((total = rwma_stream_frames(s)) != 0)
{
if (s->emitted >= total)
{
memset(frame, 0, sizeof(*frame));
return RWMA_STREAM_END;
}
if (frame->samples > total - s->emitted)
frame->samples = (unsigned)(total - s->emitted);
}
s->emitted += frame->samples;
return RWMA_OK;
}
}
void rwma_stream_rewind(rwma_stream *s)
{
unsigned i;
@@ -552,6 +612,8 @@ void rwma_stream_rewind(rwma_stream *s)
s->assembled = s->object_pos = s->object_size = 0;
s->compressed_at = s->compressed_end = s->demand = 0;
s->object = NULL;
s->primed = s->tail_done = 0;
s->skip_left = s->emitted = 0;
}
void rwma_stream_set_avail(rwma_stream *s, size_t avail)
{
@@ -6,13 +6,17 @@
* and look-alikes it must turn down; every prefix of each, and each
* byte corrupted, read inside the buffer; the fold tables.
* audio_transfer_sniff_test --file FILE TYPE [--tones] [--pcm REF]
* [--frames N]
* [--frames N] [--source REF]
* FILE sniffed as TYPE (wav flac vorbis opus mp3 aac ac3 wma mod),
* started where the sniffer says and decoded to the end. With
* --pcm the decode is FFmpeg's (REF, f32le at the file's rate):
* the same length, sample for sample within 1e-4 - where the audio
* starts and ends, gapless trims and all. With --frames the length
* audio_transfer_info states is N. With
* audio_transfer_info states is N. With --source the decode is
* the signal the encoder was given (REF, f32le): where it starts,
* to the sample, its length within 2 ms (what ASF's millisecond
* durations lose), and at least 20 dB above the coding error -
* for codecs FFmpeg's decoder is not gapless for. With
* --tones FILE is 5.1 with a tone in each channel - FL 300 Hz,
* FR 500, FC 700, LFE 80, BL 1100, BR 1300 - and the decode folded
* with audio_transfer_stereo_matrix has the left tones on the left
@@ -250,7 +254,7 @@ static double tone(const float *x, size_t n, double hz, double rate)
}
static int run_file(const char *path, const char *want, int tones,
const char *pcm_path, long want_frames)
const char *pcm_path, long want_frames, const char *src_path)
{
FILE *fp = fopen(path, "rb");
uint8_t *buf;
@@ -341,6 +345,60 @@ static int run_file(const char *path, const char *want, int tones,
free(ref);
}
if (src_path)
{
FILE *rf = fopen(src_path, "rb");
long rlen, lag, best_lag = 0;
float *ref;
size_t i, rn, n;
double best = -1e300, sig = 0, err = 0;
unsigned c;
if (!rf)
return 1;
fseek(rf, 0, SEEK_END);
rlen = ftell(rf);
fseek(rf, 0, SEEK_SET);
ref = (float*)malloc((size_t)rlen + 1);
if (fread(ref, 1, (size_t)rlen, rf) != (size_t)rlen)
return 1;
fclose(rf);
rn = (size_t)rlen / sizeof(float) / ch;
n = rn < frames ? rn : frames;
/* where the decode lines up with the source */
for (lag = -4096; lag <= 4096; lag++)
{
double dot = 0;
for (i = 0; i < n; i++)
{
long j = (long)i + lag;
if (j < 0 || (size_t)j >= frames)
continue;
for (c = 0; c < ch; c++)
dot += (double)pcm[(size_t)j * ch + c] * ref[i * ch + c];
}
if (dot > best)
{
best = dot;
best_lag = lag;
}
}
for (i = 0; i < n * ch; i++)
{
sig += (double)ref[i] * ref[i];
err += ((double)pcm[i] - ref[i]) * ((double)pcm[i] - ref[i]);
}
if ( best_lag
|| (frames > rn ? frames - rn : rn - frames) > rate / 500
|| !(sig > 0 && 10 * log10(sig / (err + 1e-30)) >= 20))
{
fprintf(stderr, "%s: %ld frames from the source, %u frames to its %u, %.1f dB above the error\n",
path, best_lag, (unsigned)frames, (unsigned)rn,
sig > 0 ? 10 * log10(sig / (err + 1e-30)) : 0.0);
bad = 1;
}
free(ref);
}
if (tones)
{
static const double hz[6] = { 300, 500, 700, 80, 1100, 1300 };
@@ -390,7 +448,7 @@ static int run_file(const char *path, const char *want, int tones,
if (!bad)
printf("audio_transfer_sniff_test: %s: %s, %u channels at %u Hz, %u frames%s%s\n",
path, want, ch, rate, (unsigned)frames, tones ? ", each tone where it belongs" : "",
pcm_path ? ", FFmpeg's samples" : "");
pcm_path ? ", FFmpeg's samples" : src_path ? ", the source's timing" : "");
audio_transfer_free(ctx, t);
free(pcm);
free(buf);
@@ -402,7 +460,7 @@ int main(int argc, char **argv)
if (argc >= 4 && !strcmp(argv[1], "--file"))
{
int i, tones = 0;
const char *pcm = NULL;
const char *pcm = NULL, *src = NULL;
long frames = -1;
for (i = 4; i < argc; i++)
{
@@ -412,8 +470,10 @@ int main(int argc, char **argv)
pcm = argv[++i];
else if (!strcmp(argv[i], "--frames") && i + 1 < argc)
frames = atol(argv[++i]);
else if (!strcmp(argv[i], "--source") && i + 1 < argc)
src = argv[++i];
}
return run_file(argv[2], argv[3], tones, pcm, frames);
return run_file(argv[2], argv[3], tones, pcm, frames, src);
}
return run_checks();
}
@@ -19,13 +19,17 @@ static void check(int valid, const char *what)
}
}
#include "wma_fixture.h"
/* The fixture is one coded frame. WMA1 and WMA2 play gapless: that
* frame's first half is the encoder's priming, dropped, and its second
* half - the overlap tail - comes out when the stream drains: what
* plays is the second half of the windowed transform. */
static void expected(void)
{
unsigned i;
for (i = 0; i < 2048; i++)
{
double transform = -cos(PI / 2048 * (i + 0.5 + 1024) * 0.5);
double window = sin((i + 0.5) * PI / 4096);
double transform = -cos(PI / 2048 * (i + 2048 + 0.5 + 1024) * 0.5);
double window = sin((i + 2048 + 0.5) * PI / 4096);
reference[i] =
(float)(256 * pow(10.0, 5.0) * (2.0 / 2048) / 32768 * transform * window);
}
@@ -356,8 +356,12 @@ static void standard_test(void)
"uncoded standard frame");
zero_frame(&f, n);
check(used == sizeof(packet), "standard packet released");
/* the drain gives the last frame's overlap tail, once */
check(!rwma_decode_packet(d, NULL, 0, &used, &f) && f.samples == n,
"standard drain gives the overlap tail");
zero_frame(&f, n);
check(!rwma_decode_packet(d, NULL, 0, &used, &f) && !f.samples,
"standard drain adds no invented tail");
"standard drain gives it once");
rwma_free(d);
}
cfg.codec = RWMA_V2;