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