zng_ext_audio/
types.rs

1use std::{
2    any::Any,
3    env, fmt, mem, ops,
4    path::{Path, PathBuf},
5    sync::Arc,
6    time::Duration,
7};
8
9use zng_app::view_process::ViewAudioHandle;
10use zng_task::channel::{IpcBytes, IpcBytesCast};
11use zng_txt::Txt;
12use zng_unit::{ByteLength, ByteUnits};
13use zng_var::{Var, VarEq, impl_from_and_into_var};
14use zng_view_api::audio::{AudioDecoded, AudioMetadata};
15
16pub use zng_app::view_process::AudioOutputId;
17pub use zng_view_api::audio::{AudioDataFormat, AudioFormat, AudioFormatCapability, AudioTracksMode};
18
19/// A custom extension for the [`AUDIOS`] service.
20///
21/// Extensions can intercept and modify requests.
22///
23/// [`AUDIOS`]: crate::AUDIOS
24pub trait AudiosExtension: Send + Sync + Any {
25    /// Modify a [`AUDIOS.audio`] request.
26    ///
27    /// Note that all other request methods are shorthand helpers so this will be called for every request.
28    ///
29    /// Note that the [`AUDIOS`] service can be used in extensions and [`AudioSource::Audio`] is returned directly by the service.
30    /// This can be used to fully replace a request here.
31    ///
32    /// [`AUDIOS.audio`]: crate::AUDIOS::audio
33    /// [`AUDIOS`]: crate::AUDIOS
34    fn audio(&mut self, limits: &AudioLimits, source: &mut AudioSource, options: &mut AudioOptions) {
35        let _ = (limits, source, options);
36    }
37
38    /// Audio data loaded.
39    ///
40    /// This is called for [`AudioSource::Read`], [`AudioSource::Download`] and [`AudioSource::Data`] after the data is loaded and before
41    /// decoding starts.
42    ///
43    /// Return a replacement variable to skip decoding or redirect to a different audio. Note that by the time this is called the service
44    /// has already returned a variable in loading state, that variable will be cached according to `mode`. The replacement variable
45    /// is bound to the return variable and lives as long as it does.
46    ///
47    /// Note that the [`AUDIOS`] service can be used in extensions.
48    ///
49    /// [`AUDIOS`]: crate::AUDIOS
50    #[allow(clippy::too_many_arguments)]
51    fn audio_data(
52        &mut self,
53        max_decoded_len: ByteLength,
54        key: &AudioHash,
55        data: &IpcBytes,
56        format: &AudioDataFormat,
57        options: &AudioOptions,
58    ) -> Option<AudioVar> {
59        let _ = (max_decoded_len, key, data, format, options);
60        None
61    }
62
63    /// Modify a [`AUDIOS.clean`] or [`AUDIOS.purge`] request.
64    ///
65    /// Return `false` to cancel the removal.
66    ///
67    /// [`AUDIOS.clean`]: crate::AUDIOS::clean
68    /// [`AUDIOS.purge`]: crate::AUDIOS::purge
69    fn remove(&mut self, key: &mut AudioHash, purge: &mut bool) -> bool {
70        let _ = (key, purge);
71        true
72    }
73
74    /// Called on [`AUDIOS.clean_all`] and [`AUDIOS.purge_all`].
75    ///
76    /// These operations cannot be intercepted, the service cache will be cleaned after this call.
77    ///
78    /// [`AUDIOS.clean_all`]: crate::AUDIOS::clean_all
79    /// [`AUDIOS.purge_all`]: crate::AUDIOS::purge_all
80    fn clear(&mut self, purge: bool) {
81        let _ = purge;
82    }
83
84    /// Add or remove formats this extension affects.
85    ///
86    /// The `formats` value starts with all formats implemented by the current view-process and will be returned
87    /// by [`AUDIOS.available_formats`] after all proxies edit it.
88    ///
89    /// [`AUDIOS.available_formats`]: crate::AUDIOS::available_formats
90    fn available_formats(&self, formats: &mut Vec<AudioFormat>) {
91        let _ = formats;
92    }
93}
94
95/// Represents an [`AudioTrack`] tracked by the [`AUDIOS`] cache.
96///
97/// The variable updates when the audio updates.
98///
99/// [`AUDIOS`]: super::AUDIOS
100pub type AudioVar = Var<AudioTrack>;
101
102/// State of an [`AudioVar`].
103///
104/// [`AUDIOS`]: crate::AUDIOS
105#[derive(Debug, Clone)]
106pub struct AudioTrack {
107    pub(crate) cache_key: Option<AudioHash>,
108
109    pub(crate) handle: ViewAudioHandle,
110    pub(crate) meta: AudioMetadata,
111    pub(crate) data: AudioDecoded,
112    tracks: Vec<VarEq<AudioTrack>>,
113
114    error: Txt,
115}
116impl PartialEq for AudioTrack {
117    fn eq(&self, other: &Self) -> bool {
118        self.handle == other.handle
119            && self.cache_key == other.cache_key
120            && self.error == other.error
121            && self.meta == other.meta
122            && self.data == other.data
123            && self.tracks == other.tracks
124    }
125}
126impl AudioTrack {
127    /// Create a dummy audio in the loading state.
128    ///
129    /// This is the same as calling [`new_error`] with an empty error.
130    ///
131    /// [`new_error`]: Self::new_error
132    pub fn new_loading() -> Self {
133        Self::new_error(Txt::from_static(""))
134    }
135
136    /// Create a dummy audio in the error state.
137    ///
138    /// If the `error` is empty the audio is *loading*, not an error.
139    pub fn new_error(error: Txt) -> Self {
140        let mut s = Self::new(None, ViewAudioHandle::dummy(), AudioMetadata::default(), AudioDecoded::default());
141        s.error = error;
142        s
143    }
144
145    pub(crate) fn new(cache_key: Option<AudioHash>, handle: ViewAudioHandle, meta: AudioMetadata, data: AudioDecoded) -> Self {
146        Self {
147            cache_key,
148            handle,
149            meta,
150            data,
151            tracks: vec![],
152            error: Txt::from_static(""),
153        }
154    }
155
156    /// Returns `true` if the is still acquiring or decoding the audio bytes.
157    pub fn is_loading(&self) -> bool {
158        self.error.is_empty() && !self.data.is_full
159    }
160
161    /// If the audio has finished loading ok or due to error.
162    ///
163    /// Note that depending on how the audio is requested and its source it may never be fully loaded.
164    pub fn is_loaded(&self) -> bool {
165        !self.is_loading()
166    }
167
168    /// If this audio can be cued for playback already.
169    ///
170    /// This is `true` when the audio has decoded enough that it can begin streaming.
171    pub fn can_cue(&self) -> bool {
172        !self.view_handle().is_dummy() && !self.is_error()
173    }
174
175    /// If the audio failed to load.
176    pub fn is_error(&self) -> bool {
177        !self.error.is_empty()
178    }
179
180    /// Returns an error message if the audio failed to load.
181    pub fn error(&self) -> Option<Txt> {
182        if self.error.is_empty() { None } else { Some(self.error.clone()) }
183    }
184
185    /// Total duration of the track, if it is known.
186    ///
187    /// Note that this value is set as soon as the header finishes decoding, the [`chunk`] may not contains the full stream.
188    ///
189    /// [`chunk`]: Self::chunk
190    pub fn total_duration(&self) -> Option<Duration> {
191        self.meta.total_duration
192    }
193
194    /// Number of channels interleaved in the track.
195    pub fn channel_count(&self) -> u16 {
196        self.meta.channel_count
197    }
198
199    /// Samples per second.
200    ///
201    /// A sample is a single sequence of [`channel_count`] in [`chunk`].
202    ///
203    /// [`channel_count`]: Self::channel_count
204    /// [`chunk`]: Self::chunk
205    pub fn sample_rate(&self) -> u32 {
206        self.meta.sample_rate
207    }
208
209    /// Current decoded samples.
210    ///
211    /// Note that depending on how the audio is requested and its source it may never be fully loaded. The [`chunk_offset`] defines the
212    /// position of the chunk in the overall stream.
213    ///
214    /// [`chunk_offset`]: Self::chunk_offset
215    pub fn chunk(&self) -> IpcBytesCast<f32> {
216        self.data.chunk.clone()
217    }
218
219    /// Offset of the [`chunk`] in the overall stream.
220    ///
221    /// [`chunk`]: Self::chunk
222    pub fn chunk_offset(&self) -> usize {
223        self.data.offset
224    }
225
226    /// If [`tracks`] is not empty.
227    ///
228    /// [`tracks`]: Self::tracks
229    pub fn has_tracks(&self) -> bool {
230        !self.tracks.is_empty()
231    }
232
233    /// Other audios from the same container that are a *child* of this audio.
234    pub fn tracks(&self) -> Vec<AudioVar> {
235        self.tracks.iter().map(|e| e.read_only()).collect()
236    }
237
238    /// All other audios from the same container that are a *descendant* of this audio.
239    ///
240    /// The values are a tuple of each track and the length of descendants tracks that follow it.
241    ///
242    /// The returned variable will update every time any track descendant var updates.
243    ///
244    /// [`tracks`]: Self::tracks
245    pub fn flat_tracks(&self) -> Var<Vec<(VarEq<AudioTrack>, usize)>> {
246        // idea here is to just rebuild the flat list on any update,
247        // assuming the audio variables don't update much and tha there are not many tracks
248        // this is more simple than some sort of recursive Var::flat_map_vec setup
249
250        // each track updates this var on update
251        let update_signal = zng_var::var(());
252
253        // init value and update bindings
254        let mut out = vec![];
255        let mut update_handles = vec![];
256        self.flat_tracks_init(&mut out, update_signal.clone(), &mut update_handles);
257        let out = zng_var::var(out);
258
259        // bind signal to rebuild list on update and rebind update signal
260        let self_ = self.clone();
261        let signal_weak = update_signal.downgrade();
262        update_signal
263            .bind_modify(&out, move |_, out| {
264                out.clear();
265                update_handles.clear();
266                self_.flat_tracks_init(&mut *out, signal_weak.upgrade().unwrap(), &mut update_handles);
267            })
268            .perm();
269        out.hold(update_signal).perm();
270        out.read_only()
271    }
272    fn flat_tracks_init(&self, out: &mut Vec<(VarEq<AudioTrack>, usize)>, update_signal: Var<()>, handles: &mut Vec<zng_var::VarHandle>) {
273        for track in self.tracks.iter() {
274            Self::flat_tracks_recursive_init(track.clone(), out, update_signal.clone(), handles);
275        }
276    }
277    fn flat_tracks_recursive_init(
278        aud: VarEq<AudioTrack>,
279        out: &mut Vec<(VarEq<AudioTrack>, usize)>,
280        signal: Var<()>,
281        handles: &mut Vec<zng_var::VarHandle>,
282    ) {
283        handles.push(aud.hook(zng_clone_move::clmv!(signal, |_| {
284            signal.update();
285            true
286        })));
287        let i = out.len();
288        out.push((aud.clone(), 0));
289        aud.with(move |aud| {
290            for track in aud.tracks.iter() {
291                Self::flat_tracks_recursive_init(track.clone(), out, signal.clone(), handles);
292            }
293            let len = out.len() - i;
294            out[i].1 = len;
295        });
296    }
297
298    /// Sort index of the audio in the list of tracks of the source container.
299    pub fn track_index(&self) -> usize {
300        match &self.meta.parent {
301            Some(p) => p.index,
302            None => 0,
303        }
304    }
305
306    /// Connection to the audio resource in the view-process.
307    pub fn view_handle(&self) -> &ViewAudioHandle {
308        &self.handle
309    }
310
311    /// Insert `track` in [`tracks`].
312    ///
313    /// [`tracks`]: Self::tracks
314    pub fn insert_track(&mut self, track: AudioVar) {
315        let i = track.with(|i| i.track_index());
316        let i = self
317            .tracks
318            .iter()
319            .position(|v| {
320                let track_i = v.with(|i| i.track_index());
321                track_i > i
322            })
323            .unwrap_or(self.tracks.len());
324        self.tracks.insert(i, VarEq(track));
325    }
326}
327
328/// A 256-bit hash for audio tracks.
329///
330/// This hash is used to identify audio files in the [`AUDIOS`] cache.
331///
332/// Use [`AudioHasher`] to compute.
333///
334/// [`AUDIOS`]: super::AUDIOS
335#[derive(Clone, Copy)]
336pub struct AudioHash([u8; 32]);
337impl AudioHash {
338    /// Compute the hash for `data`.
339    pub fn compute(data: &[u8]) -> Self {
340        let mut h = Self::hasher();
341        h.update(data);
342        h.finish()
343    }
344
345    /// Start a new [`AudioHasher`].
346    pub fn hasher() -> AudioHasher {
347        AudioHasher::default()
348    }
349}
350impl fmt::Debug for AudioHash {
351    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
352        if f.alternate() {
353            f.debug_tuple("AudioHash").field(&self.0).finish()
354        } else {
355            use base64::*;
356            write!(f, "{}", base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(self.0))
357        }
358    }
359}
360impl fmt::Display for AudioHash {
361    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362        write!(f, "{self:?}")
363    }
364}
365impl std::hash::Hash for AudioHash {
366    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
367        let h64 = [
368            self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5], self.0[6], self.0[7],
369        ];
370        state.write_u64(u64::from_ne_bytes(h64))
371    }
372}
373impl PartialEq for AudioHash {
374    fn eq(&self, other: &Self) -> bool {
375        self.0 == other.0
376    }
377}
378impl Eq for AudioHash {}
379
380/// Hasher that computes a [`AudioHash`].
381pub struct AudioHasher(sha2::Sha512_256);
382impl Default for AudioHasher {
383    fn default() -> Self {
384        use sha2::Digest;
385        Self(sha2::Sha512_256::new())
386    }
387}
388impl AudioHasher {
389    /// New default hasher.
390    pub fn new() -> Self {
391        Self::default()
392    }
393
394    /// Process data, updating the internal state.
395    pub fn update(&mut self, data: &[u8]) {
396        use sha2::Digest;
397
398        // some gigantic audios can take to long to hash, we just
399        // need the hash for identification so we sample the data
400        const NUM_SAMPLES: usize = 1000;
401        const SAMPLE_CHUNK_SIZE: usize = 1024;
402
403        let total_size = data.len();
404        if total_size == 0 {
405            return;
406        }
407        if total_size < 1000 * 1000 * 4 {
408            return self.0.update(data);
409        }
410
411        let step_size = total_size.checked_div(NUM_SAMPLES).unwrap_or(total_size);
412        for n in 0..NUM_SAMPLES {
413            let start_index = n * step_size;
414            if start_index >= total_size {
415                break;
416            }
417            let end_index = (start_index + SAMPLE_CHUNK_SIZE).min(total_size);
418            let s = &data[start_index..end_index];
419            self.0.update(s);
420        }
421    }
422
423    /// Finish computing the hash.
424    pub fn finish(self) -> AudioHash {
425        use sha2::Digest;
426        // dependencies `sha2 -> digest` need to upgrade
427        // https://github.com/RustCrypto/traits/issues/2036
428        // https://github.com/fizyk20/generic-array/issues/158
429        AudioHash(self.0.finalize().as_slice().try_into().unwrap())
430    }
431}
432impl std::hash::Hasher for AudioHasher {
433    fn finish(&self) -> u64 {
434        tracing::warn!("Hasher::finish called for AudioHasher");
435
436        use sha2::Digest;
437        let hash = self.0.clone().finalize();
438        u64::from_le_bytes(hash[..8].try_into().unwrap())
439    }
440
441    fn write(&mut self, bytes: &[u8]) {
442        self.update(bytes);
443    }
444}
445
446/// The different sources of an audio resource.
447#[derive(Clone)]
448#[non_exhaustive]
449pub enum AudioSource {
450    /// A path to an audio file in the file system.
451    ///
452    /// Audio equality is defined by the path, a copy of the audio in another path is a different audio.
453    Read(PathBuf),
454    /// A uri to an audio resource downloaded using HTTP GET with an optional HTTP ACCEPT string.
455    ///
456    /// If the ACCEPT line is not given, all audio formats supported by the view-process backend are accepted.
457    ///
458    /// Audio equality is defined by the URI and ACCEPT string.
459    #[cfg(feature = "http")]
460    Download(zng_task::http::Uri, Option<Txt>),
461    /// Shared reference to bytes for an encoded or decoded audio.
462    ///
463    /// Audio equality is defined by the hash, it is usually the hash of the bytes but it does not need to be.
464    ///
465    /// Inside [`AUDIOS`] the reference to the bytes is held only until the audio finishes decoding.
466    ///
467    /// [`AUDIOS`]: super::AUDIOS
468    Data(AudioHash, IpcBytes, AudioDataFormat),
469
470    /// Already resolved (loaded or loading) audio.
471    ///
472    /// The audio is passed-through, not cached.
473    Audio(AudioVar),
474}
475impl AudioSource {
476    /// New source from data.
477    pub fn from_data(data: IpcBytes, format: AudioDataFormat) -> Self {
478        let mut hasher = AudioHasher::default();
479        hasher.update(&data[..]);
480        let hash = hasher.finish();
481        Self::Data(hash, data, format)
482    }
483
484    /// Returns the audio hash, unless the source is [`AudioTrack`].
485    pub fn hash128(&self, options: &AudioOptions) -> Option<AudioHash> {
486        match self {
487            AudioSource::Read(p) => Some(Self::hash128_read(p, options)),
488            #[cfg(feature = "http")]
489            AudioSource::Download(u, a) => Some(Self::hash128_download(u, a, options)),
490            AudioSource::Data(h, _, _) => Some(Self::hash128_data(*h, options)),
491            AudioSource::Audio(_) => None,
492        }
493    }
494
495    /// Compute hash for a borrowed [`Data`] audio.
496    ///
497    /// [`Data`]: Self::Data
498    pub fn hash128_data(data_hash: AudioHash, options: &AudioOptions) -> AudioHash {
499        if !options.tracks.is_empty() {
500            use std::hash::Hash;
501            let mut h = AudioHash::hasher();
502            data_hash.0.hash(&mut h);
503            options.tracks.hash(&mut h);
504            h.finish()
505        } else {
506            data_hash
507        }
508    }
509
510    /// Compute hash for a borrowed [`Read`] path.
511    ///
512    /// [`Read`]: Self::Read
513    pub fn hash128_read(path: &Path, options: &AudioOptions) -> AudioHash {
514        use std::hash::Hash;
515        let mut h = AudioHash::hasher();
516        0u8.hash(&mut h);
517        path.hash(&mut h);
518        options.tracks.hash(&mut h);
519        h.finish()
520    }
521
522    /// Compute hash for a borrowed [`Download`] URI and HTTP-ACCEPT.
523    ///
524    /// [`Download`]: Self::Download
525    #[cfg(feature = "http")]
526    pub fn hash128_download(uri: &zng_task::http::Uri, accept: &Option<Txt>, options: &AudioOptions) -> AudioHash {
527        use std::hash::Hash;
528        let mut h = AudioHash::hasher();
529        1u8.hash(&mut h);
530        uri.hash(&mut h);
531        accept.hash(&mut h);
532        options.tracks.hash(&mut h);
533        h.finish()
534    }
535}
536
537impl PartialEq for AudioSource {
538    fn eq(&self, other: &Self) -> bool {
539        match (self, other) {
540            (Self::Read(l), Self::Read(r)) => l == r,
541            #[cfg(feature = "http")]
542            (Self::Download(lu, la), Self::Download(ru, ra)) => lu == ru && la == ra,
543            (Self::Audio(l), Self::Audio(r)) => l.var_eq(r),
544            (l, r) => {
545                let l_hash = match l {
546                    AudioSource::Data(h, _, _) => h,
547                    _ => return false,
548                };
549                let r_hash = match r {
550                    AudioSource::Data(h, _, _) => h,
551                    _ => return false,
552                };
553
554                l_hash == r_hash
555            }
556        }
557    }
558}
559impl fmt::Debug for AudioSource {
560    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
561        if f.alternate() {
562            write!(f, "AudioSource::")?;
563        }
564        match self {
565            AudioSource::Read(p) => f.debug_tuple("Read").field(p).finish(),
566            #[cfg(feature = "http")]
567            AudioSource::Download(u, a) => f.debug_tuple("Download").field(u).field(a).finish(),
568            AudioSource::Data(key, bytes, fmt) => f.debug_tuple("Data").field(key).field(bytes).field(fmt).finish(),
569
570            AudioSource::Audio(_) => write!(f, "Audio(_)"),
571        }
572    }
573}
574
575#[cfg(feature = "http")]
576impl_from_and_into_var! {
577    fn from(uri: zng_task::http::Uri) -> AudioSource {
578        AudioSource::Download(uri, None)
579    }
580    /// From (URI, HTTP-ACCEPT).
581    fn from((uri, accept): (zng_task::http::Uri, &'static str)) -> AudioSource {
582        AudioSource::Download(uri, Some(accept.into()))
583    }
584
585    /// Converts `http://` and `https://` to [`Download`], `file://` to
586    /// [`Read`] the path component, and the rest to [`Read`] the string as a path.
587    ///
588    /// [`Download`]: AudioSource::Download
589    /// [`Read`]: AudioSource::Read
590    fn from(s: &str) -> AudioSource {
591        use zng_task::http::*;
592        if let Ok(uri) = Uri::try_from(s)
593            && let Some(scheme) = uri.scheme()
594        {
595            if scheme == &uri::Scheme::HTTPS || scheme == &uri::Scheme::HTTP {
596                return AudioSource::Download(uri, None);
597            } else if scheme.as_str() == "file" {
598                return PathBuf::from(uri.path()).into();
599            }
600        }
601        PathBuf::from(s).into()
602    }
603}
604
605#[cfg(not(feature = "http"))]
606impl_from_and_into_var! {
607    /// Converts to [`Read`].
608    ///
609    /// [`Read`]: AudioSource::Read
610    fn from(s: &str) -> AudioSource {
611        PathBuf::from(s).into()
612    }
613}
614
615impl_from_and_into_var! {
616    fn from(audio: AudioVar) -> AudioSource {
617        AudioSource::Audio(audio)
618    }
619    fn from(path: PathBuf) -> AudioSource {
620        AudioSource::Read(path)
621    }
622    fn from(path: &Path) -> AudioSource {
623        path.to_owned().into()
624    }
625
626    /// Same as conversion from `&str`.
627    fn from(s: String) -> AudioSource {
628        s.as_str().into()
629    }
630    /// Same as conversion from `&str`.
631    fn from(s: Txt) -> AudioSource {
632        s.as_str().into()
633    }
634    /// From encoded data of [`Unknown`] format.
635    ///
636    /// [`Unknown`]: AudioDataFormat::Unknown
637    fn from(data: &[u8]) -> AudioSource {
638        AudioSource::Data(
639            AudioHash::compute(data),
640            IpcBytes::from_slice_blocking(data).expect("cannot allocate IpcBytes"),
641            AudioDataFormat::Unknown,
642        )
643    }
644    /// From encoded data of [`Unknown`] format.
645    ///
646    /// [`Unknown`]: AudioDataFormat::Unknown
647    fn from<const N: usize>(data: &[u8; N]) -> AudioSource {
648        (&data[..]).into()
649    }
650    /// From encoded data of [`Unknown`] format.
651    ///
652    /// [`Unknown`]: AudioDataFormat::Unknown
653    fn from(data: IpcBytes) -> AudioSource {
654        AudioSource::Data(AudioHash::compute(&data[..]), data, AudioDataFormat::Unknown)
655    }
656    /// From encoded data of [`Unknown`] format.
657    ///
658    /// [`Unknown`]: AudioDataFormat::Unknown
659    fn from(data: Vec<u8>) -> AudioSource {
660        IpcBytes::from_vec_blocking(data).expect("cannot allocate IpcBytes").into()
661    }
662    /// From encoded data of known format.
663    fn from<F: Into<AudioDataFormat>>((data, format): (&[u8], F)) -> AudioSource {
664        AudioSource::Data(
665            AudioHash::compute(data),
666            IpcBytes::from_slice_blocking(data).expect("cannot allocate IpcBytes"),
667            format.into(),
668        )
669    }
670    /// From encoded data of known format.
671    fn from<F: Into<AudioDataFormat>, const N: usize>((data, format): (&[u8; N], F)) -> AudioSource {
672        (&data[..], format).into()
673    }
674    /// From encoded data of known format.
675    fn from<F: Into<AudioDataFormat>>((data, format): (Vec<u8>, F)) -> AudioSource {
676        (IpcBytes::from_vec_blocking(data).expect("cannot allocate IpcBytes"), format).into()
677    }
678    /// From encoded data of known format.
679    fn from<F: Into<AudioDataFormat>>((data, format): (IpcBytes, F)) -> AudioSource {
680        AudioSource::Data(AudioHash::compute(&data[..]), data, format.into())
681    }
682}
683
684/// Cache mode of [`AUDIOS`].
685///
686/// [`AUDIOS`]: super::AUDIOS
687#[derive(Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
688pub enum AudioCacheMode {
689    /// Don't hit the cache, just loads the audio.
690    Ignore,
691    /// Gets a cached audio or loads the audio and caches it.
692    Cache,
693    /// Cache or reload if the cached audio is an error.
694    Retry,
695    /// Reloads the cache audio or loads the audio and caches it.
696    ///
697    /// The [`AudioVar`] is not replaced, other references to the audio also receive the update.
698    Reload,
699}
700impl fmt::Debug for AudioCacheMode {
701    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
702        if f.alternate() {
703            write!(f, "CacheMode::")?;
704        }
705        match self {
706            Self::Ignore => write!(f, "Ignore"),
707            Self::Cache => write!(f, "Cache"),
708            Self::Retry => write!(f, "Retry"),
709            Self::Reload => write!(f, "Reload"),
710        }
711    }
712}
713impl_from_and_into_var! {
714    fn from(cache: bool) -> AudioCacheMode {
715        if cache { AudioCacheMode::Cache } else { AudioCacheMode::Ignore }
716    }
717}
718
719/// Represents a [`PathFilter`] and [`UriFilter`].
720#[derive(Clone)]
721pub enum AudioSourceFilter<U> {
722    /// Block all requests of this type.
723    BlockAll,
724    /// Allow all requests of this type.
725    AllowAll,
726    /// Custom filter, returns `true` to allow a request, `false` to block.
727    Custom(Arc<dyn Fn(&U) -> bool + Send + Sync>),
728}
729impl<U> AudioSourceFilter<U> {
730    /// New [`Custom`] filter.
731    ///
732    /// [`Custom`]: Self::Custom
733    pub fn custom(allow: impl Fn(&U) -> bool + Send + Sync + 'static) -> Self {
734        Self::Custom(Arc::new(allow))
735    }
736
737    /// Combine `self` with `other`, if they both are [`Custom`], otherwise is [`BlockAll`] if any is [`BlockAll`], else
738    /// is [`AllowAll`] if any is [`AllowAll`].
739    ///
740    /// If both are [`Custom`] both filters must allow a request to pass the new filter.
741    ///
742    /// [`Custom`]: Self::Custom
743    /// [`BlockAll`]: Self::BlockAll
744    /// [`AllowAll`]: Self::AllowAll
745    pub fn and(self, other: Self) -> Self
746    where
747        U: 'static,
748    {
749        use AudioSourceFilter::*;
750        match (self, other) {
751            (BlockAll, _) | (_, BlockAll) => BlockAll,
752            (AllowAll, _) | (_, AllowAll) => AllowAll,
753            (Custom(c0), Custom(c1)) => Custom(Arc::new(move |u| c0(u) && c1(u))),
754        }
755    }
756
757    /// Combine `self` with `other`, if they both are [`Custom`], otherwise is [`AllowAll`] if any is [`AllowAll`], else
758    /// is [`BlockAll`] if any is [`BlockAll`].
759    ///
760    /// If both are [`Custom`] at least one of the filters must allow a request to pass the new filter.
761    ///
762    /// [`Custom`]: Self::Custom
763    /// [`BlockAll`]: Self::BlockAll
764    /// [`AllowAll`]: Self::AllowAll
765    pub fn or(self, other: Self) -> Self
766    where
767        U: 'static,
768    {
769        use AudioSourceFilter::*;
770        match (self, other) {
771            (AllowAll, _) | (_, AllowAll) => AllowAll,
772            (BlockAll, _) | (_, BlockAll) => BlockAll,
773            (Custom(c0), Custom(c1)) => Custom(Arc::new(move |u| c0(u) || c1(u))),
774        }
775    }
776
777    /// Returns `true` if the filter allows the request.
778    pub fn allows(&self, item: &U) -> bool {
779        match self {
780            AudioSourceFilter::BlockAll => false,
781            AudioSourceFilter::AllowAll => true,
782            AudioSourceFilter::Custom(f) => f(item),
783        }
784    }
785}
786impl<U> fmt::Debug for AudioSourceFilter<U> {
787    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
788        match self {
789            Self::BlockAll => write!(f, "BlockAll"),
790            Self::AllowAll => write!(f, "AllowAll"),
791            Self::Custom(_) => write!(f, "Custom(_)"),
792        }
793    }
794}
795impl<U: 'static> ops::BitAnd for AudioSourceFilter<U> {
796    type Output = Self;
797
798    fn bitand(self, rhs: Self) -> Self::Output {
799        self.and(rhs)
800    }
801}
802impl<U: 'static> ops::BitOr for AudioSourceFilter<U> {
803    type Output = Self;
804
805    fn bitor(self, rhs: Self) -> Self::Output {
806        self.or(rhs)
807    }
808}
809impl<U: 'static> ops::BitAndAssign for AudioSourceFilter<U> {
810    fn bitand_assign(&mut self, rhs: Self) {
811        *self = mem::replace(self, Self::BlockAll).and(rhs);
812    }
813}
814impl<U: 'static> ops::BitOrAssign for AudioSourceFilter<U> {
815    fn bitor_assign(&mut self, rhs: Self) {
816        *self = mem::replace(self, Self::BlockAll).or(rhs);
817    }
818}
819impl<U> PartialEq for AudioSourceFilter<U> {
820    fn eq(&self, other: &Self) -> bool {
821        match (self, other) {
822            (Self::Custom(l0), Self::Custom(r0)) => Arc::ptr_eq(l0, r0),
823            _ => core::mem::discriminant(self) == core::mem::discriminant(other),
824        }
825    }
826}
827
828/// Represents an [`AudioSource::Read`] path request filter.
829///
830/// Only absolute, normalized paths are shared with the [`Custom`] filter, there is no relative paths or `..` components.
831///
832/// The paths are **not** canonicalized and existence is not verified, no system requests are made with unfiltered paths.
833///
834/// See [`AudioLimits::allow_path`] for more information.
835///
836/// [`Custom`]: AudioSourceFilter::Custom
837pub type PathFilter = AudioSourceFilter<PathBuf>;
838impl PathFilter {
839    /// Allow any file inside `dir` or sub-directories of `dir`.
840    pub fn allow_dir(dir: impl AsRef<Path>) -> Self {
841        let dir = absolute_path(dir.as_ref(), || env::current_dir().expect("could not access current dir"), true);
842        PathFilter::custom(move |r| r.starts_with(&dir))
843    }
844
845    /// Allow any path with the `ext` extension.
846    pub fn allow_ext(ext: impl Into<std::ffi::OsString>) -> Self {
847        let ext = ext.into();
848        PathFilter::custom(move |r| r.extension().map(|e| e == ext).unwrap_or(false))
849    }
850
851    /// Allow any file inside the [`env::current_dir`] or sub-directories.
852    ///
853    /// Note that the current directory can be changed and the filter always uses the
854    /// *fresh* current directory, use [`allow_dir`] to create a filter the always points
855    /// to the current directory at the filter creation time.
856    ///
857    /// [`allow_dir`]: Self::allow_dir
858    pub fn allow_current_dir() -> Self {
859        PathFilter::custom(|r| env::current_dir().map(|d| r.starts_with(d)).unwrap_or(false))
860    }
861
862    /// Allow any file inside the current executable directory or sub-directories.
863    pub fn allow_exe_dir() -> Self {
864        if let Ok(mut p) = env::current_exe().and_then(dunce::canonicalize)
865            && p.pop()
866        {
867            return Self::allow_dir(p);
868        }
869
870        // not `BlockAll` so this can still be composed using `or`.
871        Self::custom(|_| false)
872    }
873
874    /// Allow any file inside the [`zng::env::res`] directory or sub-directories.
875    ///
876    /// [`zng::env::res`]: zng_env::res
877    pub fn allow_res() -> Self {
878        Self::allow_dir(zng_env::res(""))
879    }
880}
881
882/// Represents an [`AudioSource::Download`] path request filter.
883///
884/// See [`AudioLimits::allow_uri`] for more information.
885#[cfg(feature = "http")]
886pub type UriFilter = AudioSourceFilter<zng_task::http::Uri>;
887#[cfg(feature = "http")]
888impl UriFilter {
889    /// Allow any file from the `host` site.
890    pub fn allow_host(host: impl Into<Txt>) -> Self {
891        let host = host.into();
892        UriFilter::custom(move |u| u.authority().map(|a| a.host() == host).unwrap_or(false))
893    }
894}
895
896impl<F: Fn(&PathBuf) -> bool + Send + Sync + 'static> From<F> for PathFilter {
897    fn from(custom: F) -> Self {
898        PathFilter::custom(custom)
899    }
900}
901
902#[cfg(feature = "http")]
903impl<F: Fn(&zng_task::http::Uri) -> bool + Send + Sync + 'static> From<F> for UriFilter {
904    fn from(custom: F) -> Self {
905        UriFilter::custom(custom)
906    }
907}
908
909/// Limits for audio loading and decoding.
910#[derive(Clone, Debug, PartialEq)]
911#[non_exhaustive]
912pub struct AudioLimits {
913    /// Maximum encoded file size allowed.
914    ///
915    /// An error is returned if the file size surpasses this value. If the size can read before
916    /// read/download the validation happens before download starts, otherwise the error happens when this limit
917    /// is reached and all already downloaded bytes are dropped.
918    ///
919    /// The default is `100mb`.
920    pub max_encoded_len: ByteLength,
921    /// Maximum decoded file size allowed.
922    ///
923    /// An error is returned if the decoded audio memory (width * height * 4) would surpass this.
924    pub max_decoded_len: ByteLength,
925
926    /// Filter for [`AudioSource::Read`] paths.
927    pub allow_path: PathFilter,
928
929    /// Filter for [`AudioSource::Download`] URIs.
930    #[cfg(feature = "http")]
931    pub allow_uri: UriFilter,
932}
933impl AudioLimits {
934    /// No size limits, allow all paths and URIs.
935    pub fn none() -> Self {
936        AudioLimits {
937            max_encoded_len: ByteLength::MAX,
938            max_decoded_len: ByteLength::MAX,
939            allow_path: PathFilter::AllowAll,
940            #[cfg(feature = "http")]
941            allow_uri: UriFilter::AllowAll,
942        }
943    }
944
945    /// Set the [`max_encoded_len`].
946    ///
947    /// [`max_encoded_len`]: Self::max_encoded_len
948    pub fn with_max_encoded_len(mut self, max_encoded_len: impl Into<ByteLength>) -> Self {
949        self.max_encoded_len = max_encoded_len.into();
950        self
951    }
952
953    /// Set the [`max_decoded_len`].
954    ///
955    /// [`max_decoded_len`]: Self::max_encoded_len
956    pub fn with_max_decoded_len(mut self, max_decoded_len: impl Into<ByteLength>) -> Self {
957        self.max_decoded_len = max_decoded_len.into();
958        self
959    }
960
961    /// Set the [`allow_path`].
962    ///
963    /// [`allow_path`]: Self::allow_path
964    pub fn with_allow_path(mut self, allow_path: impl Into<PathFilter>) -> Self {
965        self.allow_path = allow_path.into();
966        self
967    }
968
969    /// Set the [`allow_uri`].
970    ///
971    /// [`allow_uri`]: Self::allow_uri
972    #[cfg(feature = "http")]
973    pub fn with_allow_uri(mut self, allow_url: impl Into<UriFilter>) -> Self {
974        self.allow_uri = allow_url.into();
975        self
976    }
977}
978impl Default for AudioLimits {
979    /// 100 megabytes encoded and 4096 megabytes decoded (BMP max).
980    ///
981    /// Allows only paths in `zng::env::res`, blocks all downloads.
982    fn default() -> Self {
983        Self {
984            max_encoded_len: 100.megabytes(),
985            max_decoded_len: 4096.megabytes(),
986            allow_path: PathFilter::allow_res(),
987            #[cfg(feature = "http")]
988            allow_uri: UriFilter::BlockAll,
989        }
990    }
991}
992impl_from_and_into_var! {
993    fn from(some: AudioLimits) -> Option<AudioLimits>;
994}
995
996/// Options for [`AUDIOS.audio`].
997///
998/// [`AUDIOS.audio`]: crate::AUDIOS::audio
999#[derive(Debug, Clone, PartialEq)]
1000#[non_exhaustive]
1001pub struct AudioOptions {
1002    /// If and how the audio is cached.
1003    pub cache_mode: AudioCacheMode,
1004    /// How to decode containers with multiple audios.
1005    pub tracks: AudioTracksMode,
1006}
1007
1008impl AudioOptions {
1009    /// New.
1010    pub fn new(cache_mode: AudioCacheMode, tracks: AudioTracksMode) -> Self {
1011        Self { cache_mode, tracks }
1012    }
1013
1014    /// New with only cache enabled.
1015    pub fn cache() -> Self {
1016        Self::new(AudioCacheMode::Cache, AudioTracksMode::empty())
1017    }
1018
1019    /// New with nothing enabled, no caching.
1020    pub fn none() -> Self {
1021        Self::new(AudioCacheMode::Ignore, AudioTracksMode::empty())
1022    }
1023}
1024
1025fn absolute_path(path: &Path, base: impl FnOnce() -> PathBuf, allow_escape: bool) -> PathBuf {
1026    if path.is_absolute() {
1027        normalize_path(path)
1028    } else {
1029        let mut dir = base();
1030        if allow_escape {
1031            dir.push(path);
1032            normalize_path(&dir)
1033        } else {
1034            dir.push(normalize_path(path));
1035            dir
1036        }
1037    }
1038}
1039/// Resolves `..` components, without any system request.
1040///
1041/// Source: https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61
1042fn normalize_path(path: &Path) -> PathBuf {
1043    use std::path::Component;
1044
1045    let mut components = path.components().peekable();
1046    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
1047        components.next();
1048        PathBuf::from(c.as_os_str())
1049    } else {
1050        PathBuf::new()
1051    };
1052
1053    for component in components {
1054        match component {
1055            Component::Prefix(..) => unreachable!(),
1056            Component::RootDir => {
1057                ret.push(component.as_os_str());
1058            }
1059            Component::CurDir => {}
1060            Component::ParentDir => {
1061                ret.pop();
1062            }
1063            Component::Normal(c) => {
1064                ret.push(c);
1065            }
1066        }
1067    }
1068    ret
1069}