Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix OGG audio loop offset pop #80452

Merged
merged 1 commit into from Oct 13, 2023
Merged

Fix OGG audio loop offset pop #80452

merged 1 commit into from Oct 13, 2023

Conversation

ghost
Copy link

@ghost ghost commented Aug 9, 2023

This fixes #75762, fixes #59348 and parts of #64775

Fixed the mixing function so it doesn't skip the last ogg packet of the file, and also made it so the burning algorithm only burns the needed samples from the last packet, and not the whole packet.

There are still some issues, if you download the example project from the issue, and you try the audio-pop-no-intro.ogg file, you'll see that there's still a pop, this is because for some reason, the engine is missing 38 samples, and not at the end of the file like the error originally was.

Another problem that became aparent is the decimal precision of the loop offset variable, in a song of mine which has a loop point at 31.46, the loop offset becomes 31.459991 or something like that, which introduces a tiny pop (this does not happen in Godot 3). If I manually change that value to 31.4600010, the pop disappears and the song plays like it does in Godot 3. So that's another problem somewhere else.

THIS DOES NOT FIX THE WAV POP.

However, it seems to have fixed the mp3 pop I was getting, so I guess it could have been a simple floating point precision error, but more testing is probably needed.

I'll keep looking into it.

If you know why the 38 samples from the 0 seconds offset loop could be going missing please let me know.

Thanks.

@ghost ghost self-requested a review as a code owner August 9, 2023 16:50
Copy link
Member

@AThousandShips AThousandShips left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for contributing, some style checks

modules/ogg/ogg_packet_sequence.cpp Outdated Show resolved Hide resolved
modules/vorbis/audio_stream_ogg_vorbis.cpp Outdated Show resolved Hide resolved
modules/vorbis/audio_stream_ogg_vorbis.cpp Outdated Show resolved Hide resolved
if (!vorbis_data_playback->next_ogg_packet(&packet)) {
have_packets_left = false;
WARN_PRINT("ran out of packets in stream");
//return -1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens below if the packet isn't loaded correctly?

@AThousandShips
Copy link
Member

AThousandShips commented Aug 9, 2023

Please update your commit message to be more brief and descriptive, like the PR title, you can add the description in the body of the message

I'll keep looking into it.

If you're not done with this please mark it as draft, top right under the reviews

@ghost ghost marked this pull request as draft August 9, 2023 16:59
@ghost
Copy link
Author

ghost commented Aug 9, 2023

Thanks, I fixed the comments, I was using them to find my changes and forgot to remove the name, also thanks for pointing out the commented return as well.

I will make this into a draft. Sorry for the inconvenience.

@AThousandShips
Copy link
Member

I will make this into a draft. Sorry for the inconvenience.

No problem!

@ghost
Copy link
Author

ghost commented Aug 9, 2023

Oh no, I renamed the commits and somehow an older commit made it's way into this, uh... is there a way I can fix this?

@AThousandShips
Copy link
Member

See here

@ghost
Copy link
Author

ghost commented Aug 9, 2023

All fixed, thank you again!

@ghost
Copy link
Author

ghost commented Aug 9, 2023

Hello again, I fixed the issue with the offset being at 0, it didn't have to do with the offset being at 0 at all, I was just checking if the samples left in the last packet were greater than the needed samples by the mixer, instead of checking if they were greater than 0 like I should have been, I think this is ready for review.

Edit: I'm leaving the decimal precision out of the question because I don't know if that's part of the audio system, or a general thing, I honestly can't think of a reason on why it changes behavior between godot 3 and 4.

@ghost ghost marked this pull request as ready for review August 9, 2023 21:05
modules/vorbis/audio_stream_ogg_vorbis.cpp Outdated Show resolved Hide resolved
modules/vorbis/audio_stream_ogg_vorbis.cpp Outdated Show resolved Hide resolved
modules/vorbis/audio_stream_ogg_vorbis.cpp Outdated Show resolved Hide resolved
modules/vorbis/audio_stream_ogg_vorbis.cpp Outdated Show resolved Hide resolved
modules/vorbis/audio_stream_ogg_vorbis.cpp Outdated Show resolved Hide resolved
@ghost
Copy link
Author

ghost commented Aug 10, 2023

Alright, everything should be correctly styled now, and I also realized that loop_offset was a float even though the getter returns a double and the seek function receives a double as well, so I changed it to double and that also fixed the decimal point precision issue, which means ogg now fully functions the same as Godot 3 (Hopefully)

Thanks for taking the time to review this.

@AThousandShips
Copy link
Member

Does it now completely fix the issue you referenced?

@ghost
Copy link
Author

ghost commented Aug 10, 2023

It completely fixes the OGG part, however, the issue also talks about WAV and MP3 having pops as well. This pull request doesn't touch any of that.

@akien-mga akien-mga changed the title [4.x] OGG audio loop offset pop fix. Fix OGG audio loop offset pop Aug 11, 2023
@MJacred
Copy link
Contributor

MJacred commented Aug 16, 2023

First of all: Thank you so much for tackling this!


EDIT: ok… as both files had the same sample count it struck me as really weird. So I took that file with the audio pop, imported it into Audacity and exported it as-is… The pop was gone. Will test with the rest of the files and leave a second comment with the result!
What is even weirder: Audacity could loop it perfectly inside itself, so it might do some auto-corrections on import??


I tested with couple of files (all loop offsets > 0 and at full seconds). And I sometimes still get pops…

I have 3 files that are exactly 92 secs. They work fine.
Then I tested two files with 67.333 secs (and same sample count). The loop offset was exactly 6 secs for both. And only one popped.
Thus, I cut that popping file in Audacity to a total length of 5.5 secs. While the loop offset changed, it was still at 1.5 secs. No audio pop.
Therefore, I reckon there is still something wrong with the sample calculation. Especially because…

I got sometimes the warning

modules/vorbis/audio_stream_ogg_vorbis.cpp:340 - Burning negative samples doesn't make sense. Check seek algorithm.

when

  • I double-click the file in the Godot Editor and it opens the importer dialog,
  • I seek with the mouse manually
  • the file loops <<< this happened every time for that file I cut in Audacity, but not for the others

And I had some file drop issues in the editor (Failed to open [filename].ogg, no further info). But that's something else

@ghost
Copy link
Author

ghost commented Aug 16, 2023

The burning negative samples warning is something that has me weirded out as well.

I was sure that to calculate the samples to burn I could substract the previous page granule from the desired granule position, which would give us the amount of "granule" to burn in the current page. Then, we'd substract the previous granule position from the current page granule position, which would give us the granule size of the page, and then use that size to compare it to the burned samples.

Doing all of that actually gets rid of the warning, so it never tries to burn negative samples, however, it seems that calculating samples_to_burn that way is wrong, because it leads to popping.

And that doesn't make any sense to me. But doing the current calculation seems to work with the caveat of giving the warning when manually seeking through the file.

I could go in and try again with the desired granule - last granule formula. Maybe I did something wrong the first time I tried it.

This is my first time working with audio, so trying to navigate all the calculations is a bit tricky,

Thanks for testing this out, I will await your next comment!

EDIT:
Ah! I found out what's going on, the samples_in_page value is missing 1024 samples, because the first packet is returning 0 samples_out for some reason.

So if the seeked sample is on the last 1024 samples of the page, it will think it has to burn negative samples, which will later cause a pop if it so happens that the loop point coincides with the warning happening. That could explain the pop you were having.

I do not understand why samples_out is 0 with the first packet, but if I set samples_out to 1024 when it's 0 in both seeking and burning loops,, the warning disappears and no pops are introduced.

However, hard coding an extra 1024 samples seems hacky, so I'll try to figure out why it's skipping the first one.

Cheers

@ghost
Copy link
Author

ghost commented Aug 16, 2023

Is there ANY chance that the bug could come from the third party ogg vorbis code?

Because when we do vorbis_synthesis_restart(), the dsp_state->pcm_returned is set to -1, which, later, when reading the first packet after the restart, it checks if pcm_returned is -1, and if it is, it sets pcm_returned to the same value as pcm_current, which basically makes it act as if those first 1024 samples were already read., thus, making samples_out be 0 when calling vorbis_synthesis_pcmout() later.

block

If I do the change shown in the image in the file thirdparty/libvorbis/block.c, everything works fine. I didn't find anything breaking, but I don't know if changing that would have an effect on a specific scenario, or whatever. It's hard to tell.

@ghost
Copy link
Author

ghost commented Aug 16, 2023

I'm going to sleep, but you can check the current commit to see a workaround for the vorbis pcm_out being 0 situation.

If we are against changing third party code, this is a solution that works.

@MJacred can you confirm that the pops you were having are gone with this new version of the code?

@MJacred
Copy link
Contributor

MJacred commented Aug 17, 2023

@strellydev

This is my first time working with audio, so trying to navigate all the calculations is a bit tricky,

Hehe, same for me. I studied up from time to time, but was still quite a bit off from doing code fixing. So I'm really grateful to you and happy. Thanks : )

Is there ANY chance that the bug could come from the third party ogg vorbis code?

Hm… maybe. But less likely (says my gut).

Pulling some stuff from my docs in the hope it helps…

Regarding the third party libs:
According to this comment #64775 (comment) we are using stb_vorbis (in Godot 4), although it was replaced with libvorbis (aka vorbisfile) + libogg (in Godot 4) -> #52406. That has me a bit confused. Maybe Godot 3 and 4 were mixed up or there is some detail gone missing. Either way, I could not find any files of stb_vorbis in master branch.

There are various reasons not to use stb_vorbis:

Maybe these comments/docs helps with seeking/looping/mixing

I'll now commence testing all the files and give feedback in the next comment (this time for sure ™)


The referenced libs are up-to-date. If they have a bug, we can fix it here (need an account there first)

or contact via

@MJacred
Copy link
Contributor

MJacred commented Aug 17, 2023

First things first: I applied the size_t -> int fix locally.

Now to my testing:

Hm… guess I don't need to test all files (yet). So far, I could only fix one file with re-exporting from Audacity.

Thus far, I have now 2 files (more lying in wait) where the pop appeared and

  • each has other files with the same sample count and the pop does not appear.
  • The only difference I can spot so far is that their file size is different.
  • And after cutting down the file to ~7 seconds, neither pop on their loop point… (no file is longer than 2 minutes)
  • Both have again smooth loop offsets of 6 and 10 seconds respectively.

So if the sample count is not the issue. Then maybe the sample / buffer size in relation to file size (just guessing here as I've read only little audio code)?
Some debug stats coming from Godot for audio files sure would be nice (so we can see how they understand the files…)


EDIT: On a positive note: I could not trigger the burning warning. Not even once

@ghost
Copy link
Author

ghost commented Aug 17, 2023

Thanks again for testing.

After a full night of sleep and some thought at work, I came to the conclusion that that fix isn't fully correct, because when we are doing the check in the burning loop, we are not checking if the samples_skipped are greater than the samples_to_burn.
If it so happens that samples_skipped is greater (like the case in which the desired sample is in the first 1024 samples of the page), it'd lead to burning more samples than we need to, and having a pop.
I shouldn't have rushed the fix before going to sleep.

The fix should be as easy as adding a quick check to set samples_out to samples_to_burn if samples_skipped is greater than the latter. I'm in my work break so I'm gonna change it and upload it here.

Edit:
Actually, nevermind, I think by how the first packet is treated as read, it will always burn it no matter what we do, so I'm actually stumped. I guess I'll try to add the check anyway just to be sure it doesn't fix it.

@ghost
Copy link
Author

ghost commented Aug 17, 2023

As expected, the check did nothing, and it's always treating the first packet as fully burned.

However, I implemented a hacky fix to the problem. If this doesn't fix the issue, I'll mark this as draft and go back to the drawing board, to check for buffer sizes maybe as you said.

@MJacred
Copy link
Contributor

MJacred commented Oct 13, 2023

@ellenhp: Before you do the final approve, please let me test through my files as well. I'll do so this night and report back.
I can already confirm that 3 popping files are playing just fine after the most recent changes.

I need some tea, there are several dozen files...

@ellenhp
Copy link
Contributor

ellenhp commented Oct 13, 2023

@MJacred Sure thing.

@MJacred
Copy link
Contributor

MJacred commented Oct 13, 2023

@ellenhp: ok, I'm done.

So, my result is: Only one file was popping. But it also popped in Audacity. And only at a rather high volume output. Therefore a file issue.
I tested on both speakers (stereo) and audio jack headphones (yeah, sometimes it pops in one but not the other. Or at least the pops are more audible in one or the other).
Though I must say all my files have a loop offset > 0, so I didn't test looping to beginning.

The final verdict from my end: the looping is so smooth, you can slip on it.

Now I want to celebrate. All of us have earned some celebration 🥳 I'll sleep well tonight


[…] is exactly what I ended up doing

@bs-mwoerner: Haha. Kinda glad I narrowed down on the solution over time and didn't get lost. And thank you again for your contribution!

@ghost
Copy link
Author

ghost commented Oct 13, 2023

Imma dance

@AThousandShips AThousandShips modified the milestones: 4.x, 4.2 Oct 13, 2023
@ellenhp
Copy link
Contributor

ellenhp commented Oct 13, 2023

@akien-mga I think it's worth merging this despite the freeze. I'll let you make that call though.

@strellydev Thank you for your hard work tracking down this bug, isolating the problem areas and coming up with partial fixes. Also for your patience with git and all the nit comments I left.
@MJacred Thank you for the test files, help tracking down the issue and ultimately proposing a working solution.
@bs-mwoerner Thank you for getting this across the finish line.

I'm so glad this is fixed. I feel like I can breathe again. It's been a source of anxiety for me for years.

@AThousandShips
Copy link
Member

Unless this is considered an enhancement or a new feature it's unaffected by freeze, unless it's a risky fix that we want to test for longer I don't see any problem merging for 4.2

Copy link
Member

@akien-mga akien-mga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing teamwork everyone!

@akien-mga akien-mga merged commit a574c02 into godotengine:master Oct 13, 2023
15 checks passed
@akien-mga
Copy link
Member

Thanks everyone, and congrats @strellydev and @bs-mwoerner for your first merged Godot contribution 🎉

@akien-mga akien-mga added the cherrypick:4.1 Considered for cherry-picking into a future 4.1.x release label Oct 13, 2023
@ellenhp
Copy link
Contributor

ellenhp commented Oct 13, 2023

@akien-mga just to make sure you don't miss it for release notes: there are importer changes here so users will (potentially? needs testing) have to re-import ogg files to get the full effect of this fix.

@@ -159,7 +159,9 @@ bool OggPacketSequencePlayback::next_ogg_packet(ogg_packet **p_packet) const {

*p_packet = packet;

packet_cursor++;
if (!packet->e_o_s) { // Added this so it doesn't try to go to the next packet if it's the last packet of the file.
Copy link
Contributor

@nikitalita nikitalita Dec 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change can cause an infinite loop since it causes the function to NEVER return false. After we reach EOS, the packet_cursor is never incremented, so the packet_cursor always stays pointed to the last packet. This causes the function to keep setting p_packet to the last packet and returning false forever.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That may be a bit unintuitive yes. Normally, callers check the e_o_s flag and are supposed to stop reading at the end of the stream. If they don't, then they would have previously gotten false and logged an error, whereas now, they get true and the last packet again. This doesn't make a difference as long as callers correctly check the e_o_s flag, but you might argue that the previous way of erroring out when trying to read past the end of the stream was the safer route.

I think this change wasn't directly related to the fix but rather one of a few modifications made in an attempt to narrow down the original problem.

Copy link
Contributor

@nikitalita nikitalita Dec 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning a boolean value instead of an error makes it look like one of those classic reader functions that you put in the condition of a while loop and keep going until it returns false; the fact that it doesn't behave that way is very confusing, and I ended up banging my head against the wall for a couple of hours trying to figure out why I kept reading after the EOS.

In either case, I've tested removing this check with the example projects for the issues that this PR is purported to have fixed, and they work fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide an ogg/vorbis file that causes this issue? Here or in the comments of #85996

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not really a problem with any particular ogg file, it's an issue with this being part of the public API and it having confusing behavior. It's also semantically incorrect, since there's no case where this function will actually return false.

When something other than godot uses this function (e.g a custom module) and expects the next_ogg_packet() to return false when there's no more packets to be read (e.g. while (playback->next_ogg_packet(&pkt)), this can cause an infinite loop.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, you mentioned an infinite loop so I assumed it was a reproducible case rather than an API issue. Sorry for misunderstanding.

@YuriSizov
Copy link
Contributor

Unfortunately cannot be cleanly cherry-picked because it's based on the codebase which includes #78084. A dedicated PR for 4.1 is welcome, if we want to fix this issue in that branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment