bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
34.94k stars 3.41k forks source link

Audio effects #10895

Open matiqo15 opened 9 months ago

matiqo15 commented 9 months ago

What problem does this solve or what need does it fill?

Currently, bevy_audio doesn't support audio effects, which could be useful for creating a better experience for players.

What solution would you like?

Extending bevy_audio crate with an Effects struct that contains settings for audio effects. These effects could then be applied in played_queued_audio_system. rodio (which is used by bevy_audio) already exposes effects such as: fade in, high pass filter, low pass filter, amplify, delay, reverb.

Example of how Effects struct could look like:

pub struct Effects {
    fade_in: Option<Duration>,
    high_pass_filter: Option<u32>,
    low_pass_filter: Option<u32>,
    amplify: Option<f32>,
    delay: Option<Duration>,
    reverb: Option<(Duration, f32)>,
}

This struct could be added to PlaybackSettings struct:

pub struct PlaybackSettings {
    ...
    pub effects: Effects,
}

What alternative(s) have you considered?

Implementing effects as enum, but that wouldn't allow chaining effects.

Additional context

When an effect is applied, rodio wraps it in a struct. For example, if we use the fade_in() effect on a Source, it returns FadeIn<Source>. This makes it harder for implementation (or at least to my limited knowledge of rust).

TheBiochemic commented 8 months ago

When an effect is applied, rodio wraps it in a struct. For example, if we use the fade_in() effect on a Source, it returns FadeIn<Source>. This makes it harder for implementation (or at least to my limited knowledge of rust).

I think this shouldn't still be too much of an issue, because the resulting wrapped sources implement Source themselves again. So you can have some type like Reverb<FadeIn<Delay<Source>>> (Pseudocode) and it would still be an impl Source

It would be a good step into the right direction, but i would almost prefer, to have a more dynamic solution, incase i want to have my own reverb implementation with different settings for example etc. It would be interesting to maybe have some bevy type like AudioEffect that implements some sort of .wrap(impl Source) method, which inturn wraps the incoming source with the rodio effect, before giving it to the next instance.

That would create the next problem of how to display that in the PlaybackSettings, maybe as a tuple or array? Maybe using some method of chaining with tuples

It would be neat, if the result would look something like this:

PlaybackSettings {
    ...
    effects: (
        FadeIn{ duration: 10.0, dry: 0.5, wet: 0.5, ..default() },
        Delay{ decay: 3.0, ..default() },
        MyLFOFilter::default() //Hypothetical custom filter
    ).chain() //or potentially .parallel()?
}

Another issue that would create is how to solve controlling effects from the outside. And the question remains how portable this solution is to other platforms.

However, in any case i definitely support the idea of adding effects to bevy_audio

matiqo15 commented 8 months ago

It would be a good step into the right direction, but i would almost prefer, to have a more dynamic solution, incase i want to have my own reverb implementation with different settings for example etc.

What about implementing it as trait? This would allow users to create their own effects.

TheBiochemic commented 8 months ago

What about implementing it as trait? This would allow users to create their own effects.

That way, it would be possible to implement the original idea, with one combined type, that has the original config, but also keeps the option possible, to build your own "effects pipeline", i like that idea

So you would have something like:

...
pub struct StandardAudioEffects {
    fade_in: Option<Duration>,
    high_pass_filter: Option<u32>,
    low_pass_filter: Option<u32>,
    amplify: Option<f32>,
    delay: Option<Duration>,
    reverb: Option<(Duration, f32)>,
}

impl AudioEffect for StandardAudioEffects {
    fn wrap(&self, audio_source: Source) -> Source {
        ... //here all the effects are being applied with the settings in self
    }
}

and later you can implement your own stuff like that for example

pub struct MyLFO { ... }
impl AudioEffects for MyLFO { ... }

...

PlaybackSettings {
    ...
    effects: (
            StandardAudioEffects::default(),
            MyLFO::default()
        ).chain()
}

However, i'm not sure how possible this is due to my limited knowledge with rodio specifically.

Edit: what i'm also realizing is, that the same system might be applicable to channels too, so it might be possible to add effects to channels as well? Just brainstorming at this point

TheBiochemic commented 8 months ago

As suggested in the discord, it might be a restriction of rodio potentially