godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
90.98k stars 21.16k forks source link

.OGG files are played back with rolled-off high frequencies (low-pass) #49131

Closed nicolasGab closed 3 years ago

nicolasGab commented 3 years ago

Godot version:

3.3 MONO

OS/device including version:

MacOS 10.13.4 Windows 10

Issue description:

When playing game audio from .ogg files, high frequencies (notably above 10kHz) are attenuated. Upon measuring frequency levels for a single track, I found a 10dB difference around 15kHz between the original (.ogg) audio and the game playback (see following graphs). This does not happen when using .wav files.

In the following graphs I've zoomed in on the high frequency range, 5kHz-22kHz, as the rest of the spectrum appears rather unaltered.

DAW audio (OGG) maximum frequency levels (average between left and right channels) DAW audio avg 5k-22k

Game audio loopback (OGG) maximum frequency levels (average between left and right channels) Game audio avg 5k-22k

The .ogg file used in this example has a sample rate of 44.1kHz. Tests were executed on MacOS 10.13.4 with Reaper DAW; game audio was routed to the DAW with "Loopback" software from Rogue Amoeba and analysed using Blue Cat's FreqAnalyst VST. Audio playback with VLC media player was also recorded through "Loopback" and analysed. While there is a difference between VLC loopback and DAW playback, it is mostly after 18 or 20kHz, this may or may not be caused by the "Loopback" software. Frequency levels between 10kHz and 15kHz remain similar to the original audio.

VLC audio loopback (OGG) maximum frequency levels (average between left and right channels) VLC audio avg 5-22

Steps to reproduce:

This issue was confirmed by another Godot Engine Discord server user on Windows 10.

Calinou commented 3 years ago

Related to https://github.com/godotengine/godot/issues/23544.

Can you reproduce this with a MP3 file? (MP3 is supported since Godot 3.3.)

nicolasGab commented 3 years ago

There seems to be a similar issue with MP3s, both audibly and measured:

DAW audio (MP3) maximum frequency levels (average between left and right channels) DAW mp3 crop

Game audio loopback (MP3) maximum frequency levels (average between left and right channels) Game mp3

We see the same -10dB around 15kHz

nicolasGab commented 3 years ago

Just to specify the issue: the music sounds like it's going through a (very wide) low-pass filter, it does not sound like bad compression artifacts

Calinou commented 3 years ago

@nicolasGab Can you reproduce this in 3.2.3 to check whether this is a regression from Godot 3.3? Also, try playing around with the output sample rate in the Project Settings.

nicolasGab commented 3 years ago

@Calinou Indeed the sound is much better in v3.2.3 (still using a .ogg file). It looks very close to the original audio.

v3.2.3 game audio loopback (OGG) maximum frequency levels (average between left and right channels) v3 2 3 OGG avg

nicolasGab commented 3 years ago

Godot standard version 3.3.2 has the same problem as 3.3 Mono

v3.3.2 standard game audio loopback (OGG) maximum frequency levels (average between left and right channels) Standard v3 3 2 OGG

Calinou commented 3 years ago

@ellenhp Any idea what could be causing this? Thanks in advance :slightly_smiling_face:

ellenhp commented 3 years ago

Weird, this could be an artifact of the resampling changes I made in #46086. From what I read in the paper I based the changes on it didn't seem like this was likely to happen, and I didn't notice anything like this but my ears are garbage. The ringing from the previous resampling algorithm was pretty awful though, and I'm hesitant to suggest we immediately revert #46086 to fix this.

The easiest way to isolate this issue is to show a difference between playback of OGGs that require runtime resampling and OGGs that do not require runtime resampling, both in Godot 3.3. Any discernible difference is a bug in the resampling algorithm.

@nicolasGab could you try comparing two different OGG files? One that matches your system's sample rate and one that doesn't? 44.1k and 48k, for example, unless your system is using some exotic sample rate. If you need to resample to generate these files, use software you trust and play them both back using an audio player you also trust using good speakers/headphones and/or a spectrogram like you did for the original report (thank you for being so scientific btw!). This is just to verify that the files sound identical using known-good playback software.

Then play them both in Godot. If you have the time I'd love see results from 3.2.3 and 3.3, each with matching/non-matching sample rate oggs. My expectation is:

This is what I observed while testing #46086, but obviously one or both of the last two statements isn't true on your system. The most compelling thing that you could observe while doing this is that matching and non-matching sample rate OGGs don't even match each other in 3.3. That would be alarming.

@ tag me if you gather this data so I don't miss it. I'm not actively following godot development anymore but I do want to make sure this gets fixed.

nicolasGab commented 3 years ago

@ellenhp

I made a 48kHz .ogg version based on the 44.1kHz .ogg (probably should have based it on the original .wav...), double checked the sample rate with another software and ran the tests:

v3.3 game audio loopback (OGG) track @ 48kHz, system @ 44.1kHz 3 3 OGG 48khz

v3.2.3 game audio loopback (OGG) track @ 48kHz, system @ 44.1kHz v3 2 3 OGG 48kHz full

OGG file at 48kHz sample rate Godot audio settings at 44.1kHz System audio settings at 44.1kHz

ellenhp commented 3 years ago

I'm not sure what to do here. All resampling algorithms are pretty much opaque to me so if it is the algorithm I don't think I can fix it. And maybe I'm the only one but I think I'd prefer a 10db low pass compared to this awful ringing: https://zylannprods.fr/dl/godot/GodotLowFrequencySound.mp4

At least with a low pass there's a workaround. :(

It's really interesting to me though that you're seeing this even when the resampling algorithm itself should be a no-op. Perhaps this isn't a resampling bug? Or perhaps the algorithm I picked isn't suitable for this use-case.

Calinou commented 3 years ago

I wonder if this is similar to what I proposed here: https://github.com/godotengine/godot-proposals/issues/2298

In short, resampling algorithms are a tradeoff. There is no algorithm that will always sound better in all situations, and some algorithms are more demanding on the CPU than others.

ellenhp commented 3 years ago

Yeah. Looking at it now, I think I have a vague sense of why we might be seeing this. The resampling algorithm operates on a window of 4 samples. The challenge for any resampling algorithm is how to take the window of samples it's looking at and correctly interpolate. I'm guessing that the algorithm I picked introduces a low pass filter during interpolation. Imagine for instance if your "interpolation" code just took the mean of the 4 sample window. That would result in an extremely aggressive low-pass. I bet this algorithm unintentionally does something more subtle than that, which is a shame because it got rid of the ringing reported in #23544 completely.

ellenhp commented 3 years ago

I'm not really an audio person. When I went through and fixed a rash of audio bugs earlier this year it was because I was evaluating Godot for an audio game my friend and I were thinking about making, but we ended up doing other things and I've moved on to other projects. Still, I'd really prefer we not just completely revert #46086 because it would be a shame to have that ringing back for extremely bass-heavy sounds. I really want Godot to succeed on an ideological level even if I'm not actively using it, and major audio bugs make that less likely. But at the same time, 10db attenuation at the high end is a lot. This might also be a major audio bug. :(

nicolasGab commented 3 years ago

@ellenhp quickly going over the paper you mentioned, I see a lot of comments on compensation, such as (p.17):

The second thing to note is that the passband of an interpolator is not flat and this must be compensated for through filtering, possibly in the oversampling stage.

I didn't actually read the whole paper, so this might be bad lead, but if we get away with this using a simple EQ boost... it would be a very easy solution.

I'll make a few measurements with white noise to see what kind of attenuation is really going on.

nicolasGab commented 3 years ago

Hi there @ellenhp @Calinou I managed to generate white noise and feed it into Godot 3.3. The effect is very clear now, below you'll find the original frequency response, then the 48kHz and 44.1kHz game audio and finally an EQ'd version. The game engine and my system are set to 44.1kHz.

White noise OGG white noise original OGG 44 1kHz This white noise lives around -63dB and gives a rough reference to analyze the resampling system.

v3.3 game audio loopback 48kHz white noise 3 3 OGG 48kHz A 48kHz OGG is generated and fed into Godot.

v3.3 game audio loopback 44.1kHz white noise 3 3 OGG 44 1kHz The same is done with a 44.1kHz OGG file.

The HF dip is much steeper in the 44.1kHz version. We read roughly -7dB @ 15kHz compared to the reference. If we compensate the with an EQ, we can fall back on a track that sound similar to the original file.

Compensated game audio 44.1kHz white noise compensated 44 1kHz

A 7dB boost is applied at 15kHz with a Q of 1, as shown in the graphical representation below.

EQ curve 7dB boost at 15kHz, Q=1

This is obviously not an ideal solution...

These tests were done with a -34dB LUFS white noise track. -70dB vs. -63dB @15kHz is an 11% difference (-7dB). With a -15dB LUFS white noise track, I observed around -52dB vs. -44dB @15kHz which is an 18% difference (-8dB).

ellenhp commented 3 years ago

Not sure what to do about this one. The architecture of the audio system doesn't make adding an EQ to compensate for this a simple endeavor from what I remember. The audio effect system is designed to be configurable by the user so I don't know how to hide something like an EQ from the user and do it unconditionally for this type of audio player.

As an aside, @nicolasGab just to unblock your project I wanted to mention that you can move all of your OGG and MP3 players from their current audio bus (call it n) to a new audio bus (call it n+1), and then I forget what the terminology is but you can set that new bus's "master" or "parent" to be the audio bus that you had those players on before (or master if you only had one audio bus). That way the old effect chain is maintained, but you can insert an EQ effect immediately before it. Hopefully the engine has a good enough EQ effect to compensate. Godot's audio documentation isn't quite as good as the docs for the rest of the engine so I wanted to make sure you knew that there is a workaround for this.

The other option to work around this while it's being fixed is just boosting the highs on your actual audio files. Both of these should also work in a fairly cross-platform way since I think the frequency response of this new resampling filter is fairly consistent whether it's actually resampling or not (unlike the old one).

Also I'd like reduz's opinion on how to solve this before I go and build something, I know he's busy but he'll be the one reviewing the code so I think it's reasonable to get him involved early so I don't waste time pursuing dead ends. Perhaps he'll just want to revert #46086 for example.

nicolasGab commented 3 years ago

Thanks for the tip, I think I can live with rolled-off highs for a few months or more knowing that the issue is taken into consideration. I don't think EQ'ing the original tracks is the way to go, especially if this is fixed in a future release. Thank you for your help!

Chlorobyte-but-real commented 3 years ago

Confirmed on Linux, I guess.

troy-lamerton commented 3 years ago

To summarize:

WAV is raw audio (not compressed). OGG is compressed, so godot must decode the audio data before playback.

The cause could be:

A. the .ogg file is invalid/corrupt B. godot reads the .ogg file incorrectly C. godot audio playback code messes with the audio of .ogg files

Note that the Opus format is better than OGG, but godot does not support Opus yet. See: https://github.com/godotengine/godot-proposals/issues/870

nicolasGab commented 3 years ago

Hi @troy-lamerton, I believe the cause of the problem has already been identified as a side-effect of the resampling algorithm (even when sample rates match). It is detailed in the paper mentioned by Ellen. The high-frequency behavior isn't really strange, it's just a low pass filter.

ellenhp commented 3 years ago

Just want to reiterate that I'm willing to do the work to fix this but I would like reduz to weigh in on how. I have some ideas but none of them are great.

I noticed in #49759 that there was a complaint about audio quality of the new filter but I really do think that it's preferable to have the frequency response of the audio stream player be near-identical regardless of whether it's resampling or not. As a developer I can't control whether the engine will have to resample a given audio file on any particular user's system, since I don't know the details of their system. I'd prefer to know what the game will sound like when I build it, rather than deal with sound quality issues that only appear on certain systems. Still, it would be nice if we could get those highs boosted back up to a reasonable level.

troy-lamerton commented 3 years ago

When playing game audio from .ogg files, high frequencies (notably above 10kHz) are attenuated. Upon measuring frequency levels for a single track, I found a 10dB difference around 15kHz between the original (.ogg) audio and the game playback (see following graphs). This does not happen when using .wav files.

Just checking if the reported issue is already fixed. Because this issue is a deal-breaker for me, I cannot use an engine that has unreliable audio.

So I can build a project with compressed audio files (like .ogg), so nice small build, and they will play correctly?

ellenhp commented 3 years ago

@troy-lamerton As far as I know this issue has not been fixed, but there are several things you can do to mitigate it, the easiest of which is putting all your mp3s/oggs on a different audio bus and adding an EQ effect to it and boosting the highs yourself. https://github.com/godotengine/godot/issues/49131#issuecomment-852267579

I might go and make a fix but without input from the reviewers upfront I don't expect it'll be merged without some difficulty. I guess I'll just boost the highs of all AudioStreamPlaybackResampled streams using the EQ10 code? I think that in this case it is appropriate to just do a straight copy of the audio filter code because reusing it in a one-off way invisible to the user would involve breaking some abstractions that make the design of the audio system really nice.

Adding a separate EQ to each audio stream would make AudioStreamPlaybackResampled players scale poorly which I can evaluate on my hardware and report back the results of, if I do come up with a fix. It's no secret that WAV files scale to many simultaneously playing sounds the best, but I think in a lot of cases there are still valid reasons to play many MP3s/OGGs at the same time so it would be good to at least know the performance impact of this before merging.

reduz commented 3 years ago

My view on this is that, having written plenty of synthesizers and trackers in the past, for such a small amount of resampling (say 44k to 48khz) to hear ringing with cubic interpolation is difficult to understand for me. In my experience, aliasing is heard when there is information loss (from higher to lower hz) which I hardly think is the case with any DAC hardware nowadays, but I know some people are very sensitive to those things and my ears are not great (I have high freq tinnitus). I would advise reverting to cubic for the time being.

I would also really like to see examples of files that create significantly audible ringing with cubic interpolation, so we have a good starting point to compare if we need to look for something better.

ellenhp commented 3 years ago

Sounds good. I agree that cubic should be good enough, but the ringing is definitely there. I'll send a PR to revert to cubic and keep digging, maybe the root cause of #23544 is a bug in the resampling algorithm that only exists in the mp3/ogg code. wavs are resampled using cubic resampling as well, but don't display the same ringing

The first post of #23544 has a really egregious example if you want to look at it, by the way. Thanks for weighing in btw! I know you're busy with 4.0 stuff and I appreciate you taking some time for this :)

troy-lamerton commented 3 years ago

@ellenhp Can you point me to the code that reads the OGG file?

Its easy to misread media metadata, such as the sample rate which is usually in the file header.

I'm familiar with media file specifications and how to read/write audio bytes.

It's also possible that we're seeing another buffer offset bug, similar to the ones that Ellen already fixed.

ellenhp commented 3 years ago

modules/stb_vorbis/audio_stream_ogg_vorbis.cpp is where most of the action happens, and you can follow the code from there into the thirdparty/libvorbis code if you're interested. Full disclosure I have a lot of stuff going on in my life right now so my capacity is limited, but I haven't completely given up on this one yet. You're obviously welcome to help though, I'd love more people to take an interest in godot audio. :)

This morning I did the git revert to roll back the PR I made that introduced this lowpass bug then I took a nice walk and now I'm going to try and do take 2 of isolating the ringing described in #23544. I have some suspicions but we'll see if I end up being able to find it.

ellenhp commented 3 years ago

Reduz was right about cubic resampling sounding great. It turns out that Godot didn't use cubic interpolation though. It used some polynomial interpolation from stackoverflow that wasn't implemented correctly at all. Other folks have commented on how bad that algorithm sounds, which is what #23544 was all about. PR incoming to roll back the change that caused this issue and implement proper cubic resampling.

@nicolasGab could I enlist your help to test the fix? See docs for details on how to grab the CI builds from the PR. I want to make sure that this is the last resampling PR I need to do, and I don't want to cause any more bugs! :)

Will link the PR in this issue once it's up. I'd love other people to test it too.

edit: PR is #51082

nicolasGab commented 3 years ago

Hi all, I finally came around to testing this. It looks good, the frequency dip after 10Khz is no more. @ellenhp

White noise response:

white noise gd 34