How to read a file from within a move FnMut closure that runs multiple times?

Viewed 225

I'm using glutin and so have a move closure for my program's main loop and I'm trying to play an audio file with the rodio crate. With the following code everything works and I get one beep every time the program loops:

...
let sink: rodio::Sink = ...;
event_loop.run(move |event, _, control_flow | {
    let sound_file = File::open("beep.ogg").unwrap();
    let buf_wrap = BufReader::new(sound_file);
    let source = rodio::Decoder::new(buf_wrap).unwrap();
    sink.append(source);
    ...
});
...

However, this is incredibly slow as I'm opening the same file every time it loops, so I try fixing it with the following:

...
let sound_file = File::open("beep.ogg").unwrap();
event_loop.run(move |event, _, control_flow | {
    let buf_wrap = BufReader::new(sound_file);
    ...
});
...

But now the compiler gives me the following error message:

error[E0507]: cannot move out of `sound_file`, a captured variable in an `FnMut` closure
  --> src/lib.rs:86:33
   |
83 |     let sound_file = File::open("beep.ogg").unwrap();
   |         ---------- captured outer variable
...
86 |         let buf_wrap = BufReader::new(sound_file);
   |                                       ^^^^^^^^^^ move occurs because `sound_file` has type `File`, which does not implement the `Copy` trait

I've been trying to fix this for a while now with no success, any insight would be greatly appreciated.

1 Answers

The Problem

Basically, the problem at hand is that rodio::Decoder::new consumes the value which it reads from (well, actually it is already consumed by BufReader::new). So, if you have a loop or a closure that can be called multiple times, you have to come up with a fresh value each time. This what File::open does in your first code snipped.

In your second code snipped, you only create a File once, and then try to consume it multiple times, which Rust's ownership concept prevents you from doing.

Also notice, that using reference is sadly not really an option with rodio since the decoders must be 'static (see for instance the Sink::append trait bound on S).

The Solution

If you think your file system is a bit slow, and you want to optimize this, then you might actually want to read the entire file up-front (which File::open doesn't do). Doing this should also provide you with a buffer (e.g. a Vec<u8>) that you can clone, and thus allows to repeatedly create fresh values that can be consumed by the Decoder. Here is an example doing this:

use std::io::Read;
let sink: rodio::Sink = /*...*/;
// Read file up-front
let mut data = Vec::new();
let mut sound_file = File::open("beep.ogg").unwrap();
sound_file.read_to_end(&mut data).unwrap();

event_loop.run(move |event, _, control_flow | {
    // Copies the now in-memory file content
    let cloned_data = data.clone();
    // Give the data a `std::io::Read` impl via the `std::io::Cursor` wrapper
    let buffered_file = Cursor::new(cloned_data);
    let source = rodio::Decoder::new(buffered_file).unwrap();
    sink.append(source);
});

However, in my personal experience with rodio there is still quite a lot of processing being done in the Decoder, so I also do the decoding up-front and use a rodio::source::Buffered wrapper, like this:

use std::io::Read;
let sink: rodio::Sink = /*...*/;
// Read & decode file
let sound_file = File::open("beep.ogg").unwrap();
let source = rodio::Decoder::new(file).unwrap();
// store the decoded audio in a buffer
let source_buffered = source.buffered();

// At least in my version, this buffer is lazyly initialized,
// So, maybe, you also want to initialize it here buffer, e.g. via:
//source_buffered.clone().for_each(|_| {})

event_loop.run(move |event, _, control_flow | {
    // Just copy the in-memory decoded buffer and play it
    sink.append(source_buffered.clone());
});

If you use this in a multithreaded environment, or if just like statics, you can also use the lazy_static crate to make these rodio::source::Buffered instance available in the entire program, while doing this initialization only once.

Related