View Full Version : Repeated lossy-encoding test: AAC, Opus, MP3, and MP2
lvqcl
15th September 2025, 00:30
I changed the hp filter from 3 to 0, trying to not break anything...
Just comment the line out, I suppose.
BTW, here's an explanation of this filter:
https://people.xiph.org/~xiphmont/demo/opus/demo3.shtml
DC Rejection
The 1.1 encoder now uses a built-in DC rejection filter (3Hz cutoff) for all modes. The effect of the filter itself is inaudible, but it prevents DC energy from polluting the masking and energy analysis of the lowest frequency bands.
GeoffreyA
15th September 2025, 08:37
*Vanquished by the code*
Luckily there's a "primitive" opus_demo.exe encoding/decoding CLI tool available just from the main libopus repo so I don't have to go through all other repos each step.
Look what have I found! I said there's some filter stuff going on didn't I?
(Need to complete the rest of the steps to confirm, of course)
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[0869829f343f85935fac22462d228a065d0ba320] Adds a 3 Hz high-pass filter and boost allocation on leakage
Bravo! You have slain the 100-headed Opus dragon, using the Sword of Git-Bisect and the Spell of Moonwalk :)
I changed the hp filter from 3 to 0, trying to not break anything... (this is done on current main branch)
The problem is mostly gone.
https://workupload.com/archive/b2dMuedNrn
(Somehow I still think the old CELT is better, is this just something in my head?)
diff --git a/src/opus_encoder.c b/src/opus_encoder.c
index 276dc58d..e87a5379 100644
--- a/src/opus_encoder.c
+++ b/src/opus_encoder.c
@@ -1897,7 +1897,7 @@ static opus_int32 opus_encode_frame_native(OpusEncoder *st, const opus_res *pcm,
}
#endif
} else {
- dc_reject(pcm, 3, &pcm_buf[total_buffer*st->channels], st->hp_mem, frame_size, st->channels, st->Fs);
+ dc_reject(pcm, 0, &pcm_buf[total_buffer*st->channels], st->hp_mem, frame_size, st->channels, st->Fs);
}
#ifndef FIXED_POINT
if (float_api)
Right now, I can't tell the difference between the CELT and 0Hz Opus. Now, this solves the issue of many iterations, but for ordinary usage of one iteration, does the DC rejection improve quality? I noticed that the wooshing sound crops up after 20-30 iterations, perhaps even 10, though it becomes harder to distinguish from the original rumbling bass at the start.
Z2697
15th September 2025, 09:06
I think the filter is meant to remove the DC shift, which is uhh, the audio that has DC shift is problematic to begin with.
Should it be dealt with in a hard coded way? I don't know.
Filtering anything just for a few problematic samples? Yes 3 Hz is inaudible, and repeated encoding is not a general use case, but still...
I guess we "tolerate" in-loop deblocking filter for a reason.
I'm not sure about the masking, and the effect to general sound quality, but here's an example of how it affects energy analysis, thus rate control I think:
(Cut off 1 Hz is enough to remove the DC shift in this case, and the generational loss is also much lower)
>ffmpeg -v 0 -i Roundabout.flac -f wav -vn -sn -ar 48k -af dcshift=0.1 - | .\org\opusenc - org.opus
Skipping chunk of type "LIST", length 124
Encoding using libopus 1.5.2-213-gb5dc74f (audio)
-----------------------------------------------------
Input: WAV, 48 kHz, 2 channels, stereo
Output: Opus, 2 channels (2 coupled), stereo
20ms packets, 96 kbit/s VBR
Preskip: 312
Encoding complete
-----------------------------------------------------
Encoded: 8 minutes and 35.22 seconds
Runtime: 2 seconds
(257.6x realtime)
Wrote: 6735550 bytes, 25761 packets, 518 pages
Bitrate: 103.805 kbit/s (without overhead)
Instant rates: 1.2 to 196.4 kbit/s
(3 to 491 bytes per packet)
Overhead: 0.746% (container+metadata)
>ffmpeg -v 0 -i Roundabout.flac -f wav -vn -sn -ar 48k -af dcshift=0.1 - | .\3to0\opusenc - 3to0.opus
Skipping chunk of type "LIST", length 124
Encoding using libopus 1.5.2-214-gca9ec9b (audio)
-----------------------------------------------------
Input: WAV, 48 kHz, 2 channels, stereo
Output: Opus, 2 channels (2 coupled), stereo
20ms packets, 96 kbit/s VBR
Preskip: 312
Encoding complete
-----------------------------------------------------
Encoded: 8 minutes and 35.22 seconds
Runtime: 2 seconds
(257.6x realtime)
Wrote: 6909963 bytes, 25761 packets, 518 pages
Bitrate: 106.507 kbit/s (without overhead)
Instant rates: 65.6 to 196.8 kbit/s
(164 to 492 bytes per packet)
Overhead: 0.733% (container+metadata)
>ffmpeg -v 0 -i Roundabout.flac -f wav -vn -sn -ar 48k -af dcshift=0.1 - | .\3to1\opusenc - 3to1.opus
Skipping chunk of type "LIST", length 124
Encoding using libopus 1.5.2-214-g8a35163 (audio)
-----------------------------------------------------
Input: WAV, 48 kHz, 2 channels, stereo
Output: Opus, 2 channels (2 coupled), stereo
20ms packets, 96 kbit/s VBR
Preskip: 312
Encoding complete
-----------------------------------------------------
Encoded: 8 minutes and 35.22 seconds
Runtime: 2 seconds
(257.6x realtime)
Wrote: 6748345 bytes, 25761 packets, 518 pages
Bitrate: 104.003 kbit/s (without overhead)
Instant rates: 1.2 to 196.8 kbit/s
(3 to 492 bytes per packet)
Overhead: 0.745% (container+metadata)
Just comment the line out, I suppose.
It seems like it's also copying samples from "pcm" to "pcm_buf", I was just being lazy and leave it be...
We can restore it to the code before that filter was introdecud.
diff --git a/src/opus_encoder.c b/src/opus_encoder.c
index 276dc58d..e8b9f5f5 100644
--- a/src/opus_encoder.c
+++ b/src/opus_encoder.c
@@ -1897,7 +1897,8 @@ static opus_int32 opus_encode_frame_native(OpusEncoder *st, const opus_res *pcm,
}
#endif
} else {
- dc_reject(pcm, 3, &pcm_buf[total_buffer*st->channels], st->hp_mem, frame_size, st->channels, st->Fs);
+ for (i=0;i<frame_size*st->channels;i++)
+ pcm_buf[total_buffer*st->channels + i] = pcm[i];
}
#ifndef FIXED_POINT
if (float_api)
GeoffreyA
15th September 2025, 10:58
I hear you. The less filtering, the better, especially if it's of no benefit for most samples.
So, the bitrate is going up slightly. I'd like to try this at one-iteration low-bitrate and see if there's any effect.
j7n
15th September 2025, 13:55
Significant DC is uncommon. You have to zoom all the way in to see it when it exists. Rumble is only found on vinyl and maybe some wind or malfunctioning equipment (I've seen it on a radio recording with one source). AC-3 lets you toggle it. Other codecs just encode it without problems. Highpass requires 1/3rd of a second (for 3 Hz) to catch up at the start, causing a click with the preceding track. An audio interface with its "coupling" has a highpass filter already.
GeoffreyA
17th September 2025, 10:11
Significant DC is uncommon. You have to zoom all the way in to see it when it exists. Rumble is only found on vinyl and maybe some wind or malfunctioning equipment (I've seen it on a radio recording with one source). AC-3 lets you toggle it. Other codecs just encode it without problems. Highpass requires 1/3rd of a second (for 3 Hz) to catch up at the start, causing a click with the preceding track. An audio interface with its "coupling" has a highpass filter already.
Helpful explanation. Thank you.
Z2697
17th September 2025, 14:08
I think it's more about the encoder's internal status / decision making than anything else, if you remove the filter (or use old CELT, or use FFmpeg native Opus), you can see a increase in intensity in low frequency when there's DC offset, this is likely due to some fundamental design of CELT, other codecs are likely not gonna have this issue, thus they don't "have to" hard code a filter.
(libopus don't have to either! It's completely out-of-loop and DC offset is uncommon to begin with)
I've opened an issue on libopus' github repo by the way https://github.com/xiph/opus/issues/434
GeoffreyA
18th September 2025, 10:16
Good that you reported our findings.
I'm trying to understand the difference between CELT and other transform codecs like MP3 and AAC, but as far as I can tell as a layman, the main differences are the size of the blocks, the filter bank, and how the frequency coefficients are quantised, along with delta encoding and pyramid quantisation. CELT discards all bands below 8 Hz (https://datatracker.ietf.org/doc/html/rfc6716#page-8), so I wonder how the DC rejection is having an effect; or perhaps, coming before MDCT, there is a difference whether one filters or not.
Z2697
18th September 2025, 12:34
URL="https://datatracker.ietf.org/doc/html/rfc6716#page-8"]CELT discards all bands below 8 Hz[/URL]
Sorry, where exactly is that reference?
The subbands are unlikely to have this level of granularity.
GeoffreyA
18th September 2025, 12:59
Sorry, where exactly is that reference?
The subbands are unlikely to have this level of granularity.
My mistake. Page 9, paragraph 3, last sentence. (https://datatracker.ietf.org/doc/html/rfc6716#page-9)
In the MDCT layer, all bands below 8 kHz are discarded
I think while we're at it, we might as well submit a ticket to FFmpeg for bad AAC quality. Using your guitar sample, after a few iterations, artefacts show up.
Z2697
18th September 2025, 16:47
My mistake. Page 9, paragraph 3, last sentence. (https://datatracker.ietf.org/doc/html/rfc6716#page-9)
I think while we're at it, we might as well submit a ticket to FFmpeg for bad AAC quality. Using your guitar sample, after a few iterations, artefacts show up.
That paragraph is describing Hybrid mode, which discards 8 kHZ from CELT and use SILK for those instead.
GeoffreyA
18th September 2025, 21:08
Oh, boy, I can't believe I didn't see the kilohertz! Apologies.
Z2697
22nd September 2025, 23:39
By the way, would you say the "fixed" libopus has same level of low generational loss as QuickTime AAC?
GeoffreyA
23rd September 2025, 09:09
By the way, would you say the "fixed" libopus has same level of low generational loss as QuickTime AAC?
Was this one at 200 iterations?
https://workupload.com/archive/b2dMuedNrn
If so, they are roughly on the same footing, without a blind test, but there is a difference.
Z2697
23rd September 2025, 21:19
Was this one at 200 iterations?
https://workupload.com/archive/b2dMuedNrn
If so, they are roughly on the same footing, without a blind test, but there is a difference.
Yeah, 200 iterations, did you compare it with 200x qaac or 50x?
GeoffreyA
23rd September 2025, 21:58
Yeah, 200 iterations, did you compare it with 200x qaac or 50x?
200x qaac. I'm surprised these encoders hold up that well.
Z2697
20th November 2025, 17:14
M$ MediaFoundation AAC encoder is actually pretty good. (both the "first pass" quality and generational loss, I suppose)
I don't really have experienced the first pass quality enough to compare it with other AAC encoders.
I just realized there's aac_mf encoder in FFmpeg when some change was made to its doc entry. :p
https://workupload.com/file/QCz73bje5LK
GeoffreyA
20th November 2025, 19:10
I haven't seen it tested against other encoders, so we don't know where it stands in the ranking. Problematic, because the usual choice is between FFmpeg's Atrocious Audio Coding and Apple. We should put it through the motions.
By the way, that's a good location to be, between one's ears :)
GeoffreyA
21st November 2025, 16:53
M$ MediaFoundation AAC encoder is actually pretty good. (both the "first pass" quality and generational loss, I suppose)
I don't really have experienced the first pass quality enough to compare it with other AAC encoders.
I just realized there's aac_mf encoder in FFmpeg when some change was made to its doc entry. :p
https://workupload.com/file/QCz73bje5LK
Surprisingly strong on the 200-pass encode; but I don't know: I thought I could hear noise on a single pass too, though maybe it's my imagination. No settings are exposed in the wrapper?
tormento
30th November 2025, 11:34
M$ MediaFoundation AAC encoder
How to use that?
GeoffreyA
25th January 2026, 19:19
How to use that?
Use:
ffmpeg -i src -c:a aac_mf out.m4a
Doesn't look as if the bitrate can be adjusted through the wrapper.
Nope... https://github.com/xiph/opus/issues/434
Apparently they think it's OK. (I might have to disagree)
But the new QEXT feature effectively disables that filter, whether it's intentional or not.
But no easily accessable encoder was availbale. (perhaps still is)
I have (https://github.com/Mr-Z-2697/opus) some (https://github.com/Mr-Z-2697/libopusenc) forks (https://github.com/Mr-Z-2697/opus-tools) now... but didn't figure out how to do 96 khz... the extended bitrate and cutoff does work though.
This is too off topic now, maybe we should continue elsewhere if we want.
Replying here, coming from the ADC thread.
Have read the issue on Github. Likely, it will take a bigger selection of samples showing how quality improves having the filter off. But, the problem showing up chiefly, or only, on the repeated-encoding case makes it hard to prove.
Z2697
25th January 2026, 19:55
About the aac_mf encoder... it's "first pass" quality is terrible.
Z2697
25th January 2026, 20:04
Have read the issue on Github. Likely, it will take a bigger selection of samples showing how quality improves having the filter off. But, the problem showing up chiefly, or only, on the repeated-encoding case makes it hard to prove.
Somehow they are now considering the value of ultrasound but don't want to talk about infrasound (and the artifact that eventually leaks to audible range).
GeoffreyA
26th January 2026, 14:01
Somehow they are now considering the value of ultrasound but don't want to talk about infrasound (and the artifact that eventually leaks to audible range).
The two should go together, being opposite ends of the same category.
j7n
27th January 2026, 12:53
But infrasound carries very little information and thus would not affect the compression ratio.
GeoffreyA
27th January 2026, 13:59
As I understand it, for the sake of a rare case that trips the encoder, files with DC shift, they are applying DC filtering to all files.
vBulletin® v3.8.11, Copyright ©2000-2026, vBulletin Solutions Inc.