Fix doubled Rosella gain and bed DRC decoding
build / windows (push) Has been cancelled
build / release (push) Has been cancelled

This commit is contained in:
2026-09-25 21:23:13 +08:00
parent 188cb9ebfb
commit 259688ef1b
3 changed files with 157 additions and 142 deletions
+144 -134
View File
@@ -1,134 +1,144 @@
# Verification
Measured results only. Each entry names the command or the log line it came from, so it can
be reproduced. Environment: Windows x64 host, official **foobar2000 1.6.19 x86** portable
installation, official SDK 2026-09-17 pinned to `FOOBAR2000_TARGET_VERSION 80`, MSVC 14.44,
ffmpeg 8.0.
Test material is supplied locally and is **not** part of this repository: the `testdata/` and
`vectors/` files of the upstream renderer project, and — for the binaural measurements — an
HRTF file. Everything except binaural rendering runs without any HRTF; binaural runs take the
file as an argument (`tests/render_harness.cpp --hrtf …`) or use the default location beside
the DLL.
## Component and renderer
| Check | Result |
|---|---|
| Sources compiled in, nothing loaded at run time | log: `core: in-process renderer 0.1.0-m1 (abi 3), component built against abi 3` |
| One artefact, no companion DLL | `dist\*.fb2k-component` holds `foo_input_joc.dll` and `README.md` only |
| Kernel sources untouched | the upstream working tree's file timestamps are unchanged; it is only ever read |
| Both architectures build | `build\Win32\foo_input_joc.dll`, `build\x64\foo_input_joc.dll` |
## Playback in foobar2000 1.6.19 x86
| Case | Log evidence |
|---|---|
| Speaker 7.1, 5 s file | `stream created, 8 output channel(s), layout=7.1` … `frames_in=157 frames_out=157 samples_out=241152` — 157 × 1536 exactly |
| Binaural, SOFA | `stream created, 2 output channel(s)` … `end of stream after 481215 frames` (241152 source + 240063 tail) |
| Binaural, Rosella model | `stream created, 2 output channel(s)` … `end of stream after 481855 frames` |
| Binaural, HRTF path left empty | resolves to `<component directory>\HRTF\binaural.sofa` and produces the same 481215 frames as naming that file explicitly |
| Full 238 s file, binaural SOFA | `eac3 frames queued=7436, bed frames pushed=7436` … `samples_out=11661759`; no ffmpeg process left behind |
| Installed from the `.fb2k-component` package | unpacked into `user-components\foo_input_joc\`, plays with the default-folder HRTF |
## Bitstream recognition
`tests/scan_crosscheck.cpp` compares, frame by frame, the syncframe lengths and the JOC
verdict this component computes against the renderer's own `joc_eac3_frame_bytes()` /
`joc_parse_eac3_frame()`:
```
testdata\gold_forever.eac3 frames=64 plugin_joc=64 kernel_joc=64 len_mismatch=0 verdict_mismatch=0
vectors\valid.eac3 frames=7 plugin_joc=7 kernel_joc=7 len_mismatch=0 verdict_mismatch=0
build\plain_eac3.eac3 frames=64 plugin_joc=0 kernel_joc=0 len_mismatch=0 verdict_mismatch=0
crosscheck: 3 file(s), AGREES WITH CORE
```
Ten corrupt vectors were compared as well: no frame-length disagreement, verdicts agreed on
9 of 10. The one difference is `corrupt_truncated_huffman.eac3`: this component only tests
for the container, while the renderer also parses the payload and reports a truncated
bitstream later.
## Plain E-AC-3 is handed back
Playing a file the component's own encoder produced without JOC:
```
decoder: open "...plain_eac3.eac3" reason=1 bytes=144384 frames=8 with_joc=0
decoder: yielding to the built-in decoder (at least one examined syncframe has no JOC EMDF container)
```
The file then plays through the built-in decoder; no decode log appears for it. Verified both
before and after the renderer was compiled in.
## Output identical to the reference renderer
`tests/render_harness.cpp` drives the component's own engine and writes a WAV in the same
format the reference command-line renderer writes, so the two files can be compared byte for
byte. 30 s of the reference file, speaker 5.1:
| Product | Whole-file SHA-256 |
|---|---|
| reference renderer (`--speaker-layout 5.1 --bed … --duration 30`) | `99a8e3edbd1c047a3c0f547eaf85e56941f882af9e65468fbde0a9f18b6c5b6e` |
| this component's engine, x64 | `99a8e3edbd1c047a3c0f547eaf85e56941f882af9e65468fbde0a9f18b6c5b6e` |
| this component's engine, x86 | `99a8e3edbd1c047a3c0f547eaf85e56941f882af9e65468fbde0a9f18b6c5b6e` |
34,578,500 bytes each. Binaural with a real SOFA file: with the same input frame count the
whole file is identical too (`588a15ce5526977f…baa88`, 12,158,780 bytes), and over the whole
238 s file the total sample count matches exactly (11,661,759 = 11,420,735 program +
241,024 tail) with identical peak (1.144561172) and an identical SHA-256 over the reference
renderer's entire payload.
The one structural difference is the tail: the reference renderer trims trailing samples
below 1e-8 and this component returns the tail in full.
## Gain
Same file, speaker 5.1, rendered with and without attenuation:
| Check | Result |
|---|---|
| Peak | 0.183568597 → 0.092002235, i.e. exactly −6.000000 dB |
| RMS over the whole signal | exactly −6.000000 dB |
| Per-sample, 1,446,912 floats | largest deviation from the ideal scaling is 2.1e-06 relative; the gain is applied in double precision before the DSP, so the remaining difference is float32 rounding |
| +6 dB | same check passes |
| The switch | switch on and −6 dB → log `gain=-6.00 dB`, delivered peak 0.000063; switch off with −6 dB still stored → `gain=0.00 dB`, peak 0.000126; switch on and 0 dB → peak 0.000126 |
## Preferences page
`tests/prefs_layout_check.cpp` builds the page from the component's own dialog resource and
asserts, per control, that it is enabled, lies inside the client area, and is not covered by
another interactive control (static text and group boxes are transparent to the mouse, as
they are for real clicks):
```
dialog client=495x367 non-client=0x0
WS_CAPTION=no WS_BORDER=no WS_CHILD=yes WS_VISIBLE=yes
content extent=485x354 client=495x367 (everything fits)
controls=26 problems=0
```
The page has no caption of its own — the host draws the frame — and no control is ever
disabled, which is what "the option is there but cannot be clicked" otherwise looks like.
Not verified here: how the page and the `%joc_*%` fields look on screen; that needs a human
in front of the window.
## Container support (mp4 / m4a / mov / mkv / mka / webm)
Verified with the Win32 build, a 5.1 speaker layout, and the component installed in a
portable foobar2000 1.6.19 profile:
- `ffmpeg -i <bare .eac3> -c:a copy` muxed into MP4 and Matroska, then `-map 0:a:0 -c:a copy
-f eac3` extracted again, is byte-identical to a direct `-t 30 -c:a copy` of the source
(same SHA-256) — the renderer therefore sees the stored syncframes, not a re-encode.
- The header probe (`tests/container_scan_test.cpp`, no foobar2000 involved) reports, for the
same material: `joc.mp4 -> mp4 eac3=1 audio#0 codec=ec-3 30.016 s`,
`joc.mkv -> matroska eac3=1 audio#0 codec=A_EAC3 30.016 s`, `plain_eac3.mp4 -> ec-3`,
`ac3.mp4 -> ac-3 (declined)`, `aac.mp4 -> mp4a (declined)`.
- End to end, with the component ordered ahead of the container reader in
Preferences -> Decoding: `open()` is called, the track is found, the JOC verdict is positive,
the file is claimed, and playback reaches `end of stream`.
- Negative case end to end: an MP4 holding E-AC-3 without JOC yields with
`E-AC-3 track 0 carries no JOC`, and the built-in decoder plays it.
- Bare `.eac3` / `.ec3` handling is unchanged.
# Verification
Measured results only. Each entry names the command or the log line it came from, so it can
be reproduced. Environment: Windows x64 host, official **foobar2000 1.6.19 x86** portable
installation, official SDK 2026-09-17 pinned to `FOOBAR2000_TARGET_VERSION 80`, MSVC 14.44,
ffmpeg 8.0.
Test material is supplied locally and is **not** part of this repository: the `testdata/` and
`vectors/` files of the upstream renderer project, and — for the binaural measurements — an
HRTF file. Everything except binaural rendering runs without any HRTF; binaural runs take the
file as an argument (`tests/render_harness.cpp --hrtf …`) or use the default location beside
the DLL.
## Component and renderer
| Check | Result |
|---|---|
| Sources compiled in, nothing loaded at run time | log: `core: in-process renderer 0.1.0-m1 (abi 3), component built against abi 3` |
| One artefact, no companion DLL | `dist\*.fb2k-component` holds `foo_input_joc.dll` and `README.md` only |
| Kernel sources untouched | the upstream working tree's file timestamps are unchanged; it is only ever read |
| Both architectures build | `build\Win32\foo_input_joc.dll`, `build\x64\foo_input_joc.dll` |
## Playback in foobar2000 1.6.19 x86
| Case | Log evidence |
|---|---|
| Speaker 7.1, 5 s file | `stream created, 8 output channel(s), layout=7.1` … `frames_in=157 frames_out=157 samples_out=241152` — 157 × 1536 exactly |
| Binaural, SOFA | `stream created, 2 output channel(s)` … `end of stream after 481215 frames` (241152 source + 240063 tail) |
| Binaural, Rosella model | `stream created, 2 output channel(s)` … `end of stream after 481855 frames` |
| Binaural, HRTF path left empty | resolves to `<component directory>\HRTF\binaural.sofa` and produces the same 481215 frames as naming that file explicitly |
| Full 238 s file, binaural SOFA | `eac3 frames queued=7436, bed frames pushed=7436` … `samples_out=11661759`; no ffmpeg process left behind |
| Installed from the `.fb2k-component` package | unpacked into `user-components\foo_input_joc\`, plays with the default-folder HRTF |
## Bitstream recognition
`tests/scan_crosscheck.cpp` compares, frame by frame, the syncframe lengths and the JOC
verdict this component computes against the renderer's own `joc_eac3_frame_bytes()` /
`joc_parse_eac3_frame()`:
```
testdata\gold_forever.eac3 frames=64 plugin_joc=64 kernel_joc=64 len_mismatch=0 verdict_mismatch=0
vectors\valid.eac3 frames=7 plugin_joc=7 kernel_joc=7 len_mismatch=0 verdict_mismatch=0
build\plain_eac3.eac3 frames=64 plugin_joc=0 kernel_joc=0 len_mismatch=0 verdict_mismatch=0
crosscheck: 3 file(s), AGREES WITH CORE
```
Ten corrupt vectors were compared as well: no frame-length disagreement, verdicts agreed on
9 of 10. The one difference is `corrupt_truncated_huffman.eac3`: this component only tests