Fix Capture and Record AudioEffect bugs for surround systems#92532
Fix Capture and Record AudioEffect bugs for surround systems#92532BuzzLord wants to merge 1 commit intogodotengine:masterfrom
Conversation
3dca84f to
c3f8750
Compare
|
Is anyone able to take a look at this? I can appreciate it might be hard to test without an affected audio setup. I have a (crude) change to the Audio Dummy Driver that could help with testing, if the audio setup is too much of an issue. The change just exposes the speaker mode as a configurable parameter when the dummy driver is active in the editor. |
|
Letting the tests run. |
I have a game where the entire gameplay is recording and playing back audio via AudioEffectRecord from players' microphones, and many players have reported that the recording doesn't work. Input is working, because they can hear themselves when the bus isn't muted and AudioEffectSpectrumAnalyzer works just fine. Only the recording is failing. Do you think my game's issue could also be due to the bug you describe? If so, what's the best way to try out your fix in my version of Godot? Apologies, I'm unfamiliar with both Github and how to change the Godot Engine's own code. |
|
I'm also experiencing this - as long as the output is a stereo 2.0 channel, recording works fine, but any HiFi / Surround configuration makes AudioEffectCapture unusable for my use case (speech to text). |
I do think this is the bug. AudioEffectRecord only records from the last channel (because of the bug), which on anything more than a stereo system will be silent (microphones normally only record stereo to the first channel). This should fix it. The SpectrumAnalyzer is one of the only effects that is channel aware, which is why it works. You select the effect instance (which is per-channel) with
Unfortunately I'm new here too, and am not sure what the best way to test would be. I setup a dev environment to build and test my own releases, but I don't think sharing executables is the way to go. Maybe one of the main contributors can chime in.
If this is the bug, the audio should sound very garbled and static-y. A work around I use in my own project (in GDScript) is to pull frames from the capture buffer in chunks of 512. Then before passing that array on, I loop over it, and check that both channels of every frame is exactly equal to 0.0. If so, I skip that chunk. This works because the |
c3f8750 to
b5a57de
Compare
|
@OpenFormEon I was looking at the Godot Contribution Workflow docs and see that you can download the editor with this PR applied for testing, once the workflow/tests are run on it again, or by using the nightly.link website. Details available here. |
|
is there any solution, I have the same problem in my Ubuntu 24 the same code runs fine in Windows. AudioEffectCapture get_buffer() will return only [(0,0), (0,0), (0,0)..... (0,0)] |
From the description, I'm not sure this is the same bug. It should be OS agnostic, but maybe the Ubuntu audio driver being used is a surround sound configuration, whereas the Windows system is stereo. The effect on Are you able to pass the audio you're trying to capture out the master audio bus, to confirm it's actually the audio effect that is at fault, rather than the audio device or driver configuration? |
b5a57de to
08f92e9
Compare
08f92e9 to
65e7382
Compare
|
Hi! Instead of adding the "set_current_channel" method to the effect, wouldn't it be better to pass the current channel to the process method? By changing the method signature like this? Also, instead of selecting a single channel for the capture effect, wouldn't it be better to move the RingBuffer inside the effect instance? The methods to retrieve the buffer could be moved to the instance (accessible through AudioServer.get_bus_effect_instance()), and they could also remain on the "total effect," which would handle combining all instance buffers and averaging all frames. |
That could work... I decided on
I felt
This was actually something I had thought of after I finished the current implementation, and was hoping someone might bring up. I do think it could be better by moving the buffer into the instance. My initial criticism of the idea was I didn't want to add the burden of storing up to 4x the memory for the buffers... but actually writing it out now, that seems a little silly. The buffers are tiny, relatively speaking, so it's probably not as much of an issue as I initially thought. I'm not sure about the averaging frames, though. That doesn't make sense to me from an audio processing point of view. I think a better option would be for the main audio effect to just return the channel 0 instance data. So using the effects works as intended for 99% of cases (and existing code works without change), but if you need other channels, you can access them via the This change would also mean I could remove the I like this solution better than the current one, so I'll implement it on another branch (for now), and see how it turns out. In the meantime, any contributors/reviewers (or anyone else) have thoughts or questions about it? |
|
As for the "set_current_channel," even if it's no longer used, I would suggest keeping it as a standard so that future effects can generally know which channel they are on. As for averaging all the ring buffers into a single one, I agree with you. We can start with just channel 0, as long as the individual buffers are accessible—processing the audio in other ways won’t be complicated. |
65e7382 to
e2b80a9
Compare
|
Updated the PR. The changes didn't feel significant enough to justify making a different PR, so I just pushed them here. I removed the I rearranged the cpp files a bit to hopefully give an easier to follow order to them. I put effect instance methods at the top, starting with process, then other important methods, then other setters/getters, and finally _bind_methods and constructor/destructor. Then the base effect methods are after, following the same pattern. It makes the PR kind of messy, but hopefully the long term benefits outweigh the mess. I updated the built in documentation for the affected classes, but I'm not particularly happy with it, and would welcome changes or feedback on it. I changed AudioEffectInstance to now expose |
30d5222 to
49049f9
Compare
Thank you, waiting for 4.6 then :) |
0f1cb2a to
dcf0d67
Compare
|
So I added some I thought setting a default value in the Edit: I've been pointed to Handling compatibility breakages, will fix this up then get some eyes on the changes. |
3035e60 to
a834e99
Compare
a834e99 to
b58bd08
Compare
|
I'm not really in the audio team, but what is keeping this pull request from being active? It's currently a draft. |
|
I think I switched it to draft while I was making changes, and was waiting for a check to run, then forgot to recheck it (or rather, thought I would get an email about something...). |
b58bd08 to
1000459
Compare
33ad474 to
1d88a64
Compare
|
I made some more changes to this, to fix some issues I still had with how instances were configured. I didn't like having a separate initialization function for instances (i.e. So I removed
|
1d88a64 to
0f7ac6c
Compare
|
So the bit @Ivorforce picked up on was where you set the temporary channel value in the The better option is to set the channel value in the This appears to be more a requirement on the What if you just pushed the |
I could make that would work, and is almost exactly the same as what I had before with Thinking about it a bit, I could actually remove
void AudioServer::_update_bus_effects(int p_bus) {
for (int i = 0; i < buses[p_bus]->channels.size(); i++) {
buses.write[p_bus]->channels.write[i].effect_instances.resize(buses[p_bus]->effects.size());
for (int j = 0; j < buses[p_bus]->effects.size(); j++) {
Ref<AudioEffectInstance> fx = buses.write[p_bus]->effects.write[j].effect->instantiate();
fx->set_current_channel(i);
buses.write[p_bus]->channels.write[i].effect_instances.write[j] = fx;
}
}
for (int j = 0; j < buses[p_bus]->effects.size(); j++) {
buses.write[p_bus]->effects.write[j].effect->finalize();
}
}Thoughts? |
Adds new channel specific data to the AudioEffect, so the effect (and effect instances) can be made audio channel aware: - AudioEffectInstances have a `current_channel` variable that is set by the AudioServer after instantiation. - AudioEffects have a new virtual function `finalize` that is called after all AudioEffectInstances have been instantiated. These two changes are used to refactor AudioEffectCapture and AudioEffectRecord, to make them audio channel aware. All the capture/record logic from each class is moved into their respective AudioEffectInstance classes, with the base AudioEffect class keeping a Vector of references to the instances. The interface of each AudioEffect was copied to the instance classes. The methods of the base AudioEffects were changed to have a `p_channel` parameter on them, which will pass the method calls through to the channel specific instance. The new params are optional, defaulting to channel 0, which will retain the existing functionality for the non-surround sound case (which worked previously).
0f7ac6c to
d8f1ce2
Compare
|
I implemented the changes described in my previous comment. Here is a small demo project that I made for testing and demonstrating changes to Attached is a video of the demo scene. The first run is default Godot 4.5 playing back audio captured with 7.1 surround sound. audio-capture-demo-2026-01-12.mp4 |
The
AudioEffectCaptureandAudioEffectRecordclasses do not work properly when there is more than one audio channel active. This PR makes some changes to severalAudioEffectclasses to fix these bugs. Fixes #91133.I added a
channelvariable toAudioEffect, which is set when audio bus effects are updated, for each channel right beforeAudioEffect::instantiate()is called, so that channel specific configuration can be done. It's not exposed as a property, but is accessible with a read-onlyget_channel().Also added to
AudioEffectis a new virtual functionset_channel_countthat an effect can override if they want access to channel count (this will matchAudioServer::get_channel_count()). It gets called when audio bus effects are updated, before theAudioEffectInstances are instantiated.Using these two changes, the
AudioEffectCaptureandAudioEffectRecordhave been refactored. All of the core recording/capturing logic has been moved from the baseAudioEffectclasses into the respectiveAudioEffectInstanceclasses, along with the public interface methods. The base effects keep a vector of the instances, keyed by the channel number.The
AudioEffectCapture/AudioEffectRecordinterface methods have been changed to take a new optionalchannelparameter (defaults to 0), which is used to pass the calls through to the requestedAudioEffectInstancemethods. However,AudioEffectproperties (buffer_lengthfor capture, andformatfor record) are not instance specific: they keep local copies of the values, and pass the set value on to all instances.Update (2025/12/30): I didn't like the previous solution, which used a
virtual init_instance()method onAudioEffectto do extra channel specific configuration to anAudioEffectInstance. I removedset_current_channelfromAudioEffectInstance, andinit_instancefromAudioEffect, and instead have achannelvariable onAudioEffect, set beforeinstantiate()gets called.Added a virtual function
set_channel_counttoAudioEffectthat can be overridden if an effect wants the total number of channels in the system at runtime.Also changed the capture/record
instancesarrays to vectors, which get resized byset_channel_countto avoid the edge case where some instance refs were sitting in there after the number of channels changed.Updated the description above, and removed all the deprecated notes.