audiobox/assets/
loader.rs1use super::{AssetKey, AudioCache};
2use crate::{Audio, DecodeOptions, Error, ErrorCode, Result, decode_with_options};
3use std::fs::File;
4use std::io::Read;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7use std::sync::mpsc::{self, Receiver, SyncSender, TryRecvError, TrySendError};
8use std::thread::{self, JoinHandle};
9
10#[derive(Clone, Debug)]
12pub struct LoaderConfig {
13 pub cache_bytes: usize,
15 pub queue_capacity: usize,
17 pub max_input_bytes: usize,
20 pub decode: DecodeOptions,
22}
23
24impl Default for LoaderConfig {
25 fn default() -> Self {
26 Self {
27 cache_bytes: 64 * 1024 * 1024,
28 queue_capacity: 16,
29 max_input_bytes: 64 * 1024 * 1024,
30 decode: DecodeOptions::default(),
31 }
32 }
33}
34
35pub struct LoadHandle {
38 receiver: Receiver<Result<Arc<Audio>>>,
39 taken: bool,
40}
41
42impl LoadHandle {
43 pub fn try_take(&mut self) -> Option<Result<Arc<Audio>>> {
47 if self.taken {
48 return None;
49 }
50 let result = match self.receiver.try_recv() {
51 Ok(result) => result,
52 Err(TryRecvError::Empty) => return None,
53 Err(TryRecvError::Disconnected) => Err(disconnected()),
54 };
55 self.taken = true;
56 Some(result)
57 }
58
59 pub fn wait(self) -> Result<Arc<Audio>> {
62 if self.taken {
63 return Err(Error::invalid("Load result already taken"));
64 }
65 self.receiver.recv().map_err(|_| disconnected())?
66 }
67}
68
69enum Input {
70 Bytes(Vec<u8>),
71 File(PathBuf),
72}
73
74struct Request {
75 key: AssetKey,
76 input: Input,
77 result: SyncSender<Result<Arc<Audio>>>,
78}
79
80pub struct Loader {
88 sender: Option<SyncSender<Request>>,
89 worker: Option<JoinHandle<()>>,
90 max_input_bytes: usize,
91}
92
93impl Loader {
94 pub fn new(config: LoaderConfig) -> Result<Self> {
96 if config.queue_capacity == 0 {
97 return Err(Error::invalid("Loader queue capacity must be positive"));
98 }
99 let (sender, receiver) = mpsc::sync_channel(config.queue_capacity);
100 let max_input_bytes = config.max_input_bytes;
101 let worker = thread::Builder::new()
102 .name("audiobox-loader".into())
103 .spawn(move || run(receiver, config))?;
104 Ok(Self {
105 sender: Some(sender),
106 worker: Some(worker),
107 max_input_bytes,
108 })
109 }
110
111 pub fn load_bytes(&self, name: impl Into<String>, bytes: Vec<u8>) -> Result<LoadHandle> {
114 if bytes.len() > self.max_input_bytes {
115 return Err(Error::limit("Encoded input exceeds the loader limit"));
116 }
117 self.submit(AssetKey::Name(name.into()), Input::Bytes(bytes))
118 }
119
120 pub fn load_file(&self, path: impl AsRef<Path>) -> Result<LoadHandle> {
123 let path = path.as_ref();
124 let path = if path.is_absolute() {
125 path.to_owned()
126 } else {
127 std::path::absolute(path)?
128 };
129 self.submit(AssetKey::Path(path.clone()), Input::File(path))
130 }
131
132 fn submit(&self, key: AssetKey, input: Input) -> Result<LoadHandle> {
133 let (result, receiver) = mpsc::sync_channel(1);
134 let request = Request { key, input, result };
135 match self.sender.as_ref().unwrap().try_send(request) {
136 Ok(()) => Ok(LoadHandle {
137 receiver,
138 taken: false,
139 }),
140 Err(TrySendError::Full(_)) => Err(Error::limit("Loader request queue is full")),
141 Err(TrySendError::Disconnected(_)) => Err(disconnected()),
142 }
143 }
144}
145
146impl Drop for Loader {
147 fn drop(&mut self) {
148 self.sender.take();
149 if let Some(worker) = self.worker.take() {
150 let _ = worker.join();
151 }
152 }
153}
154
155fn disconnected() -> Error {
156 Error::new(ErrorCode::Io, "Audio loader worker disconnected")
157}
158
159fn run(receiver: Receiver<Request>, config: LoaderConfig) {
160 let mut cache = AudioCache::new(config.cache_bytes);
161 for request in receiver {
162 let result = if let Some(audio) = cache.get(&request.key) {
163 Ok(audio)
164 } else {
165 load(request.input, &config).map(|audio| {
166 let audio = Arc::new(audio);
167 let _ = cache.insert(request.key, Arc::clone(&audio));
169 audio
170 })
171 };
172 let _ = request.result.send(result);
174 }
175}
176
177fn load(input: Input, config: &LoaderConfig) -> Result<Audio> {
178 let bytes = match input {
179 Input::Bytes(bytes) => bytes,
180 Input::File(path) => {
181 let file = File::open(path)?;
182 if file.metadata()?.len() > config.max_input_bytes as u64 {
183 return Err(Error::limit("Encoded file exceeds the loader limit"));
184 }
185 let mut bytes = Vec::new();
186 file.take((config.max_input_bytes as u64).saturating_add(1))
188 .read_to_end(&mut bytes)?;
189 if bytes.len() > config.max_input_bytes {
190 return Err(Error::limit("Encoded file exceeds the loader limit"));
191 }
192 bytes
193 }
194 };
195 decode_with_options(&bytes, &config.decode)
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn full_queue_and_disconnected_worker_report_errors() {
204 let (sender, receiver) = mpsc::sync_channel(1);
206 let loader = Loader {
207 sender: Some(sender),
208 worker: None,
209 max_input_bytes: 16,
210 };
211 let mut first = loader.load_bytes("first", vec![]).unwrap();
212 assert!(first.try_take().is_none());
213 assert_eq!(
214 loader.load_bytes("full", vec![]).err().unwrap().code,
215 ErrorCode::LimitExceeded
216 );
217 drop(receiver);
218 assert_eq!(first.try_take().unwrap().unwrap_err().code, ErrorCode::Io);
219 assert!(first.try_take().is_none());
220 assert_eq!(first.wait().unwrap_err().code, ErrorCode::InvalidArgument);
221 assert_eq!(
222 loader.load_bytes("closed", vec![]).err().unwrap().code,
223 ErrorCode::Io
224 );
225 }
226}