Rust Audio

Apply arbitrary effects to a Source

I’m trying to apply custom effects to audio files, so the user can type something like:

cargo run -- dubstep.wav "speed 1.25" "reverb 200 1.3" "vibrato 0.1 0.3" "vibrato 10 0.0001" "custom_effect 42"

etc.

So I implemented something like this:

let decoder = rodio::Decoder::new(file).unwrap();
let sound: &mut (dyn rodio::Source<Item = i16> + Send + Sync) = &mut decoder;

while let Some(effect) = args.next() {
    match effect.as_str() {
        "speed" => {
            let ratio = args.next().unwrap().parse().unwrap();
            *sound = sound.speed(ratio);
        }
        _ => panic!("unknown effect {:?}", effect),
    }
}

let (_stream, stream_handle) = rodio::OutputStream::try_default().unwrap();
let sink = rodio::Sink::try_new(&stream_handle).unwrap();

sink.append(sound); // made a custom impl<S: Source> Source for &mut S
sink.play();
sink.sleep_until_end();

However compilation always end up with these errors:

error: the `speed` method cannot be invoked on a trait object
   --> src/main.rs:26:23
    |
26  |                 sound.speed(ratio);
    |                       ^^^^^
    |
   ::: /home/h/dev/caudio/rodio/src/source/mod.rs:264:15
    |
264 |         Self: Sized,
    |               ----- this has a `Sized` requirement

Do you have ideas on what approach to take here?

Thank you very much.

Edit: full code is here: Rust Playground

Hi @hhirtz, welcome to this forum!

Ok, there’s a lot going on here. I honestly must say that I don’t fully grasp the errors, I think it has to do with the fact that *sound has an unsized data type and that for instance the method speed expects the self parameter to be sized. I have no idea how this should be solved and even if it’s a good idea to solve it.

There’s a more fundamental problem going on – I think – on the line *sound = sound.speed(ratio). This is in a while loop and sound will be overwritten in the next iteration. So the lifetime of the value of sound is only to the end of the while loop. I’m not sure, but I think that if we somehow manage to get around the problems of the sizedness, we will end up with the next problem, that is related to the lifetimes (basically, *sound holds an &mut reference to something that only existed in the previous iteration of the loop). So this leads me to the idea of making sure that sound can outlive the while loop by using a boxed trait object instead of an &mut trait object.
Edit: this probably wasn’t clear at all. The point I want to make is the following. sound has type &mut (dyn rodio::Source<Item = i16> + Send + Sync + 'a), where 'a is an anonymous lifetime. If we somehow fix the sizedness issue, the compiler will probably complain that it cannot infer an appropriate lifetime for 'a due to conflicting requirements. That’s what I tried to explain above. Thinking about it some more, I think that the sizedness issue is the fundamental problem in the original code: everything lives on the stack, but the size needed to store this can only be determined at run time since it depends on the command line arguments.

Anyway, here’s the code:

use rodio::Source;
const USAGE: &str = "caudio

usage: caudio <file> [effects]

effects:
    speed FACTOR    Change audio speed
";
fn main() {
    let mut args = std::env::args().skip(1);

    let input = match args.next() {
        Some(filename) => std::fs::File::open(filename).unwrap(),
        None => {
            eprint!("{}", USAGE);
            std::process::exit(1);
        }
    };

    let decoder = rodio::Decoder::new(input).unwrap();
    let mut sound: Box<dyn rodio::Source<Item = i16> + Send + Sync> = Box::new(decoder);

    while let Some(effect) = args.next() {
        match effect.as_str() {
            "speed" => {
                let ratio = args.next().unwrap().parse().unwrap();
                sound = Box::new(sound.speed(ratio));
            }
            _ => panic!("unknown effect {:?}", effect),
        }
    }

    let (_stream, stream_handle) = rodio::OutputStream::try_default().unwrap();
    let sink = rodio::Sink::try_new(&stream_handle).unwrap();

    sink.append(sound);
    sink.play();
    sink.sleep_until_end();
}

The nice thing is: this also solves the sizedness problem and removes the need to patch rodio :slight_smile:

1 Like

Thank you very much! Making sound a box works.

Will still add some cool effects to Source, though!