-
-
Notifications
You must be signed in to change notification settings - Fork 21.4k
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
Conversation
There was a problem hiding this 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
if (!vorbis_data_playback->next_ogg_packet(&packet)) { | ||
have_packets_left = false; | ||
WARN_PRINT("ran out of packets in stream"); | ||
//return -1; |
There was a problem hiding this comment.
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?
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
If you're not done with this please mark it as draft, top right under the reviews |
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. |
No problem! |
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? |
See here |
All fixed, thank you again! |
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. |
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. |
Does it now completely fix the issue you referenced? |
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. |
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! 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. I got sometimes the warning
when
And I had some file drop issues in the editor ( |
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: 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 |
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. 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. |
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? |
@strellydev
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 : )
Hm… maybe. But less likely (says my gut). Pulling some stuff from my docs in the hope it helps… Regarding the third party libs: 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
|
First things first: I applied the 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
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)? EDIT: On a positive note: I could not trigger the burning warning. Not even once |
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. 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: |
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. |
@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 need some tea, there are several dozen files... |
@MJacred Sure thing. |
@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. 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
@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! |
Imma dance |
@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. 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. |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing teamwork everyone!
Thanks everyone, and congrats @strellydev and @bs-mwoerner for your first merged Godot contribution 🎉 |
@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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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. |
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.