godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.15k stars 97 forks source link

Allow individual AudioStreamPlayer nodes to have their own audio effect stack and custom audio effects #5613

Open ghsoares opened 2 years ago

ghsoares commented 2 years ago

Describe the project you are working on

A racing game with car engine SFX.

Describe the problem or limitation you are having in your project

I'm creating a racing game where I have pre-recorded engine sounds which I added to the car AudioStreamPlayer3D, I would like to some audio post-processing to the indivual players of each car, for example rpm-based pitch scaling, high-frequency rev-limiting and reverb based on current ambient the car is located. The problem is that I need to add some of these features by modifying the sound wave itself, like a custom-filter, because some of these features would change the sound wave with high-frequency, which running in _process or _physics_process and simply changing the pitch scale and the volume wouldn't be sufficient.

Another problem is that even if there was a way to create custom audio effects, it would require to insert then in a audio bus, which would apply the processing for all the cars in the game, which isn't what I want.

I'm playing around with GDNative and AudioStreamGeneratorPlayback to generate the audio, but there is no way to modify a source audio frame, I can only generate plain new audio frames, unless I pass a AudioStreamSample to the generator script, read the frames from the data and use then to generate the new audio, but this requires a lot of effort to convert the pcm byte data to audio frames.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

So I'm proposing two features:

Creation of custom audio effects

The idea is to have a way to inherit from AudioEffect and AudioEffectInstance and expose virtual functions to modify a chunk of audio frames, it receives the source audio frames chunk and expect the processed frames, each frame is a Vector2, being x the left channel and y the right channel. It would have some problems with GDScript as not being fast enough to handle a high sample rate, but I think it would work better with GDNative or C#.

Per AudioStreamPlayer audio effect stack

The idea is the ability to have a audio effect stack for each AudioStreamPlayer instance, which is used to apply the effects in the audio frames before sending to the bus. This would be useful to apply specific post-processing effects to individual streams players, without interfering in the processing of other streams. One example is having two cars where one is inside the tunnel which have a reverb effect in the engine sound while the other car outside the tunnel sounds normal.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

For the custom audio effect, it would need to allow AudioEffect and AudioEffectInstance script inheritance, in the AudioEffect it would have instance virtual function to return a custom AudioEffectInstance and have two virtual functions for the AudioEffectInstance: void process(PoolVector2Array source_frames) to process a chunk of audio frames; bool process_silence() tells if this audio effect should process plain silence audio frames.

Note: It seens that in 4.x the API it's already exposed, at least by looking at the AudioEffectInstance documentation there's the functions exposed, but I hope there is a way to backport into 3.x, maybe I can contribute with this.

Now, for the per AudioStreamPlayer audio effect stack, it could have a interface simillar to adding effects into a bus, which adds the effect into a internal effect vector, create the effect instances for each effect everytime the stack updates and use then when mixing the audio frames buffer by calling instance->process(buffer, temp_buffer, buffer_size).

If this enhancement will not be used often, can it be worked around with a few lines of script?

For custom audio effects one would require to manually sample from a audio stream and insert modified frames into a AudioStreamGeneratorPlayback, and wouldn't be much scalable. For per AudioStreamPlayer audio effect stack, it requires core modification to change how AudioStreamPlayers send audio buffer to the audio buses.

Is there a reason why this should be core and not an add-on in the asset library?

Simillar to above, the addon would require to sample from a AudioStreamSample and feed into a overridable function to modify the audio frames, but wouldn't be able to implement per AudioStreamPlayer audio effect stack.

Calinou commented 2 years ago

Related to https://github.com/godotengine/godot-proposals/issues/1836 and https://github.com/godotengine/godot-proposals/issues/2765.

I'm creating a racing game where I have pre-recorded engine sounds which I added to the car AudioStreamPlayer3D, I would like to some audio post-processing to the indivual players of each car, for example rpm-based pitch scaling, high-frequency rev-limiting and reverb based on current ambient the car is located.

I'm surprised you need real-time audio effects to do that. Typically, engine sounds are done by pitch-shifting and blending a few prerecorded sounds together depending on RPM. No advanced real-time effects are needed. You usually only need 6 sound samples, maybe even just 5:

This is how most AAA titles still do engine sounds nowadays :slightly_smiling_face:

ghsoares commented 2 years ago

@Calinou For simple effects like linear rpm acceleration, just changing the pitching and blending the samples works, the problem comes with high frequency changes, for example rev limiting in a car with high acceleration and decceleration, it basically cuts off combustion audio when achieving max rpm and only enabling again when achieving a lower threshold. If the acceleration and decceleration is too high, it would produce a high-frequency cutoff wave, something like in this video.

With GDScript, running on _physics_process on 60hz changing the pitch scale and volume wouldn't be sufficient and would be off-sync with the audio, it would need to run the rev-limiting wave with the same frequency as the sample rate, this means that it requires a custom audio processing effect to run for each audio frame of the engine sound. It isn't as complex as one would imagine, it is just a square wave used to multiply the audio frame depending on the rev-limiting rpm if current rpm is higher than max rpm.

The car engine sound is just a example for custom audio effects, it could be used for all kind of effects involved in audio processing, which wouldn't be general enough to put on-core, so it leaves to the developer implement the whenever effect they would want.

marcinn commented 2 weeks ago

@Calinou this is not quite related to sending audio buses.

I would also like to have audio effects directly set to audio players. First, they aren't related to the bus layout, which is project wide (think about addons or reusable components). Second, I would like to implement fmod-like sound effects, where each track is an kind of audioplayer which a custom effects stack (both pre-fader and post-fader).

For both cases buses are too limiting.