如何从多次运行的移动 FnMut 闭包中读取文件?

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

我正在使用 glutin,因此我的程序的主循环有一个移动闭包,我正在尝试使用 rodio 箱子播放音频文件。使用以下代码一切正常,每次程序循环时我都会发出一声哔哔声:

...
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);
    ...
});
...

但是,这非常慢,因为每次循环时我都打开同一个文件,所以我尝试使用以下方法修复它:

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

但是现在编译器给我以下错误信息:

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

我已经尝试解决这个问题一段时间了,但没有成功,任何见解都将不胜感激。

问题

基本上,手头的问题是 rodio::Decoder::new 消耗了它读取的值(好吧,实际上它已经被 BufReader::new 消耗了)。所以,如果你有一个可以多次调用的循环或闭包,你必须每次都想出一个 fresh 值。这就是 File::open 在您截取的第一个代码中所做的。

在你截取的第二个代码中,你只创建了一个 File 一次,然后尝试使用它 multiple 次,Rust 的所有权概念阻止你这样做。

另请注意,遗憾的是,使用引用并不是 rodio 的真正选择,因为解码器必须是 'static(例如,参见 [=19= 上的 Sink::append 特征绑定]).

解决方案

如果您认为您的文件系统有点慢,并且想要对其进行优化,那么您实际上可能想要预先 读取 整个文件(File::open 不做)。这样做还应该为您提供一个可以克隆的缓冲区(例如 Vec<u8>),从而允许重复创建 fresh 值,这些值可以被 Decoder.这是一个这样做的例子:

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);
});

然而,根据我个人使用 rodio 的经验,在 Decoder 中仍有很多处理要做,所以我也预先进行解码并使用 rodio::source::Buffered 包装器,像这样:

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());
});

如果你在多线程环境中使用它,或者就像静态一样,你也可以使用 lazy_static crate 使这些 rodio::source::Buffered 实例在整个程序中可用,同时进行此初始化只有一次。