Skip to main content

koan_core/audio/
streaming.rs

1//! `PartialFileSource` — a Read+Seek adapter over a file that is still downloading.
2//!
3//! The download thread writes the track to a `.part` file and publishes how far
4//! it has got in an `AtomicU64`; the Symphonia decoder reads the same file
5//! through a `PartialFileSource`, which blocks when the read position catches
6//! up to the write head. Playback starts long before the transfer finishes and
7//! seeking anywhere below the write head costs a `lseek`.
8//!
9//! Nothing is copied. An earlier design pumped the file into a shared `Vec<u8>`
10//! so the decoder could read from memory, which cost as much RAM as the track
11//! was long — half a gigabyte for a nine-hour recording, held for as long as it
12//! played. The bytes are already on disk; the page cache is better at this.
13//!
14//! The open descriptor survives the download's final rename from `.part` to its
15//! cache path, so a transfer landing mid-playback changes nothing for a reader.
16
17use std::fs::File;
18use std::io::{self, Read, Seek, SeekFrom};
19use std::path::Path;
20use std::sync::Arc;
21use std::sync::atomic::Ordering;
22use std::time::{Duration, Instant};
23
24/// Longest a read may block waiting for bytes before the transfer counts as
25/// dead. A download that stops advancing must surface as an error, not park the
26/// decode thread forever holding the ring buffer producer.
27const STALL_LIMIT: Duration = Duration::from_secs(30);
28
29/// Where a download has got to, as the source needs to know it.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum StreamStatus {
32    /// Bytes are still arriving.
33    Downloading,
34    /// Every byte landed. Reads past the end are a clean EOF.
35    Complete,
36    /// The transfer died before delivering everything. Reads past the written
37    /// bytes fail rather than reporting EOF, which would silently truncate the
38    /// track and look like a short file.
39    Failed,
40}
41
42/// A `Read + Seek` view of a file that is still being written.
43pub struct PartialFileSource {
44    file: File,
45    pos: u64,
46    /// How many bytes the download has committed to disk so far.
47    bytes_written: Arc<crate::remote::downloads::ByteFeed>,
48    /// Total expected length, or 0 when the server sent no Content-Length.
49    total: u64,
50    status: Arc<dyn Fn() -> StreamStatus + Send + Sync>,
51    stall_limit: Duration,
52    /// Whether a read may wait at the write head for more of the download.
53    ///
54    /// Playback waits; probing does not. A container asked to describe itself
55    /// reads whatever it needs to, and Ogg needs its last page — so a probe
56    /// that waits waits for the whole transfer. Refusing instead turns that
57    /// into an immediate answer of "not from what has arrived", which is
58    /// something the caller can act on.
59    wait_for_bytes: bool,
60    /// Whether to state the advertised length.
61    ///
62    /// Saying nothing is what stops a container going looking for its tail:
63    /// Ogg reads its final page only when the source claims both a length and
64    /// seekability, and Symphonia scans for trailing metadata on the same
65    /// terms — which no partial file can satisfy. So this is what every
66    /// container mid-download ends up opened with, not only Ogg.
67    ///
68    /// It governs the end of the stream as well as the length, because they
69    /// have to be the same end. See `Seek`.
70    advertise_len: bool,
71    /// Whether `SeekFrom::End` answers for the whole file or for what has
72    /// arrived. Separate from `advertise_len` because Ogg needs the first
73    /// without the second — see `ProbeMode`.
74    whole_file_end: bool,
75}
76
77/// How much a probe may claim, and how far it may read.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum ProbeMode {
80    /// State the length. A container that can describe itself from the bytes
81    /// already downloaded does, and the result is fully seekable.
82    Full,
83    /// State no length, so a container that would go looking for its tail
84    /// settles for what it can read from the front. Opens immediately, and
85    /// stays seekable within what has arrived for anything that describes its
86    /// frames from the front. What it gives up is whatever only the tail could
87    /// give — for Ogg that is the duration, and with it seeking at all.
88    ///
89    /// The end is what has arrived. FLAC bisects between its first frame and
90    /// the end it is given, so an end it cannot reach sends every probe into
91    /// bytes that are not on disk.
92    Lengthless,
93    /// The same, except that the end stays the whole file's.
94    ///
95    /// Ogg takes the end it is handed as the end of the *stream*. Handed the
96    /// write head it reports a track that is already over — nought
97    /// milliseconds — and the decode thread reaches the end of it in a second
98    /// and moves on to the next, over and over, so a large Opus file
99    /// downloading never plays at all. It cannot seek mid-download under either
100    /// answer; this is the difference between playing and not.
101    LengthlessWholeEnd,
102}
103
104impl PartialFileSource {
105    /// Open `path` for playback: reads wait at the write head for the download
106    /// to catch up. `bytes_written` is the download's own counter and `total`
107    /// its advertised length, 0 when it sent none.
108    ///
109    /// `mode` must be whatever the probe settled on. A container opened without
110    /// a length to describe itself has to be decoded without one too — given a
111    /// length it goes looking for its tail all over again, and this time on the
112    /// decode thread, where the cost is silence rather than a busy player.
113    pub fn open(
114        path: &Path,
115        bytes_written: Arc<crate::remote::downloads::ByteFeed>,
116        total: u64,
117        status: Arc<dyn Fn() -> StreamStatus + Send + Sync>,
118        mode: ProbeMode,
119    ) -> io::Result<Self> {
120        let mut source = Self::with_stall_limit(path, bytes_written, total, status, STALL_LIMIT)?;
121        source.advertise_len = mode == ProbeMode::Full;
122        source.whole_file_end = mode != ProbeMode::Lengthless;
123        Ok(source)
124    }
125
126    /// Open for a probe: never waits at the write head, so a container that
127    /// cannot describe itself from what has arrived says so at once.
128    pub fn open_for_probe(
129        path: &Path,
130        bytes_written: Arc<crate::remote::downloads::ByteFeed>,
131        total: u64,
132        status: Arc<dyn Fn() -> StreamStatus + Send + Sync>,
133        mode: ProbeMode,
134    ) -> io::Result<Self> {
135        let mut source = Self::with_stall_limit(path, bytes_written, total, status, STALL_LIMIT)?;
136        source.wait_for_bytes = false;
137        source.advertise_len = mode == ProbeMode::Full;
138        source.whole_file_end = mode != ProbeMode::Lengthless;
139        Ok(source)
140    }
141
142    fn with_stall_limit(
143        path: &Path,
144        bytes_written: Arc<crate::remote::downloads::ByteFeed>,
145        total: u64,
146        status: Arc<dyn Fn() -> StreamStatus + Send + Sync>,
147        stall_limit: Duration,
148    ) -> io::Result<Self> {
149        Ok(Self {
150            file: File::open(path)?,
151            pos: 0,
152            bytes_written,
153            total,
154            status,
155            stall_limit,
156            wait_for_bytes: true,
157            advertise_len: true,
158            whole_file_end: true,
159        })
160    }
161
162    /// Bytes known to be readable — what the download has written, or the whole
163    /// file once it has landed.
164    fn available(&self) -> u64 {
165        let written = self.bytes_written.load(Ordering::Acquire);
166        match (self.status)() {
167            StreamStatus::Complete => self.file.metadata().map(|m| m.len()).unwrap_or(written),
168            _ => written,
169        }
170    }
171
172    /// Read straight from the file, tolerating a short read at the write head:
173    /// Whatever ends a transfer must call `ByteFeed::done` when it sets the
174    /// status: a read waiting for bytes is parked on the feed, and a download
175    /// that failed has no more bytes to wake it with.
176    ///
177    /// `bytes_written` is published by the downloader as it goes and the data
178    /// behind it can lag by a moment.
179    fn read_available(&mut self, buf: &mut [u8], limit: u64) -> io::Result<usize> {
180        let to_read = (limit as usize).min(buf.len());
181        self.file.read(&mut buf[..to_read]).inspect(|n| {
182            self.pos += *n as u64;
183        })
184    }
185}
186
187impl Read for PartialFileSource {
188    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
189        if buf.is_empty() {
190            return Ok(0);
191        }
192
193        let deadline = Instant::now() + self.stall_limit;
194        loop {
195            let available = self.available();
196            if available > self.pos {
197                let n = self.read_available(buf, available - self.pos)?;
198                if n > 0 {
199                    return Ok(n);
200                }
201                // The counter ran ahead of what is visible on disk. Fall
202                // through and wait rather than reporting a false EOF.
203            }
204
205            match (self.status)() {
206                StreamStatus::Failed => {
207                    return Err(io::Error::new(
208                        io::ErrorKind::BrokenPipe,
209                        "stream download failed before delivering the whole track",
210                    ));
211                }
212                // Everything landed and there is nothing past `pos`: real EOF.
213                StreamStatus::Complete if available <= self.pos => return Ok(0),
214                StreamStatus::Complete => {}
215                StreamStatus::Downloading => {
216                    // A server that sent a Content-Length has delivered it all.
217                    if self.total > 0 && available >= self.total && self.pos >= self.total {
218                        return Ok(0);
219                    }
220                }
221            }
222
223            if !self.wait_for_bytes {
224                return Err(io::Error::new(
225                    io::ErrorKind::UnexpectedEof,
226                    "past what the download has delivered",
227                ));
228            }
229            if Instant::now() >= deadline {
230                return Err(io::Error::new(
231                    io::ErrorKind::TimedOut,
232                    "stream download stalled",
233                ));
234            }
235            // Woken by the download itself, and by it finishing or failing.
236            // The deadline is the giving-up clock rather than a look-again
237            // one: this thread is not scheduled between one chunk and the next.
238            self.bytes_written.wait_past(available, deadline);
239        }
240    }
241}
242
243impl Seek for PartialFileSource {
244    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
245        let target: i64 = match pos {
246            SeekFrom::Start(n) => n as i64,
247            SeekFrom::Current(n) => self.pos as i64 + n,
248            // The end has to be the same end `byte_len` describes. A source
249            // that states a length is asked about the whole file and answers
250            // for it; one that states none has only what has arrived, and
251            // answering with the advertised total there sends a reader
252            // bisecting into bytes that are not on disk yet — which is the
253            // whole file's worth of waiting for a FLAC seeked mid-download.
254            SeekFrom::End(n) => {
255                let len = if self.whole_file_end && self.total > 0 {
256                    self.total
257                } else {
258                    self.available()
259                };
260                len as i64 + n
261            }
262        };
263
264        if target < 0 {
265            return Err(io::Error::new(
266                io::ErrorKind::InvalidInput,
267                "seek before beginning of stream",
268            ));
269        }
270
271        self.pos = self.file.seek(SeekFrom::Start(target as u64))?;
272        Ok(self.pos)
273    }
274}
275
276// Symphonia requires MediaSource: Read + Seek + Send + Any
277impl symphonia::core::io::MediaSource for PartialFileSource {
278    fn is_seekable(&self) -> bool {
279        // Backward seeks and forward seeks below the write head are a `lseek`
280        // on a file that is already there. A forward seek past it lands on a
281        // read that blocks until the bytes arrive, which is the honest
282        // behaviour — callers clamp to `seekable_ms` to avoid asking.
283        //
284        // Saying no would cost more than it saved: a reader told a stream is
285        // unseekable does not stop seeking, it walks the whole file to the
286        // target instead, and cannot go backwards at all.
287        true
288    }
289
290    fn byte_len(&self) -> Option<u64> {
291        (self.advertise_len && self.total > 0).then_some(self.total)
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use std::io::Write;
298    use std::sync::atomic::AtomicU8;
299
300    use symphonia::core::io::MediaSource;
301
302    use super::*;
303
304    /// A file plus the counter and status a download would publish, so a test
305    /// can advance either independently.
306    struct Fixture {
307        _dir: tempfile::TempDir,
308        path: std::path::PathBuf,
309        written: Arc<crate::remote::downloads::ByteFeed>,
310        status: Arc<AtomicU8>,
311    }
312
313    const DOWNLOADING: u8 = 0;
314    const COMPLETE: u8 = 1;
315    const FAILED: u8 = 2;
316
317    impl Fixture {
318        fn new() -> Self {
319            let dir = tempfile::tempdir().unwrap();
320            let path = dir.path().join("track.opus.part");
321            File::create(&path).unwrap();
322            Self {
323                _dir: dir,
324                path,
325                written: crate::remote::downloads::ByteFeed::new(),
326                status: Arc::new(AtomicU8::new(DOWNLOADING)),
327            }
328        }
329
330        /// Append bytes and publish them, as the downloader does per chunk.
331        fn push(&self, chunk: &[u8]) {
332            let mut f = std::fs::OpenOptions::new()
333                .append(true)
334                .open(&self.path)
335                .unwrap();
336            f.write_all(chunk).unwrap();
337            f.flush().unwrap();
338            self.written.advance(chunk.len() as u64);
339        }
340
341        fn set(&self, status: u8) {
342            self.status.store(status, Ordering::Release);
343            // What ends a transfer says so, the way the downloader does — a
344            // reader blocked for bytes that will never come is waiting on the
345            // feed, not on the status.
346            self.written.done();
347        }
348
349        fn status_fn(&self) -> Arc<dyn Fn() -> StreamStatus + Send + Sync> {
350            let status = self.status.clone();
351            Arc::new(move || match status.load(Ordering::Acquire) {
352                COMPLETE => StreamStatus::Complete,
353                FAILED => StreamStatus::Failed,
354                _ => StreamStatus::Downloading,
355            })
356        }
357
358        fn source(&self, total: u64) -> PartialFileSource {
359            self.source_with_stall(total, STALL_LIMIT)
360        }
361
362        fn source_with_stall(&self, total: u64, stall: Duration) -> PartialFileSource {
363            let status = self.status.clone();
364            PartialFileSource::with_stall_limit(
365                &self.path,
366                self.written.clone(),
367                total,
368                Arc::new(move || match status.load(Ordering::Acquire) {
369                    COMPLETE => StreamStatus::Complete,
370                    FAILED => StreamStatus::Failed,
371                    _ => StreamStatus::Downloading,
372                }),
373                stall,
374            )
375            .unwrap()
376        }
377    }
378
379    #[test]
380    fn reads_what_has_landed() {
381        let fx = Fixture::new();
382        fx.push(b"hello streaming world");
383        fx.set(COMPLETE);
384
385        let mut out = Vec::new();
386        fx.source(21).read_to_end(&mut out).unwrap();
387        assert_eq!(out, b"hello streaming world");
388    }
389
390    #[test]
391    fn read_stops_at_the_write_head_then_resumes() {
392        let fx = Fixture::new();
393        fx.push(b"abcd");
394        let mut src = fx.source(10);
395
396        let mut first = [0u8; 8];
397        assert_eq!(src.read(&mut first).unwrap(), 4);
398        assert_eq!(&first[..4], b"abcd");
399
400        // The rest arrives while the reader is blocked on it.
401        std::thread::spawn({
402            let path = fx.path.clone();
403            let written = fx.written.clone();
404            move || {
405                std::thread::sleep(Duration::from_millis(20));
406                let mut f = std::fs::OpenOptions::new()
407                    .append(true)
408                    .open(&path)
409                    .unwrap();
410                f.write_all(b"efghij").unwrap();
411                f.flush().unwrap();
412                written.advance(6);
413            }
414        });
415
416        let mut rest = [0u8; 8];
417        let n = src.read(&mut rest).unwrap();
418        assert_eq!(&rest[..n], b"efghij");
419    }
420
421    #[test]
422    fn seeks_freely_below_the_write_head() {
423        let fx = Fixture::new();
424        fx.push(b"0123456789");
425        let mut src = fx.source(1_000_000);
426
427        assert_eq!(src.seek(SeekFrom::Start(5)).unwrap(), 5);
428        let mut out = [0u8; 3];
429        src.read_exact(&mut out).unwrap();
430        assert_eq!(&out, b"567");
431
432        // Backwards, into bytes already read — no re-download, no buffer.
433        assert_eq!(src.seek(SeekFrom::Start(1)).unwrap(), 1);
434        src.read_exact(&mut out).unwrap();
435        assert_eq!(&out, b"123");
436
437        assert_eq!(src.seek(SeekFrom::Current(-2)).unwrap(), 2);
438    }
439
440    #[test]
441    fn seek_from_end_uses_the_advertised_length() {
442        let fx = Fixture::new();
443        fx.push(b"0123456789");
444        let mut src = fx.source(10);
445
446        assert_eq!(src.seek(SeekFrom::End(0)).unwrap(), 10);
447        assert_eq!(src.seek(SeekFrom::End(-3)).unwrap(), 7);
448
449        let mut out = [0u8; 3];
450        src.read_exact(&mut out).unwrap();
451        assert_eq!(&out, b"789");
452    }
453
454    #[test]
455    fn a_lengthless_source_ends_where_the_download_does() {
456        // The end has to agree with `byte_len`. FLAC seeks by bisecting
457        // between its first frame and `SeekFrom::End(0)`, so answering with the
458        // advertised total aims the search at bytes that are not on disk yet:
459        // every probe of the range waits at the write head, and a seek into a
460        // half-downloaded track spends the stall limit before failing.
461        let fx = Fixture::new();
462        fx.push(b"0123456789");
463
464        let mut src = PartialFileSource::open(
465            &fx.path,
466            fx.written.clone(),
467            1_000,
468            fx.status_fn(),
469            ProbeMode::Lengthless,
470        )
471        .unwrap();
472        assert_eq!(src.byte_len(), None);
473        assert_eq!(src.seek(SeekFrom::End(0)).unwrap(), 10);
474
475        // Stating a length means answering for the whole of it.
476        let mut src = PartialFileSource::open(
477            &fx.path,
478            fx.written.clone(),
479            1_000,
480            fx.status_fn(),
481            ProbeMode::Full,
482        )
483        .unwrap();
484        assert_eq!(src.seek(SeekFrom::End(0)).unwrap(), 1_000);
485    }
486
487    /// Ogg's half of the same question, and the opposite answer.
488    ///
489    /// Handed the write head as the end, Ogg reports a stream that is already
490    /// over: a large Opus file downloading opened, said nought milliseconds,
491    /// and the decode thread finished it and moved on, over and over, so it
492    /// never played at all.
493    #[test]
494    fn an_ogg_source_still_ends_at_the_whole_file() {
495        let fx = Fixture::new();
496        fx.push(b"0123456789");
497        let mut src = PartialFileSource::open(
498            &fx.path,
499            fx.written.clone(),
500            1_000,
501            fx.status_fn(),
502            ProbeMode::LengthlessWholeEnd,
503        )
504        .unwrap();
505        // Still no length: the point of opening this way is that Ogg does not
506        // go looking for a tail that has not arrived.
507        assert_eq!(src.byte_len(), None);
508        // But the end it is told about is the file's, not the download's.
509        assert_eq!(src.seek(SeekFrom::End(0)).unwrap(), 1_000);
510    }
511
512    #[test]
513    fn a_landed_download_ends_at_the_whole_file() {
514        // The bound moves with the transfer, so it stops bounding anything
515        // once every byte is there.
516        let fx = Fixture::new();
517        fx.push(b"0123456789");
518        fx.set(COMPLETE);
519
520        let mut src = PartialFileSource::open(
521            &fx.path,
522            fx.written.clone(),
523            10,
524            fx.status_fn(),
525            ProbeMode::Lengthless,
526        )
527        .unwrap();
528        assert_eq!(src.seek(SeekFrom::End(0)).unwrap(), 10);
529    }
530
531    #[test]
532    fn seek_before_start_errors() {
533        let fx = Fixture::new();
534        fx.push(b"hello");
535        assert!(fx.source(5).seek(SeekFrom::Current(-1)).is_err());
536    }
537
538    #[test]
539    fn failed_download_errors_instead_of_reporting_eof() {
540        let fx = Fixture::new();
541        fx.push(b"partial");
542        fx.set(FAILED);
543        let mut src = fx.source(1000);
544
545        let mut out = [0u8; 7];
546        src.read_exact(&mut out).unwrap();
547        assert_eq!(&out, b"partial");
548
549        // Past the written bytes: an error, never a clean EOF — Ok(0) here
550        // would end the track early and look like a short file.
551        assert_eq!(
552            src.read(&mut out).unwrap_err().kind(),
553            io::ErrorKind::BrokenPipe
554        );
555    }
556
557    #[test]
558    fn failure_wakes_a_blocked_reader() {
559        let fx = Fixture::new();
560        let mut src = fx.source(1000);
561
562        let (status, written) = (fx.status.clone(), fx.written.clone());
563        std::thread::spawn(move || {
564            std::thread::sleep(Duration::from_millis(20));
565            status.store(FAILED, Ordering::Release);
566            written.done();
567        });
568
569        let mut out = [0u8; 8];
570        assert_eq!(
571            src.read(&mut out).unwrap_err().kind(),
572            io::ErrorKind::BrokenPipe
573        );
574    }
575
576    #[test]
577    fn a_probe_reads_only_what_has_arrived() {
578        // The first attempt must fail at the write head rather than wait: a
579        // container that would go looking for its tail has to be detected, not
580        // waited for.
581        let fx = Fixture::new();
582        fx.push(b"0123456789");
583        let mut src = PartialFileSource::open_for_probe(
584            &fx.path,
585            fx.written.clone(),
586            1_000,
587            fx.status_fn(),
588            ProbeMode::Full,
589        )
590        .unwrap();
591
592        let mut out = [0u8; 10];
593        src.read_exact(&mut out).unwrap();
594        assert_eq!(
595            src.read(&mut out).unwrap_err().kind(),
596            io::ErrorKind::UnexpectedEof,
597            "past the write head is an answer, not a wait"
598        );
599    }
600
601    #[test]
602    fn playback_reads_wait_for_what_has_not_arrived() {
603        // And the second attempt does wait, which is what lets a container
604        // whose audio starts further in than the streaming threshold — a FLAC
605        // with a large padding block, most of them — be opened at all.
606        let fx = Fixture::new();
607        fx.push(b"0123456789");
608        let mut src = PartialFileSource::open(
609            &fx.path,
610            fx.written.clone(),
611            1_000,
612            fx.status_fn(),
613            ProbeMode::Lengthless,
614        )
615        .unwrap();
616
617        let mut out = [0u8; 10];
618        src.read_exact(&mut out).unwrap();
619
620        std::thread::spawn({
621            let path = fx.path.clone();
622            let written = fx.written.clone();
623            move || {
624                std::thread::sleep(Duration::from_millis(20));
625                let mut f = std::fs::OpenOptions::new()
626                    .append(true)
627                    .open(&path)
628                    .unwrap();
629                f.write_all(b"abcde").unwrap();
630                f.flush().unwrap();
631                written.advance(5);
632            }
633        });
634
635        let n = src.read(&mut out).unwrap();
636        assert_eq!(&out[..n], b"abcde", "it waited rather than giving up");
637    }
638
639    #[test]
640    fn stalled_download_times_out() {
641        // A download with a Content-Length that never arrives: the read must
642        // give up rather than park the decode thread forever.
643        let fx = Fixture::new();
644        let mut src = fx.source_with_stall(1000, Duration::from_millis(20));
645        let mut out = [0u8; 8];
646        assert_eq!(
647            src.read(&mut out).unwrap_err().kind(),
648            io::ErrorKind::TimedOut
649        );
650    }
651
652    #[test]
653    fn completion_ends_the_read_at_the_true_length() {
654        // A chunked transfer reports no total; completion is what says the file
655        // is whole, and its length on disk is what is readable.
656        let fx = Fixture::new();
657        fx.push(b"chunked");
658        fx.set(COMPLETE);
659
660        let mut out = Vec::new();
661        fx.source(0).read_to_end(&mut out).unwrap();
662        assert_eq!(out, b"chunked");
663    }
664
665    #[test]
666    fn survives_the_part_file_being_renamed() {
667        // The download's final act is a rename. A reader that already has the
668        // file open must not notice.
669        let fx = Fixture::new();
670        fx.push(b"0123456789");
671        let mut src = fx.source(10);
672
673        let mut out = [0u8; 4];
674        src.read_exact(&mut out).unwrap();
675        assert_eq!(&out, b"0123");
676
677        std::fs::rename(&fx.path, fx.path.with_extension("")).unwrap();
678        fx.set(COMPLETE);
679
680        let mut rest = Vec::new();
681        src.read_to_end(&mut rest).unwrap();
682        assert_eq!(rest, b"456789");
683    }
684
685    #[test]
686    fn byte_len_is_the_advertised_length_only() {
687        let fx = Fixture::new();
688        assert_eq!(fx.source(42).byte_len(), Some(42));
689        // No Content-Length: the length is genuinely unknown, and claiming one
690        // would have Symphonia compute a duration from it.
691        assert_eq!(fx.source(0).byte_len(), None);
692    }
693
694    #[test]
695    fn is_seekable_true() {
696        let fx = Fixture::new();
697        assert!(fx.source(0).is_seekable());
698    }
699}