From d9818ce9f699f033178384e54d382ffeb98d6cd9 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 8 Aug 2025 00:36:57 +0200 Subject: [PATCH 01/17] feat(decoders)!: implement comprehensive seeking and bit depth detection for all decoders Major decoder enhancements implementing universal try_seek() support, bit depth detection, and changing decoder selection to prioritize alternative decoders over Symphonia. Key changes: - Universal seeking: All decoders implement try_seek() with SeekMode configuration - Bit depth detection: Added Source::bits_per_sample() for lossless formats - Decoder precedence: Alternative decoders now tried before Symphonia decoders - Enhanced DecoderBuilder: Added scan_duration, total_duration, seek_mode settings - Critical fix: Resolved zero span length issue in Symphonia Ogg Vorbis decoder - Performance: Optimized Vorbis, FLAC, and WAV decoder implementations BREAKING CHANGES: - SeekError::NotSupported renamed to SeekError::SeekingNotSupported - WavDecoder no longer implements ExactSizeIterator - Alternative decoders now take precedence when multiple format features enabled - DecoderBuilder::with_coarse_seek deprecated for with_seek_mode Closes: - #190 - #775 --- CHANGELOG.md | 37 +- Cargo.lock | 279 ++++--- Cargo.toml | 10 +- benches/shared.rs | 7 + examples/into_file.rs | 2 +- examples/noise_generator.rs | 4 +- src/buffer.rs | 5 + src/decoder/builder.rs | 696 ++++++++++++++-- src/decoder/flac.rs | 653 ++++++++++++++- src/decoder/mod.rs | 301 ++++--- src/decoder/mp3.rs | 896 +++++++++++++++++++-- src/decoder/read_seek_source.rs | 212 ++++- src/decoder/symphonia.rs | 1309 +++++++++++++++++++++++++++---- src/decoder/utils.rs | 215 +++++ src/decoder/vorbis.rs | 1121 ++++++++++++++++++++++++-- src/decoder/wav.rs | 629 ++++++++++++++- src/math.rs | 4 +- src/mixer.rs | 10 +- src/queue.rs | 5 + src/source/agc.rs | 5 + src/source/amplify.rs | 5 + src/source/blt.rs | 5 + src/source/buffered.rs | 16 +- src/source/channel_volume.rs | 5 + src/source/chirp.rs | 5 + src/source/delay.rs | 5 + src/source/distortion.rs | 5 + src/source/done.rs | 5 + src/source/empty.rs | 9 +- src/source/empty_callback.rs | 9 +- src/source/fadein.rs | 5 + src/source/fadeout.rs | 5 + src/source/from_iter.rs | 9 + src/source/limit.rs | 10 + src/source/linear_ramp.rs | 5 + src/source/mix.rs | 18 +- src/source/mod.rs | 90 ++- src/source/noise.rs | 12 + src/source/pausable.rs | 5 + src/source/periodic.rs | 5 + src/source/position.rs | 5 + src/source/repeat.rs | 8 + src/source/sawtooth.rs | 5 + src/source/signal_generator.rs | 5 + src/source/sine.rs | 5 + src/source/skip.rs | 5 + src/source/skippable.rs | 5 + src/source/spatial.rs | 5 + src/source/speed.rs | 5 + src/source/square.rs | 5 + src/source/stoppable.rs | 5 + src/source/take.rs | 5 + src/source/triangle.rs | 5 + src/source/uniform.rs | 8 + src/source/zero.rs | 5 + src/static_buffer.rs | 7 +- tests/seek.rs | 226 +++--- tests/source_traits.rs | 341 ++++++++ tests/total_duration.rs | 107 --- 59 files changed, 6424 insertions(+), 971 deletions(-) create mode 100644 src/decoder/utils.rs create mode 100644 tests/source_traits.rs delete mode 100644 tests/total_duration.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f44f6d5a..81c1d4ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,27 +10,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added - - `Chirp` now implements `Iterator::size_hint` and `ExactSizeIterator`. - Added `Red` noise generator that is more practical than `Brownian` noise. - Added `std_dev()` to `WhiteUniform` and `WhiteTriangular`. - Added a macro `nz!` which facilitates creating NonZero's for `SampleRate` and `ChannelCount`. -- Adds a new input source: Microphone. -- Adds a new method on source: record which collects all samples into a - SamplesBuffer. +- Adds a new input source: `Microphone`. +- Adds a new method on source: `record` which collects all samples into a + `SamplesBuffer`. - Adds `wav_to_writer` which writes a `Source` to a writer. +- All decoders implement `try_seek()`. +- Added `Source::bits_per_sample()` method for lossless formats (FLAC, WAV). +- Added `SeekMode` enum (`Fastest`/`Nearest`) and DecoderBuilder methods: + - `with_seek_mode()` - Configure seeking precision. + - `with_total_duration()` - Provide pre-computed duration to avoid scanning. + - `with_scan_duration()` - Enable file scanning for duration computation. +- All alternative decoders now support `Settings` via `new_with_settings()`. +- Symphonia decoder handles multi-track containers and chained Ogg streams. + +### Changed +- `output_to_wav` renamed to `wav_to_file` and now takes ownership of the `Source`. +- `Blue` noise generator uses uniform instead of Gaussian noise for better performance. +- `Gaussian` noise generator has standard deviation of 0.6 for perceptual equivalence. +- Alternative decoders (`hound`, `claxon`, `lewton`, `minimp3`) now take precedence over Symphonia + decoders when both features are enabled. +- Breaking: `SeekError::NotSupported` renamed to `SeekError::SeekingNotSupported`. +- Improved `size_hint()` accuracy across all decoders. +- Improved decoder memory allocations and efficiency + +### Deprecated +- `DecoderBuilder::with_coarse_seek()` in favor of `with_seek_mode(SeekMode::Fastest)`. ### Fixed - docs.rs will now document all features, including those that are optional. - `Chirp::next` now returns `None` when the total duration has been reached, and will work correctly for a number of samples greater than 2^24. - `PeriodicAccess` is slightly more accurate for 44.1 kHz sample rate families. +- Fixed Symphonia Ogg Vorbis decoder returning zero span length that broke buffered sources and + effects. -### Changed -- `output_to_wav` renamed to `wav_to_file` and now takes ownership of the `Source`. -- `Blue` noise generator uses uniform instead of Gaussian noise for better performance. -- `Gaussian` noise generator has standard deviation of 0.6 for perceptual equivalence. +### Removed +- `WavDecoder` no longer implements `ExactSizeIterator`. +- `Source::source_intact` removed. ## Version [0.21.1] (2025-07-14) diff --git a/Cargo.lock b/Cargo.lock index d2c88ff6..d94e3921 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,7 +18,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", - "bitflags 2.9.1", + "bitflags 2.9.4", "cfg-if", "libc", ] @@ -74,9 +74,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "bumpalo" @@ -86,9 +86,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" [[package]] name = "byteorder" @@ -104,10 +104,11 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.29" +version = "1.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" +checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44" dependencies = [ + "find-msvc-tools", "shlex", ] @@ -119,24 +120,24 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "clap" -version = "4.5.41" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.41" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" dependencies = [ "anstyle", "clap_lex", @@ -263,7 +264,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "objc2", ] @@ -325,12 +326,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -339,6 +340,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" +[[package]] +name = "find-msvc-tools" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" + [[package]] name = "futures-core" version = "0.3.31" @@ -420,20 +427,20 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.5+wasi-0.2.4", ] [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" [[package]] name = "hound" @@ -443,9 +450,9 @@ checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921" dependencies = [ "equivalent", "hashbrown", @@ -457,7 +464,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "crossterm", "dyn-clone", "fuzzy-matcher", @@ -492,9 +499,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" dependencies = [ "once_cell", "wasm-bindgen", @@ -513,15 +520,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" dependencies = [ "byteorder", - "ogg", + "ogg 0.8.0", "tinyvec", ] [[package]] name = "libc" -version = "0.2.174" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libm" @@ -531,9 +538,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "lock_api" @@ -547,9 +554,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "mach2" @@ -598,13 +605,22 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mp3-duration" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348bdc7300502f0801e5b57c448815713cd843b744ef9bda252a2698fdf90a0f" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "ndk" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "jni-sys", "log", "ndk-sys", @@ -711,9 +727,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" dependencies = [ "objc2-encode", ] @@ -724,7 +740,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "libc", "objc2", "objc2-core-audio", @@ -751,7 +767,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "objc2", ] @@ -761,7 +777,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "dispatch2", "objc2", ] @@ -790,6 +806,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "ogg" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdab8dcd8d4052eaacaf8fb07a3ccd9a6e26efadb42878a413c68fc4af1dee2b" +dependencies = [ + "byteorder", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -857,9 +882,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -903,9 +928,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -956,7 +981,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" dependencies = [ "num-traits", - "rand 0.9.1", + "rand 0.9.2", ] [[package]] @@ -965,14 +990,14 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", @@ -982,9 +1007,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", @@ -993,15 +1018,15 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" +checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30" [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "relative-path" @@ -1024,15 +1049,17 @@ dependencies = [ "inquire", "lewton", "minimp3_fixed", + "mp3-duration", "num-rational", + "ogg 0.9.2", "quickcheck", - "rand 0.9.1", + "rand 0.9.2", "rand_distr", "rstest", "rstest_reuse", "rtrb", "symphonia", - "thiserror 2.0.12", + "thiserror 2.0.16", "tracing", ] @@ -1094,22 +1121,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "same-file" @@ -1388,9 +1415,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -1399,12 +1426,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ "rustix", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1418,11 +1445,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.16", ] [[package]] @@ -1438,9 +1465,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", @@ -1458,9 +1485,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -1521,9 +1548,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-segmentation" @@ -1555,30 +1582,40 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.5+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.0+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" dependencies = [ "bumpalo", "log", @@ -1590,9 +1627,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" dependencies = [ "cfg-if", "js-sys", @@ -1603,9 +1640,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1613,9 +1650,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" dependencies = [ "proc-macro2", "quote", @@ -1626,18 +1663,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" dependencies = [ "js-sys", "wasm-bindgen", @@ -1661,11 +1698,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -1694,6 +1731,18 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-result" version = "0.1.2" @@ -1723,20 +1772,20 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.52.6", + "windows-targets 0.53.3", ] [[package]] name = "windows-sys" -version = "0.60.2" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" dependencies = [ - "windows-targets 0.53.2", + "windows-link 0.2.0", ] [[package]] @@ -1787,10 +1836,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -1983,36 +2033,33 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.1", -] +checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index d43c0676..a1c73540 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,10 +82,10 @@ symphonia-vorbis = ["symphonia/vorbis"] symphonia-wav = ["symphonia/wav"] # Alternative decoders and demuxers -claxon = ["dep:claxon"] # FLAC -hound = ["dep:hound"] # WAV -minimp3 = ["dep:minimp3_fixed"] # MP3 -lewton = ["dep:lewton"] # Ogg Vorbis +claxon = ["dep:claxon"] # FLAC +hound = ["dep:hound"] # WAV +minimp3 = ["dep:minimp3_fixed", "dep:mp3-duration"] # MP3 +lewton = ["dep:lewton", "dep:ogg"] # Ogg Vorbis [package.metadata.docs.rs] all-features = true @@ -97,7 +97,9 @@ dasp_sample = "0.11.0" claxon = { version = "0.4.2", optional = true } hound = { version = "3.5", optional = true } lewton = { version = "0.10", optional = true } +ogg = { version = "0.9.2", optional = true } minimp3_fixed = { version = "0.5.4", optional = true } +mp3-duration = { version = "0.1", optional = true } symphonia = { version = "0.5.4", optional = true, default-features = false } crossbeam-channel = { version = "0.5.15", optional = true } thiserror = "2" diff --git a/benches/shared.rs b/benches/shared.rs index dbe23395..bb524afe 100644 --- a/benches/shared.rs +++ b/benches/shared.rs @@ -8,6 +8,7 @@ pub struct TestSource { samples: vec::IntoIter, channels: ChannelCount, sample_rate: SampleRate, + bits_per_sample: Option, total_duration: Duration, } @@ -47,6 +48,11 @@ impl Source for TestSource { fn total_duration(&self) -> Option { Some(self.total_duration) } + + #[inline] + fn bits_per_sample(&self) -> Option { + self.bits_per_sample + } } pub fn music_wav() -> TestSource { @@ -62,6 +68,7 @@ pub fn music_wav() -> TestSource { channels: sound.channels(), sample_rate: sound.sample_rate(), total_duration: duration, + bits_per_sample: sound.bits_per_sample(), samples: sound.into_iter().collect::>().into_iter(), } } diff --git a/examples/into_file.rs b/examples/into_file.rs index 1e3c9d4b..cc87a8e3 100644 --- a/examples/into_file.rs +++ b/examples/into_file.rs @@ -11,7 +11,7 @@ fn main() -> Result<(), Box> { .speed(0.8); let wav_path = "music_mp3_converted.wav"; - println!("Storing converted audio into {}", wav_path); + println!("Storing converted audio into {wav_path}"); wav_to_file(&mut audio, wav_path)?; Ok(()) diff --git a/examples/noise_generator.rs b/examples/noise_generator.rs index 3adc0f0e..c08dbcc2 100644 --- a/examples/noise_generator.rs +++ b/examples/noise_generator.rs @@ -76,8 +76,8 @@ fn play_noise(stream_handle: &rodio::OutputStream, source: S, name: &str, des where S: Source + Send + 'static, { - println!("{} Noise", name); - println!(" Application: {}", description); + println!("{name} Noise"); + println!(" Application: {description}"); stream_handle.mixer().add( source diff --git a/src/buffer.rs b/src/buffer.rs index 305f6b1f..dd124724 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -91,6 +91,11 @@ impl Source for SamplesBuffer { Some(self.duration) } + #[inline] + fn bits_per_sample(&self) -> Option { + None + } + /// This jumps in memory till the sample for `pos`. #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { diff --git a/src/decoder/builder.rs b/src/decoder/builder.rs index 8d6dc247..ce12144d 100644 --- a/src/decoder/builder.rs +++ b/src/decoder/builder.rs @@ -1,11 +1,36 @@ -//! Builder pattern for configuring and constructing decoders. +//! Builder pattern for configuring and constructing audio decoders. //! -//! This module provides a flexible builder API for creating decoders with custom settings. -//! The builder allows configuring format hints, seeking behavior, byte length and other -//! parameters that affect decoder behavior. +//! This module provides a flexible builder API for creating decoders with custom settings +//! and optimizations. The builder pattern allows fine-grained control over decoder behavior, +//! performance characteristics, and feature enablement before decoder creation. +//! +//! # Architecture +//! +//! The builder system consists of three main components: +//! - **Settings**: Configuration container holding all decoder parameters +//! - **DecoderBuilder**: Fluent API for configuring settings and creating decoders +//! - **SeekMode**: Enum controlling seeking accuracy vs. speed trade-offs +//! +//! # Configuration Categories +//! +//! Settings are organized into several categories: +//! - **Format detection**: Hints and MIME types for faster format identification +//! - **Seeking behavior**: Seeking enablement, modes, and requirements +//! - **Performance**: Duration scanning, gapless playback, buffer management +//! - **Stream properties**: Byte length, seekability, duration information +//! +//! # Performance Optimization +//! +//! The builder enables several performance optimizations: +//! - **Format hints**: Reduce format detection overhead +//! - **Byte length**: Enable efficient seeking and duration calculation +//! - **Seek mode selection**: Balance speed vs. accuracy based on use case +//! - **Duration scanning**: Control expensive file analysis operations //! //! # Examples //! +//! ## Basic Usage +//! //! ```no_run //! use std::fs::File; //! use rodio::Decoder; @@ -14,10 +39,10 @@ //! let file = File::open("audio.mp3")?; //! let len = file.metadata()?.len(); //! -//! Decoder::builder() +//! let decoder = Decoder::builder() //! .with_data(file) //! .with_byte_len(len) // Enable seeking and duration calculation -//! .with_hint("mp3") // Optional format hint +//! .with_hint("mp3") // Optional format hint for performance //! .with_gapless(true) // Enable gapless playback //! .build()?; //! @@ -26,18 +51,59 @@ //! } //! ``` //! -//! # Settings +//! ## Advanced Configuration +//! +//! ```no_run +//! use std::fs::File; +//! use std::time::Duration; +//! use rodio::{Decoder, decoder::builder::SeekMode}; +//! +//! fn main() -> Result<(), Box> { +//! let file = File::open("audio.flac")?; +//! let len = file.metadata()?.len(); +//! +//! let decoder = Decoder::builder() +//! .with_data(file) +//! .with_byte_len(len) +//! .with_hint("flac") +//! .with_mime_type("audio/flac") +//! .with_seekable(true) +//! .with_seek_mode(SeekMode::Nearest) +//! .with_scan_duration(true) +//! .with_gapless(false) +//! .build()?; +//! +//! // High-quality decoder with precise seeking +//! Ok(()) +//! } +//! ``` +//! +//! # Configuration Reference //! //! The following settings can be configured: //! -//! - `byte_len` - Total length of the input data in bytes -//! - `hint` - Format hint like "mp3", "wav", etc -//! - `mime_type` - MIME type hint for container formats -//! - `seekable` - Whether seeking operations are enabled -//! - `gapless` - Enable gapless playback -//! - `coarse_seek` - Use faster but less precise seeking +//! - **`byte_len`**: Total length of the input data in bytes (enables seeking/duration) +//! - **`hint`**: Format hint like "mp3", "wav", "flac" for faster detection +//! - **`mime_type`**: MIME type hint for container format identification +//! - **`seekable`**: Whether random access seeking operations are enabled +//! - **`seek_mode`**: Balance between seeking speed and accuracy +//! - **`gapless`**: Enable gapless playback for supported formats +//! - **`scan_duration`**: Allow expensive file scanning for duration calculation +//! - **`total_duration`**: Pre-computed duration to avoid file scanning +//! +//! # Format Compatibility +//! +//! Different formats benefit from different configuration approaches: +//! - **MP3**: Benefits from byte length and duration scanning +//! - **FLAC**: Excellent seeking with any configuration +//! - **OGG Vorbis**: Requires seekable flag and benefits from duration scanning +//! - **WAV**: Excellent performance with minimal configuration +//! - **Symphonia formats**: May require format hints for optimal detection -use std::io::{Read, Seek}; +use std::{ + io::{Read, Seek}, + time::Duration, +}; #[cfg(feature = "symphonia")] use self::read_seek_source::ReadSeekSource; @@ -46,59 +112,315 @@ use ::symphonia::core::io::{MediaSource, MediaSourceStream}; use super::*; +/// Seeking modes for audio decoders. +/// +/// This enum controls the trade-off between seeking speed and accuracy. Different modes +/// are appropriate for different use cases, and format support varies. +/// +/// # Performance Characteristics +/// +/// - **Fastest**: Optimized for speed, may sacrifice precision +/// - **Nearest**: Optimized for accuracy, may sacrifice speed +/// +/// # Format Support +/// +/// Not all formats support both modes equally: +/// - **MP3**: Fastest requires byte length, otherwise falls back to Nearest +/// - **FLAC**: Both modes generally equivalent (always accurate) +/// - **OGG Vorbis**: Fastest uses granule-based seeking, Nearest uses linear +/// - **WAV**: Both modes equivalent (always fast and accurate) +/// - **Symphonia**: Mode support varies by underlying format +/// +/// # Use Case Guidelines +/// +/// - **User scrubbing**: Use Fastest for responsive UI +/// - **Gapless playback**: Use Nearest for seamless transitions +/// - **Real-time applications**: Use Fastest to minimize latency +/// - **Audio analysis**: Use Nearest for sample-accurate positioning +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum SeekMode { + /// Use the fastest available seeking method with coarse positioning. + /// + /// This mode prioritizes speed over precision, using format-specific optimizations + /// that may result in positioning slightly before or after the requested time, + /// especially with variable bitrate content. + /// + /// # Behavior + /// + /// - Uses coarse seeking when available (keyframe-based, byte-position estimation) + /// - Falls back to nearest seeking when coarse seeking unavailable + /// - May require additional sample skipping for exact positioning + /// - Optimal for user interface responsiveness and scrubbing + /// + /// # Requirements + /// + /// For optimal performance with this mode: + /// - Set `seekable` to enable backward seeking + /// - Set `byte_len` for formats that need it (especially MP3) + /// - Consider format-specific limitations and fallback behavior + /// + /// # Performance + /// + /// Typically provides O(1) or O(log n) seeking performance depending on format, + /// making it suitable for real-time applications and responsive user interfaces. + Fastest, + + /// Use the most accurate seeking method available with precise positioning. + /// + /// This mode prioritizes accuracy over speed, seeking to the exact sample requested + /// whenever possible. It provides sample-accurate positioning for applications + /// requiring precise timing control. + /// + /// # Behavior + /// + /// - Uses accurate seeking when available (sample-level positioning) + /// - Falls back to nearest seeking with refinement when accurate unavailable + /// - Falls back to coarse seeking only as last resort + /// - Performs additional sample-level positioning for exact results + /// + /// # Requirements + /// + /// For optimal accuracy with this mode: + /// - Set `seekable` to enable full seeking capabilities + /// - Set `byte_len` for reliable positioning calculations + /// - Expect potentially slower seeking performance + /// + /// # Performance + /// + /// May provide O(n) seeking performance for some formats due to linear + /// sample consumption, but guarantees sample-accurate results. + /// + /// # Use Cases + /// + /// Ideal for gapless playback, audio analysis, precise editing operations, + /// and any application where exact positioning is more important than speed. + #[default] + Nearest, +} + /// Audio decoder configuration settings. -/// Support for these settings depends on the underlying decoder implementation. -/// Currently, settings are only used by the Symphonia decoder. +/// +/// This structure contains all configurable parameters that affect decoder behavior, +/// performance, and capabilities. Settings are organized into logical groups for +/// different aspects of decoder operation. +/// +/// # Settings Categories +/// +/// - **Stream properties**: `byte_len`, `is_seekable`, `total_duration` +/// - **Format detection**: `hint`, `mime_type` +/// - **Seeking behavior**: `seek_mode`, `is_seekable` +/// - **Playback features**: `gapless`, `scan_duration` +/// +/// # Decoder Support +/// +/// Support for these settings varies by decoder implementation: +/// - Some settings are universal (e.g., `is_seekable`) +/// - Others are format-specific (e.g., `gapless` for supported formats) +/// - Unsupported settings are typically ignored gracefully +/// +/// # Performance Impact +/// +/// Several settings significantly affect performance: +/// - `byte_len`: Enables efficient seeking and duration calculation +/// - `hint`/`mime_type`: Reduce format detection overhead +/// - `scan_duration`: Controls expensive file analysis operations +/// - `seek_mode`: Balances seeking speed vs. accuracy #[derive(Clone, Debug)] pub struct Settings { - /// The length of the stream in bytes. - /// This is required for: - /// - Reliable seeking operations - /// - Duration calculations in formats that lack timing information (e.g. MP3, Vorbis) + /// The total length of the stream in bytes. + /// + /// This setting enables several important optimizations: + /// - **Seeking operations**: Required for reliable backward seeking + /// - **Duration calculations**: Essential for formats lacking timing metadata + /// - **Progress indication**: Enables accurate progress tracking + /// - **Buffer optimization**: Helps with memory management decisions + /// + /// # Format Requirements /// - /// Can be obtained from file metadata or by seeking to the end of the stream. - pub(crate) byte_len: Option, + /// - **MP3**: Required for coarse seeking and duration scanning + /// - **OGG Vorbis**: Used for duration scanning optimization + /// - **FLAC/WAV**: Improves seeking performance but not strictly required + /// - **Symphonia**: May be used for internal optimizations + /// + /// # Obtaining Byte Length + /// + /// Can be obtained from: + /// - File metadata: `file.metadata()?.len()` + /// - Stream seeking: `stream.seek(SeekFrom::End(0))?` + /// - HTTP Content-Length headers for network streams + pub byte_len: Option, - /// Whether to use coarse seeking, or sample-accurate seeking instead. - pub(crate) coarse_seek: bool, + /// The seeking mode controlling speed vs. accuracy trade-offs. + /// + /// This setting affects seeking behavior across all decoder implementations + /// that support seeking operations. The actual behavior depends on format + /// capabilities and available optimizations. + pub seek_mode: SeekMode, - /// Whether to trim frames for gapless playback. - /// Note: Disabling this may affect duration calculations for some formats - /// as padding frames will be included. - pub(crate) gapless: bool, + /// Whether to enable gapless playback by trimming padding frames. + /// + /// When enabled, removes silence padding added during encoding to achieve + /// seamless transitions between tracks. This is particularly important for + /// albums designed for continuous playback. + /// + /// # Format Support + /// + /// - **MP3**: Removes encoder delay and padding frames + /// - **AAC**: Removes padding specified in container metadata + /// - **FLAC**: Generally gapless by nature, minimal effect + /// - **OGG Vorbis**: Handles sample-accurate boundaries + /// + /// # Duration Impact + /// + /// Disabling gapless may affect duration calculations as padding frames + /// will be included in the total sample count for some formats. + pub gapless: bool, - /// An extension hint for the decoder about the format of the stream. - /// When known, this can help the decoder to select the correct codec. - pub(crate) hint: Option, + /// Format extension hint for accelerated format detection. + /// + /// Providing an accurate hint significantly improves decoder initialization + /// performance by reducing the number of format probes required. Common + /// values include file extensions without the dot. + /// + /// # Common Values + /// + /// - Audio formats: "mp3", "flac", "wav", "ogg", "m4a" + /// - Container hints: "mp4", "mkv", "webm" + /// - Codec hints: "aac", "opus", "vorbis" + /// + /// # Performance Impact + /// + /// Without hints, decoders must probe all supported formats sequentially, + /// which can be slow for large format lists or complex containers. + pub hint: Option, - /// An MIME type hint for the decoder about the format of the stream. - /// When known, this can help the decoder to select the correct demuxer. - pub(crate) mime_type: Option, + /// MIME type hint for container format identification. + /// + /// Provides additional information for format detection, particularly useful + /// for network streams where file extensions may not be available. This + /// complements the extension hint for comprehensive format identification. + /// + /// # Common Values + /// + /// - "audio/mpeg" (MP3) + /// - "audio/flac" (FLAC) + /// - "audio/ogg" (OGG Vorbis/Opus) + /// - "audio/mp4" or "audio/aac" (AAC in MP4) + /// - "audio/wav" or "audio/vnd.wav" (WAV) + pub mime_type: Option, - /// Whether the decoder should report as seekable. - pub(crate) is_seekable: bool, + /// Whether the decoder should report seeking capabilities. + /// + /// This setting controls whether the decoder will attempt backward seeking + /// operations. When disabled, only forward seeking (sample skipping) is + /// allowed, which is suitable for streaming scenarios. + /// + /// # Requirements + /// + /// For reliable seeking behavior: + /// - The underlying stream must support `Seek` trait + /// - `byte_len` should be set for optimal performance + /// - Some formats may have additional requirements + /// + /// # Automatic Setting + /// + /// This is automatically set to `true` when `byte_len` is provided, + /// as byte length information typically implies seekable streams. + pub is_seekable: bool, + + /// Pre-computed total duration to avoid expensive file scanning. + /// + /// When provided, decoders will use this value instead of performing + /// potentially expensive duration calculation operations. This is + /// particularly useful when duration is known from external metadata. + /// + /// # Use Cases + /// + /// - Database-stored track durations + /// - Previously calculated durations + /// - External metadata sources (ID3, database, etc.) + /// - Avoiding redundant file scanning in batch operations + /// + /// # Priority + /// + /// This setting takes precedence over `scan_duration` when both are set. + pub total_duration: Option, + + /// Enable expensive file scanning for accurate duration computation. + /// + /// When enabled, allows decoders to perform comprehensive file analysis + /// to determine accurate duration information. This can be slow for large + /// files but provides the most accurate duration data. + /// + /// # Prerequisites + /// + /// This setting only takes effect when: + /// - `is_seekable` is `true` + /// - `byte_len` is set + /// - The decoder supports duration scanning + /// + /// # Format Behavior + /// + /// - **MP3**: Scans for XING/VBRI headers, then frame-by-frame if needed + /// - **OGG Vorbis**: Uses binary search to find last granule position + /// - **FLAC**: Duration available in metadata, no scanning needed + /// - **WAV**: Duration available in header, no scanning needed + /// + /// # Performance Impact + /// + /// Scanning time varies significantly: + /// - Small files (< 10MB): Usually very fast + /// - Large files (> 100MB): Can take several seconds + /// - Variable bitrate files: May require more extensive scanning + pub scan_duration: bool, } impl Default for Settings { fn default() -> Self { Self { byte_len: None, - coarse_seek: false, + seek_mode: SeekMode::default(), gapless: true, hint: None, mime_type: None, is_seekable: false, + total_duration: None, + scan_duration: false, } } } -/// Builder for configuring and creating a decoder. +/// Builder for configuring and creating audio decoders. +/// +/// This builder provides a fluent API for configuring decoder settings before creation. +/// It follows the builder pattern to enable method chaining and ensures that all +/// necessary configuration is provided before decoder instantiation. +/// +/// # Design Philosophy +/// +/// The builder is designed to: +/// - **Prevent invalid configurations**: Validates settings at build time +/// - **Optimize performance**: Enables format-specific optimizations +/// - **Simplify common cases**: Provides sensible defaults for most use cases +/// - **Support advanced scenarios**: Allows fine-grained control when needed +/// +/// # Configuration Flow +/// +/// 1. **Create builder**: `DecoderBuilder::new()` +/// 2. **Set data source**: `.with_data(source)` +/// 3. **Configure options**: `.with_hint()`, `.with_seekable()`, etc. +/// 4. **Build decoder**: `.build()` or `.build_looped()` +/// +/// # Error Handling /// -/// This provides a flexible way to configure decoder settings before creating -/// the actual decoder instance. +/// The builder defers most validation to build time, allowing for flexible +/// configuration while ensuring that invalid combinations are caught before +/// decoder creation. /// /// # Examples /// +/// ## Basic File Decoding +/// /// ```no_run /// use std::fs::File; /// use rodio::decoder::DecoderBuilder; @@ -115,11 +437,41 @@ impl Default for Settings { /// Ok(()) /// } /// ``` +/// +/// ## High-Performance Configuration +/// +/// ```no_run +/// use std::fs::File; +/// use rodio::{decoder::DecoderBuilder, decoder::builder::SeekMode}; +/// +/// fn main() -> Result<(), Box> { +/// let file = File::open("audio.flac")?; +/// let len = file.metadata()?.len(); +/// +/// let decoder = DecoderBuilder::new() +/// .with_data(file) +/// .with_byte_len(len) +/// .with_hint("flac") +/// .with_seekable(true) +/// .with_seek_mode(SeekMode::Fastest) +/// .build()?; +/// +/// // Optimized decoder ready for use +/// Ok(()) +/// } +/// ``` #[derive(Clone, Debug)] pub struct DecoderBuilder { /// The input data source to decode. + /// + /// Holds the audio data stream until decoder creation. Must implement + /// `Read + Seek + Send + Sync` for compatibility with all decoder types. data: Option, + /// Configuration settings for the decoder. + /// + /// Contains all parameters that will be used to configure the decoder + /// behavior, performance characteristics, and feature enablement. settings: Settings, } @@ -135,7 +487,15 @@ impl Default for DecoderBuilder { impl DecoderBuilder { /// Creates a new decoder builder with default settings. /// + /// Initializes the builder with sensible defaults suitable for most use cases: + /// - Gapless playback enabled + /// - Nearest seeking mode (accuracy over speed) + /// - No format hints (universal detection) + /// - Seeking disabled (streaming-friendly) + /// - Duration scanning disabled + /// /// # Examples + /// /// ```no_run /// use std::fs::File; /// use rodio::decoder::DecoderBuilder; @@ -146,15 +506,40 @@ impl DecoderBuilder { /// .with_data(file) /// .build()?; /// - /// // Use the decoder... + /// // Use the decoder with default settings /// Ok(()) /// } /// ``` + /// + /// # Default Configuration + /// + /// The default configuration prioritizes compatibility and quality over performance, + /// making it suitable for most applications without additional configuration. pub fn new() -> Self { Self::default() } /// Sets the input data source to decode. + /// + /// The data source must implement `Read + Seek + Send + Sync` to be compatible + /// with all decoder implementations. Most standard types like `File`, `Cursor>`, + /// and `BufReader` satisfy these requirements. + /// + /// # Examples + /// + /// ```no_run + /// use std::fs::File; + /// use rodio::decoder::DecoderBuilder; + /// + /// let file = File::open("audio.wav").unwrap(); + /// let builder = DecoderBuilder::new().with_data(file); + /// ``` + /// + /// # Requirements + /// + /// The data source must remain valid for the lifetime of the decoder, + /// as seeking and reading operations will be performed on it throughout + /// the decoding process. pub fn with_data(mut self, data: R) -> Self { self.data = Some(data); self @@ -164,10 +549,17 @@ impl DecoderBuilder { /// This is required for: /// - Reliable seeking operations /// - Duration calculations in formats that lack timing information (e.g. MP3, Vorbis) + /// when `scan_duration` is enabled + /// - Symphonia decoders may also use this for internal scanning operations /// /// Note that this also sets `is_seekable` to `true`. /// - /// The byte length should typically be obtained from file metadata: + /// To enable duration scanning for formats that require it, use `with_scan_duration(true)`. + /// File-based decoders (`try_from(File)`) automatically enable scanning. + /// + /// `DecoderBuilder::try_from::()` automatically sets this from file metadata. + /// Alternatively, you can set it manually from file metadata: + /// /// ```no_run /// use std::fs::File; /// use rodio::Decoder; @@ -185,7 +577,11 @@ impl DecoderBuilder { /// } /// ``` /// - /// Alternatively, it can be obtained by seeking to the end of the stream. + /// The byte length may also be obtained by seeking to the end of the stream: + /// + /// ```ignore + /// let len = data.seek(std::io::SeekFrom::End(0))?; + /// ``` /// /// An incorrect byte length can lead to unexpected behavior, including but not limited to /// incorrect duration calculations and seeking errors. @@ -195,13 +591,27 @@ impl DecoderBuilder { self } + /// Sets the seeking mode for the decoder. + pub fn with_seek_mode(mut self, seek_mode: SeekMode) -> Self { + self.settings.seek_mode = seek_mode; + self + } + /// Enables or disables coarse seeking. This is disabled by default. /// - /// This needs `byte_len` to be set. Coarse seeking is faster but less accurate: - /// it may seek to a position slightly before or after the requested one, - /// especially when the bitrate is variable. + /// This may also need `byte_len` to be set. Coarse seeking is faster but less accurate: it may + /// seek to a position slightly before or after the requested one, especially when the bitrate + /// is variable. + #[deprecated( + note = "Use `with_seek_mode(SeekMode::Fastest)` instead.", + since = "0.22.0" + )] pub fn with_coarse_seek(mut self, coarse_seek: bool) -> Self { - self.settings.coarse_seek = coarse_seek; + if coarse_seek { + self.settings.seek_mode = SeekMode::Fastest; + } else { + self.settings.seek_mode = SeekMode::Nearest; + } self } @@ -231,12 +641,13 @@ impl DecoderBuilder { self } - /// Configure whether the data supports random access seeking. Without this, - /// only forward seeking may work. + /// Configure whether the data supports random access seeking. Without this, only forward + /// seeking may work. /// - /// For reliable seeking behavior, `byte_len` should be set either from file metadata - /// or by seeking to the end of the stream. While seeking may work without `byte_len` - /// for some formats, it is not guaranteed. + /// `DecoderBuilder::try_from::()` automatically sets this to `true`. + /// + /// For reliable seeking behavior, `byte_len` should also be set. While random access seeking + /// may work without `byte_len` for some decoders, it is not guaranteed. /// /// # Examples /// ```no_run @@ -263,29 +674,119 @@ impl DecoderBuilder { self } + /// Provides a pre-computed total duration to avoid file scanning. + /// + /// When provided, decoders will use this value when they would otherwise need to scan the file. + /// + /// This affects decoder implementations that may scan for duration: + /// - **MP3**: May scan if metadata doesn't contain duration + /// - **Vorbis/OGG**: Scans to determine total duration + /// - **Symphonia**: Not controlled by this setting; may scan if byte_len is set + /// + /// # Examples + /// ```no_run + /// use std::time::Duration; + /// use std::fs::File; + /// use rodio::Decoder; + /// + /// let file = File::open("audio.mp3").unwrap(); + /// let known_duration = Duration::from_secs(180); + /// + /// let decoder = Decoder::builder() + /// .with_data(file) + /// .with_total_duration(known_duration) // Skip any scanning + /// .build() + /// .unwrap(); + /// ``` + pub fn with_total_duration(mut self, duration: Duration) -> Self { + self.settings.total_duration = Some(duration); + self + } + + /// Enable file scanning for duration computation. + /// + /// **Important**: This setting only takes effect when the source is both seekable and has + /// `byte_len` is set. If these prerequisites are not met, this setting is ignored. + /// + /// This affects specific decoder implementations: + /// - **MP3**: May scan if metadata doesn't contain duration + /// - **Vorbis/OGG**: Scans to determine total duration + /// - **Symphonia**: Not controlled by this setting; may scan if byte_len is set + /// + /// File-based decoders (`Decoder::try_from(file)`) automatically enable this and set the + /// required prerequisites. + /// + /// # Examples + /// ```no_run + /// use std::fs::File; + /// use rodio::Decoder; + /// + /// let file = File::open("audio.mp3").unwrap(); + /// let len = file.metadata().unwrap().len(); + /// + /// # let file = std::fs::File::open("audio.mp3").unwrap(); + /// # let len = file.metadata().unwrap().len(); + /// let decoder = Decoder::builder() + /// .with_data(file) + /// .with_byte_len(len) // Required + /// .with_seekable(true) // Already set by with_byte_len() + /// .with_scan_duration(true) // Now effective + /// .build() + /// .unwrap(); + /// ``` + pub fn with_scan_duration(mut self, scan: bool) -> Self { + self.settings.scan_duration = scan; + self + } + /// Creates the decoder implementation with configured settings. + /// + /// This internal method handles the format detection and decoder creation process. + /// It attempts to create decoders in a specific order, passing the data source + /// between attempts until a compatible format is found. + /// + /// # Format Detection Order + /// + /// Decoders are tried in this order for optimal performance: + /// 1. **WAV**: Fast header-based detection + /// 2. **FLAC**: Distinctive magic bytes for quick identification + /// 3. **OGG Vorbis**: Well-defined container format + /// 4. **MP3**: Frame-based detection (more expensive) + /// 5. **Symphonia**: Multi-format fallback (most comprehensive) + /// + /// # Error Handling + /// + /// Each decoder attempts format detection and returns either: + /// - `Ok(decoder)`: Format recognized and decoder created + /// - `Err(data)`: Format not recognized, data returned for next attempt + /// + /// # Performance Optimization + /// + /// Format hints can significantly improve this process by allowing + /// the appropriate decoder to be tried first, reducing detection overhead. + #[allow(unused_variables)] fn build_impl(self) -> Result<(DecoderImpl, Settings), DecoderError> { let data = self.data.ok_or(DecoderError::UnrecognizedFormat)?; - #[cfg(all(feature = "hound", not(feature = "symphonia-wav")))] - let data = match wav::WavDecoder::new(data) { + #[cfg(feature = "hound")] + let data = match wav::WavDecoder::new_with_settings(data, &self.settings) { Ok(decoder) => return Ok((DecoderImpl::Wav(decoder), self.settings)), Err(data) => data, }; - #[cfg(all(feature = "claxon", not(feature = "symphonia-flac")))] - let data = match flac::FlacDecoder::new(data) { + #[cfg(feature = "claxon")] + let data = match flac::FlacDecoder::new_with_settings(data, &self.settings) { Ok(decoder) => return Ok((DecoderImpl::Flac(decoder), self.settings)), Err(data) => data, }; - #[cfg(all(feature = "lewton", not(feature = "symphonia-vorbis")))] - let data = match vorbis::VorbisDecoder::new(data) { + #[cfg(feature = "lewton")] + let data = match vorbis::VorbisDecoder::new_with_settings(data, &self.settings) { Ok(decoder) => return Ok((DecoderImpl::Vorbis(decoder), self.settings)), Err(data) => data, }; - #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] - let data = match mp3::Mp3Decoder::new(data) { + #[cfg(feature = "minimp3")] + let data = match mp3::Mp3Decoder::new_with_settings(data, &self.settings) { Ok(decoder) => return Ok((DecoderImpl::Mp3(decoder), self.settings)), Err(data) => data, }; @@ -297,7 +798,7 @@ impl DecoderBuilder { Default::default(), ); - symphonia::SymphoniaDecoder::new(mss, &self.settings) + symphonia::SymphoniaDecoder::new_with_settings(mss, &self.settings) .map(|decoder| (DecoderImpl::Symphonia(decoder, PhantomData), self.settings)) } @@ -307,10 +808,37 @@ impl DecoderBuilder { /// Creates a new decoder with previously configured settings. /// - /// # Errors + /// This method finalizes the builder configuration and attempts to create + /// an appropriate decoder for the provided data source. Format detection + /// is performed automatically unless format hints are provided. + /// + /// # Error Handling /// - /// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined - /// or is not supported. + /// Returns `DecoderError::UnrecognizedFormat` if: + /// - No data source was provided via `with_data()` + /// - The audio format could not be determined from the data + /// - No enabled decoder supports the detected format + /// - The file is corrupted or incomplete + /// + /// # Examples + /// + /// ```no_run + /// use std::fs::File; + /// use rodio::decoder::DecoderBuilder; + /// + /// let file = File::open("audio.mp3").unwrap(); + /// let decoder = DecoderBuilder::new() + /// .with_data(file) + /// .with_hint("mp3") + /// .build() + /// .unwrap(); + /// ``` + /// + /// # Performance Notes + /// + /// - Format hints significantly improve build performance + /// - Large files may take longer due to format detection + /// - Duration scanning (if enabled) may add additional build time pub fn build(self) -> Result, DecoderError> { let (decoder, _) = self.build_impl()?; Ok(Decoder(decoder)) @@ -318,15 +846,49 @@ impl DecoderBuilder { /// Creates a new looped decoder with previously configured settings. /// - /// # Errors + /// This method creates a decoder that automatically restarts from the beginning + /// when it reaches the end of the audio stream, providing seamless looping + /// functionality for background music and ambient audio. + /// + /// # Looping Behavior + /// + /// The looped decoder: + /// - Automatically resets to the beginning when the stream ends + /// - Preserves all configured settings across loop iterations + /// - Maintains consistent audio quality and timing + /// - Supports seeking operations within the current loop + /// + /// # Error Handling + /// + /// Returns the same errors as `build()`: + /// - `DecoderError::UnrecognizedFormat` for format detection failures + /// - Other decoder-specific errors during initialization + /// + /// # Examples + /// + /// ```no_run + /// use std::fs::File; + /// use rodio::decoder::DecoderBuilder; + /// + /// let file = File::open("background_music.ogg").unwrap(); + /// let looped_decoder = DecoderBuilder::new() + /// .with_data(file) + /// .with_hint("ogg") + /// .with_gapless(true) + /// .build_looped() + /// .unwrap(); + /// ``` + /// + /// # Performance Considerations /// - /// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined - /// or is not supported. + /// Looped decoders cache duration information to avoid recalculating it + /// on each loop iteration, improving performance for repeated playback. pub fn build_looped(self) -> Result, DecoderError> { let (decoder, settings) = self.build_impl()?; Ok(LoopedDecoder { inner: Some(decoder), settings, + cached_duration: None, }) } } diff --git a/src/decoder/flac.rs b/src/decoder/flac.rs index be93d6bc..8ae3e11f 100644 --- a/src/decoder/flac.rs +++ b/src/decoder/flac.rs @@ -1,62 +1,302 @@ -use std::io::{Read, Seek, SeekFrom}; -use std::mem; -use std::time::Duration; +//! FLAC audio decoder implementation. +//! +//! This module provides FLAC decoding capabilities using the `claxon` library. The FLAC format +//! is a lossless audio compression format that preserves original audio data while reducing file +//! size through sophisticated entropy coding and decorrelation techniques. +//! +//! # Features +//! +//! - **Bit depths**: Full support for 8, 16, 24, and 32-bit audio (including 12 and 20-bit) +//! - **Sample rates**: Supports all FLAC-compatible sample rates (1Hz to 655,350Hz) +//! - **Channels**: Supports mono, stereo, and multi-channel audio (up to 8 channels) +//! - **Seeking**: Full forward and backward seeking with sample-accurate positioning +//! - **Duration**: Accurate total duration calculation from stream metadata +//! - **Performance**: Optimized block-based decoding with reusable buffers +//! +//! # Advantages +//! +//! - **Perfect quality**: Lossless compression preserves original audio fidelity +//! - **Efficient compression**: Typically 30-50% size reduction vs. uncompressed +//! - **Fast decoding**: Optimized algorithms with minimal computational overhead +//! - **Precise seeking**: Sample-accurate positioning without approximation +//! - **Rich metadata**: Comprehensive format specification with extensive metadata support +//! +//! # Limitations +//! +//! - Seeking requires `is_seekable` setting for backward seeks (forward-only otherwise) +//! - No support for embedded cue sheets or complex metadata (focus on audio data) +//! - Larger files than lossy formats (trade-off for perfect quality) +//! +//! # Configuration +//! +//! The decoder can be configured through `DecoderBuilder`: +//! - `with_seekable(true)` - Enable backward seeking (recommended for FLAC) +//! - Other settings are informational and don't affect FLAC decoding performance +//! +//! # Performance Notes +//! +//! - Block-based decoding minimizes memory allocations during playback +//! - Seeking operations use efficient linear scanning or stream reset +//! - Buffer reuse optimizes performance for continuous playback +//! - Metadata parsing optimized for audio-focused applications +//! - Memory usage scales with maximum block size, not file size +//! +//! # Example +//! +//! ```ignore +//! use std::fs::File; +//! use rodio::Decoder; +//! +//! let file = File::open("audio.flac").unwrap(); +//! let decoder = Decoder::builder() +//! .with_data(file) +//! .with_seekable(true) +//! .build() +//! .unwrap(); +//! +//! // FLAC supports seeking and bit depth detection +//! println!("Bit depth: {:?}", decoder.bits_per_sample()); +//! println!("Duration: {:?}", decoder.total_duration()); +//! println!("Sample rate: {}", decoder.sample_rate().get()); +//! ``` -use crate::source::SeekError; -use crate::Source; +use std::{ + io::{Read, Seek}, + mem, + sync::Arc, + time::Duration, +}; -use crate::common::{ChannelCount, Sample, SampleRate}; - -use claxon::FlacReader; +use claxon::{FlacReader, FlacReaderOptions}; use dasp_sample::Sample as _; use dasp_sample::I24; -/// Decoder for the FLAC format. +use super::{utils, Settings}; +use crate::{ + common::{ChannelCount, Sample, SampleRate}, + source::SeekError, + Source, +}; + +/// Reader options for `claxon` FLAC decoder. +/// +/// Configured to skip metadata parsing and vorbis comments for faster initialization. +/// This improves decoder creation performance by only parsing essential stream information +/// needed for audio playback. +/// +/// # Fields +/// +/// - `metadata_only`: Set to `false` to parse audio blocks, not just metadata +/// - `read_vorbis_comment`: Set to `false` to skip comment blocks for performance +const READER_OPTIONS: FlacReaderOptions = FlacReaderOptions { + metadata_only: false, + read_vorbis_comment: false, +}; + +/// Decoder for the FLAC format using the `claxon` library. +/// +/// Provides lossless audio decoding with block-based processing, linear seeking, and duration +/// calculation through FLAC stream metadata analysis. The decoder maintains internal buffers +/// for efficient sample-by-sample iteration while preserving the original audio quality. +/// +/// # Block-based Processing +/// +/// FLAC audio is organized into variable-size blocks containing interleaved samples. +/// The decoder maintains a buffer of the current block and tracks position within it +/// for efficient sample access without re-decoding. +/// +/// # Memory Management +/// +/// The decoder pre-allocates buffers based on FLAC stream metadata (maximum block size) +/// to minimize allocations during playback. Buffers are reused across blocks for +/// optimal performance. +/// +/// # Thread Safety +/// +/// This decoder is not thread-safe. Create separate instances for concurrent access +/// or use appropriate synchronization primitives. +/// +/// # Generic Parameters +/// +/// * `R` - The underlying data source type, must implement `Read + Seek` pub struct FlacDecoder where R: Read + Seek, { - reader: FlacReader, + /// The underlying FLAC reader, wrapped in Option for seeking operations. + /// + /// Temporarily set to `None` during stream reset operations for backward seeking. + /// Always `Some` during normal operation and iteration. + reader: Option>, + + /// Buffer containing decoded samples from current block. + /// + /// Stores raw i32 samples from the current FLAC block in the decoder's native + /// interleaved format. Capacity is pre-allocated based on stream's maximum block size. current_block: Vec, + + /// Number of samples per channel in current block. + /// + /// Used for calculating the correct memory layout when accessing interleaved samples. + /// FLAC blocks can have variable sizes, so this changes per block. current_block_channel_len: usize, + + /// Current position within the current block. + /// + /// Tracks the next sample index to return from `current_block`. When this reaches + /// `current_block.len()`, a new block must be decoded. current_block_off: usize, + + /// Number of bits per sample (8, 12, 16, 20, 24, or 32). + /// + /// Preserved from the original FLAC stream metadata and used for proper + /// sample conversion during iteration. FLAC supports various bit depths + /// including non-standard ones like 12 and 20-bit. bits_per_sample: u32, + + /// Sample rate in Hz. + /// + /// Cached from FLAC stream metadata. FLAC supports sample rates from 1 Hz + /// to 655,350 Hz, though typical rates are 44.1kHz, 48kHz, 96kHz, etc. sample_rate: SampleRate, + + /// Number of audio channels. + /// + /// FLAC supports 1 to 8 channels. Common configurations include mono (1), + /// stereo (2), 5.1 surround (6), and 7.1 surround (8). channels: ChannelCount, + + /// Total duration if known from stream metadata. + /// + /// Calculated from the total sample count in FLAC metadata. `None` indicates + /// missing or invalid metadata, though this is rare for valid FLAC files. total_duration: Option, + + /// Total number of audio frames in the stream. + /// + /// Represents the total number of inter-channel samples (frames) as stored + /// in FLAC metadata. Used for accurate seeking and duration calculation. + total_samples: Option, + + /// Number of samples read so far (for seeking calculations). + /// + /// Tracks the current playback position in total samples (across all channels). + /// Used to determine if seeking requires stream reset or can be done by + /// skipping forward. + samples_read: u64, + + /// Whether the stream supports random access seeking. + /// + /// When `true`, enables backward seeking by allowing stream reset operations. + /// When `false`, only forward seeking (sample skipping) is allowed. + is_seekable: bool, } impl FlacDecoder where R: Read + Seek, { - /// Attempts to decode the data as FLAC. - pub fn new(mut data: R) -> Result, R> { - if !is_flac(data.by_ref()) { + /// Attempts to decode the data as FLAC with default settings. + /// + /// This method probes the input data to detect FLAC format and initializes the decoder if + /// successful. Uses default settings with no seeking support enabled. + /// + /// # Arguments + /// + /// * `data` - Input stream implementing `Read + Seek` + /// + /// # Returns + /// + /// - `Ok(FlacDecoder)` if the data contains valid FLAC format + /// - `Err(R)` if the data is not FLAC, returning the original stream + /// + /// # Examples + /// + /// ```ignore + /// use std::fs::File; + /// use rodio::decoder::flac::FlacDecoder; + /// + /// let file = File::open("audio.flac").unwrap(); + /// match FlacDecoder::new(file) { + /// Ok(decoder) => println!("FLAC decoder created"), + /// Err(file) => println!("Not a FLAC file"), + /// } + /// ``` + /// + /// # Performance + /// + /// This method performs format detection which requires reading the FLAC header. + /// The stream position is restored if detection fails, so the original stream + /// can be used for other format detection attempts. + #[allow(dead_code)] + pub fn new(data: R) -> Result, R> { + Self::new_with_settings(data, &Settings::default()) + } + + /// Attempts to decode the data as FLAC with custom settings. + /// + /// This method provides full control over decoder configuration including seeking behavior. + /// It performs format detection, parses FLAC metadata, and initializes internal buffers + /// based on the stream characteristics. + /// + /// # Arguments + /// + /// * `data` - Input stream implementing `Read + Seek` + /// * `settings` - Configuration settings from `DecoderBuilder` + /// + /// # Returns + /// + /// - `Ok(FlacDecoder)` if the data contains valid FLAC format + /// - `Err(R)` if the data is not FLAC, returning the original stream + /// + /// # Settings Usage + /// + /// - `is_seekable`: Enables backward seeking operations + /// - Other settings don't affect FLAC decoding + /// + /// # Examples + /// + /// ```ignore + /// use std::fs::File; + /// use rodio::decoder::{flac::FlacDecoder, Settings}; + /// + /// let file = File::open("audio.flac").unwrap(); + /// let mut settings = Settings::default(); + /// settings.is_seekable = true; + /// + /// let decoder = FlacDecoder::new_with_settings(file, &settings).unwrap(); + /// ``` + /// + /// # Panics + /// + /// Panics if the FLAC stream has invalid metadata (zero sample rate, zero channels, + /// or more than 65,535 channels). + /// + /// # Performance + /// + /// Buffer allocation is based on the stream's maximum block size to minimize + /// reallocations during playback. Larger maximum block sizes will use more memory + /// but provide better streaming performance. + pub fn new_with_settings(mut data: R, settings: &Settings) -> Result, R> { + if !is_flac(&mut data) { return Err(data); } - let reader = FlacReader::new(data).expect("should still be flac"); + let reader = FlacReader::new_ext(data, READER_OPTIONS).expect("should still be flac"); let spec = reader.streaminfo(); let sample_rate = spec.sample_rate; + let max_block_size = spec.max_block_size as usize * spec.channels as usize; // `samples` in FLAC means "inter-channel samples" aka frames // so we do not divide by `self.channels` here. - let total_duration = spec.samples.map(|s| { - // Calculate duration as (samples * 1_000_000) / sample_rate - // but do the division first to avoid overflow - let sample_rate = sample_rate as u64; - let secs = s / sample_rate; - let nanos = ((s % sample_rate) * 1_000_000_000) / sample_rate; - Duration::new(secs, nanos as u32) - }); - - Ok(FlacDecoder { - reader, - current_block: Vec::with_capacity( - spec.max_block_size as usize * spec.channels as usize, - ), + let total_samples = spec.samples; + let total_duration = + total_samples.map(|s| utils::samples_to_duration(s, sample_rate as u64)); + + Ok(Self { + reader: Some(reader), + current_block: Vec::with_capacity(max_block_size), current_block_channel_len: 1, current_block_off: 0, bits_per_sample: spec.bits_per_sample, @@ -69,12 +309,38 @@ where ) .expect("flac should never have zero channels"), total_duration, + total_samples, + samples_read: 0, + is_seekable: settings.is_seekable, }) } + /// Consumes the decoder and returns the underlying data stream. + /// + /// This can be useful for recovering the original data source after decoding is complete or + /// when the decoder needs to be replaced. The stream position will be at the current + /// playback position. + /// + /// # Examples + /// + /// ```ignore + /// use std::fs::File; + /// use rodio::decoder::flac::FlacDecoder; + /// + /// let file = File::open("audio.flac").unwrap(); + /// let decoder = FlacDecoder::new(file).unwrap(); + /// let recovered_file = decoder.into_inner(); + /// ``` + /// + /// # Panics + /// + /// Panics if called during a seeking operation when the reader is temporarily `None`. + /// This should never happen during normal usage. #[inline] pub fn into_inner(self) -> R { - self.reader.into_inner() + self.reader + .expect("reader should always be Some") + .into_inner() } } @@ -82,31 +348,196 @@ impl Source for FlacDecoder where R: Read + Seek, { + /// Returns the number of samples before parameters change. + /// + /// For FLAC, this always returns `None` because audio parameters (sample rate, channels, bit + /// depth) never change during the stream. This allows Rodio to optimize by not frequently + /// checking for parameter changes. + /// + /// # Implementation Note + /// + /// FLAC streams have fixed parameters throughout their duration, unlike some formats + /// that may have parameter changes at specific points. This enables optimizations + /// in the audio pipeline by avoiding frequent parameter validation. #[inline] fn current_span_len(&self) -> Option { None } + /// Returns the number of audio channels. + /// + /// FLAC supports 1 to 8 channels. Common configurations: + /// - 1 channel: Mono + /// - 2 channels: Stereo + /// - 6 channels: 5.1 surround + /// - 8 channels: 7.1 surround + /// + /// # Guarantees + /// + /// The returned value is constant for the lifetime of the decoder and matches + /// the channel count specified in the FLAC stream metadata. #[inline] fn channels(&self) -> ChannelCount { self.channels } + /// Returns the sample rate in Hz. + /// + /// Common rates that FLAC supports are: + /// - **44.1kHz**: CD quality (most common) + /// - **48kHz**: Professional audio standard + /// - **96kHz**: High-resolution audio + /// - **192kHz**: Ultra high-resolution audio + /// + /// # Guarantees + /// + /// The returned value is constant for the lifetime of the decoder and matches + /// the sample rate specified in the FLAC stream metadata. This value is + /// available immediately upon decoder creation. #[inline] fn sample_rate(&self) -> SampleRate { self.sample_rate } + /// Returns the total duration of the audio stream. + /// + /// FLAC metadata contains the total number of samples, allowing accurate duration calculation. + /// This is available immediately upon decoder creation without needing to scan the entire file. + /// + /// Returns `None` only for malformed FLAC files missing sample count metadata. + /// + /// # Accuracy + /// + /// The duration is calculated from exact sample counts, providing sample-accurate + /// timing information. This is more precise than duration estimates based on + /// bitrate calculations used by lossy formats. #[inline] fn total_duration(&self) -> Option { self.total_duration } + /// Returns the bit depth of the audio samples. + /// + /// FLAC is a lossless format that preserves the original bit depth: + /// - 16-bit: Standard CD quality + /// - 24-bit: Professional/high-resolution audio + /// - 32-bit: Professional/studio quality + /// - Other depths: 8, 12, and 20-bit are also supported + /// + /// Always returns `Some(depth)` for valid FLAC streams. + /// + /// # Implementation Note + /// + /// The bit depth information is preserved from the original FLAC stream and + /// used for proper sample scaling during conversion to Rodio's sample format. #[inline] - fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> { - Err(SeekError::NotSupported { - underlying_source: std::any::type_name::(), - }) + fn bits_per_sample(&self) -> Option { + Some(self.bits_per_sample) + } + + /// Attempts to seek to the specified position in the audio stream. + /// + /// # Seeking Behavior + /// + /// - **Forward seeking**: Fast linear sample skipping from current position + /// - **Backward seeking**: Requires stream reset and forward seek (needs `is_seekable`) + /// - **Beyond end**: Seeking past stream end is clamped to actual duration + /// - **Channel preservation**: Maintains correct channel order across seeks + /// + /// # Performance + /// + /// - Forward seeks are O(n) where n is samples to skip + /// - Backward seeks are O(target_position) due to stream reset + /// - Precise positioning without approximation + /// + /// # Arguments + /// + /// * `pos` - Target position as duration from stream start + /// + /// # Errors + /// + /// - `SeekError::ForwardOnly` - Backward seek attempted without `is_seekable` + /// - `SeekError::ClaxonDecoder` - Underlying FLAC decoder error + /// - `SeekError::IoError` - I/O error during stream reset + /// + /// # Examples + /// + /// ```no_run + /// use std::{fs::File, time::Duration}; + /// use rodio::{Decoder, Source}; + /// + /// let file = File::open("audio.flac").unwrap(); + /// let mut decoder = Decoder::builder() + /// .with_data(file) + /// .with_seekable(true) + /// .build() + /// .unwrap(); + /// + /// // Seek to 30 seconds into the track + /// decoder.try_seek(Duration::from_secs(30)).unwrap(); + /// ``` + /// + /// # Implementation Details + /// + /// The seeking implementation handles channel alignment to ensure that seeking + /// to a specific time position results in the correct channel being returned + /// for the first sample after the seek operation. + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + // Seeking should be "saturating", meaning: target positions beyond the end of the stream + // are clamped to the end. + let mut target = pos; + if let Some(total_duration) = self.total_duration() { + if target > total_duration { + target = total_duration; + } + } + + // Remember the current channel position before seeking (for channel order preservation) + let active_channel = self.current_block_off % self.channels.get() as usize; + + // Convert duration to sample number (interleaved samples for FLAC) + // FLAC samples are interleaved, so we need total samples including all channels + let target_sample = (target.as_secs_f64() + * self.sample_rate.get() as f64 + * self.channels.get() as f64) as u64; + + // FLAC is a structured format, so without seek index support in claxon we can only seek + // forwards or from the start. + let samples_to_skip = if target_sample < self.samples_read { + if !self.is_seekable { + return Err(SeekError::ForwardOnly); + } + + // Backwards seek: reset to start by recreating reader + let mut reader = self + .reader + .take() + .expect("reader should always be Some") + .into_inner(); + + reader.rewind().map_err(Arc::new)?; + + // Recreate FLAC reader and reset state + let new_reader = FlacReader::new_ext(reader, READER_OPTIONS)?; + + self.reader = Some(new_reader); + self.current_block.clear(); + self.current_block_off = 0; + self.samples_read = 0; + + // Skip to target position + target_sample + } else { + // Forward seek: skip from current position + target_sample - self.samples_read + }; + + // Consume samples to reach target position + for _ in 0..(samples_to_skip + active_channel as u64) { + let _ = self.next(); + } + + Ok(()) } } @@ -114,11 +545,45 @@ impl Iterator for FlacDecoder where R: Read + Seek, { + /// The type of items yielded by the iterator. + /// + /// Returns `Sample` (typically `f32`) values representing individual audio samples. + /// Samples are interleaved across channels in the order: channel 0, channel 1, etc. type Item = Sample; + /// Returns the next audio sample from the FLAC stream. + /// + /// This method implements efficient block-based decoding by maintaining an internal + /// buffer of the current FLAC block. It returns samples one at a time while + /// automatically decoding new blocks as needed. + /// + /// # Sample Format Conversion + /// + /// Raw FLAC samples are converted to Rodio's sample format based on bit depth: + /// - 8-bit: Direct conversion from `i8` + /// - 16-bit: Direct conversion from `i16` + /// - 24-bit: Conversion using `I24` type + /// - 32-bit: Direct conversion from `i32` + /// - Other (12, 20-bit): Bit-shifted to 32-bit then converted + /// + /// # Performance + /// + /// - **Hot path**: Returning samples from current block (very fast) + /// - **Cold path**: Decoding new blocks when buffer is exhausted (slower) + /// + /// # Returns + /// + /// - `Some(sample)` - Next audio sample + /// - `None` - End of stream reached or decoding error occurred + /// + /// # Channel Order + /// + /// Samples are returned in interleaved order: [L, R, L, R, ...] for stereo, + /// [FL, FR, C, LFE, BL, BR] for 5.1, etc. #[inline] fn next(&mut self) -> Option { loop { + // Hot path: return sample from current block if available if self.current_block_off < self.current_block.len() { // Read from current block. let real_offset = (self.current_block_off % self.channels.get() as usize) @@ -126,6 +591,7 @@ where + self.current_block_off / self.channels.get() as usize; let raw_val = self.current_block[real_offset]; self.current_block_off += 1; + self.samples_read += 1; let bits = self.bits_per_sample; let real_val = match bits { 8 => (raw_val as i8).to_sample(), @@ -145,27 +611,130 @@ where return Some(real_val); } - // Load the next block. + // Cold path: need to decode next block self.current_block_off = 0; let buffer = mem::take(&mut self.current_block); - match self.reader.blocks().read_next_or_eof(buffer) { + match self + .reader + .as_mut() + .expect("reader should always be Some") + .blocks() + .read_next_or_eof(buffer) + { Ok(Some(block)) => { self.current_block_channel_len = (block.len() / block.channels()) as usize; self.current_block = block.into_buffer(); } - _ => return None, + Ok(None) | Err(_) => { + // No more blocks or error, current_block becomes empty + // (buffer was consumed by read_next_or_eof) + return None; + } } } } + + /// Returns bounds on the remaining length of the iterator. + /// + /// Provides accurate size estimates based on FLAC metadata when available. + /// This information can be used by consumers for buffer pre-allocation + /// and progress indication. + /// + /// # Returns + /// + /// A tuple `(lower_bound, upper_bound)` where: + /// - `lower_bound`: Minimum number of samples guaranteed to be available + /// - `upper_bound`: Maximum number of samples that might be available (None if unknown) + /// + /// # Accuracy + /// + /// - **With metadata**: Exact remaining sample count (lower == upper) + /// - **Without metadata**: Conservative estimate based on current block + /// - **Stream exhausted**: (0, Some(0)) + /// + /// # Implementation + /// + /// The lower bound counts buffered samples that are immediately available. + /// The upper bound uses total stream metadata when available for precise counting. + fn size_hint(&self) -> (usize, Option) { + // Samples already decoded and buffered (guaranteed available) + let buffered_samples = self + .current_span_len() + .unwrap_or(0) + .saturating_sub(self.current_block_off); + + if let Some(total_samples) = self.total_samples { + let total_remaining = total_samples.saturating_sub(self.samples_read) as usize; + (buffered_samples, Some(total_remaining)) + } else if self.current_block.is_empty() { + // Stream exhausted (no more blocks available) + (0, Some(0)) + } else { + (buffered_samples, None) + } + } } -/// Returns true if the stream contains FLAC data, then tries to rewind it to where it was. -fn is_flac(mut data: R) -> bool +/// Probes input data to detect FLAC format. +/// +/// This function attempts to parse the FLAC magic bytes and stream info header to determine if the +/// data contains a valid FLAC stream. The stream position is restored regardless of the result. +/// +/// # Arguments +/// +/// * `data` - Mutable reference to the input stream to probe +/// +/// # Returns +/// +/// - `true` if the data appears to contain a valid FLAC stream +/// - `false` if the data is not FLAC or is corrupted +/// +/// # Implementation +/// +/// Uses the common `utils::probe_format` helper which: +/// 1. Saves the current stream position +/// 2. Attempts FLAC detection using `claxon::FlacReader` +/// 3. Restores the original stream position +/// 4. Returns the detection result +/// +/// # Performance +/// +/// This function only reads the minimum amount of data needed to identify +/// the FLAC format (magic bytes and basic header), making it efficient for +/// format detection in multi-format scenarios. +fn is_flac(data: &mut R) -> bool where R: Read + Seek, { - let stream_pos = data.stream_position().unwrap_or_default(); - let result = FlacReader::new(data.by_ref()).is_ok(); - let _ = data.seek(SeekFrom::Start(stream_pos)); - result + utils::probe_format(data, |reader| FlacReader::new(reader).is_ok()) +} + +/// Converts claxon decoder errors to rodio seek errors. +/// +/// This implementation provides error context preservation when FLAC decoding operations fail +/// during seeking. The original `claxon` error is wrapped in an `Arc` for thread safety and +/// converted to the appropriate Rodio error type. +/// +/// # Error Mapping +/// +/// All `claxon::Error` variants are mapped to `SeekError::ClaxonDecoder` with the +/// original error preserved for debugging and error analysis. +/// +/// # Thread Safety +/// +/// The error is wrapped in `Arc` to allow sharing across thread boundaries if needed, +/// following Rodio's error handling patterns. +impl From for SeekError { + /// Converts a claxon error into a Rodio seek error. + /// + /// # Arguments + /// + /// * `err` - The original claxon decoder error + /// + /// # Returns + /// + /// A `SeekError::ClaxonDecoder` containing the original error wrapped in an `Arc`. + fn from(err: claxon::Error) -> Self { + SeekError::ClaxonDecoder(Arc::new(err)) + } } diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index c68fbb2d..49647c78 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -65,18 +65,19 @@ use crate::{ pub mod builder; pub use builder::{DecoderBuilder, Settings}; -#[cfg(all(feature = "claxon", not(feature = "symphonia-flac")))] +mod utils; + +#[cfg(feature = "claxon")] mod flac; -#[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] +#[cfg(feature = "minimp3")] mod mp3; #[cfg(feature = "symphonia")] mod read_seek_source; #[cfg(feature = "symphonia")] -/// Symphonia decoders types -pub mod symphonia; -#[cfg(all(feature = "lewton", not(feature = "symphonia-vorbis")))] +mod symphonia; +#[cfg(feature = "lewton")] mod vorbis; -#[cfg(all(feature = "hound", not(feature = "symphonia-wav")))] +#[cfg(feature = "hound")] mod wav; /// Source of audio samples decoded from an input stream. @@ -89,6 +90,11 @@ pub struct Decoder(DecoderImpl); /// A `LoopedDecoder` will attempt to seek back to the start of the stream when it reaches /// the end. If seeking fails for any reason (like IO errors), iteration will stop. /// +/// For seekable sources with gapless playback enabled, this uses `try_seek(Duration::ZERO)` +/// which is fast. For non-seekable sources or when gapless is disabled, it recreates the +/// decoder but caches metadata from the first iteration to avoid expensive file scanning +/// on subsequent loops. +/// /// # Examples /// /// ```no_run @@ -98,46 +104,60 @@ pub struct Decoder(DecoderImpl); /// let file = File::open("audio.mp3").unwrap(); /// let looped_decoder = Decoder::new_looped(file).unwrap(); /// ``` +#[allow(dead_code)] pub struct LoopedDecoder { /// The underlying decoder implementation. inner: Option>, /// Configuration settings for the decoder. settings: Settings, + /// Cached metadata from the first successful decoder creation. + /// Used to avoid expensive file scanning on subsequent loops. + cached_duration: Option, } // Cannot really reduce the size of the VorbisDecoder. There are not any -// arrays just a lot of struct fields. +/// Internal enum representing different decoder implementations. +/// +/// This enum dispatches to the appropriate decoder based on detected format +/// and available features. Large enum variant size is acceptable here since +/// these are infrequently created and moved. #[allow(clippy::large_enum_variant)] enum DecoderImpl { - #[cfg(all(feature = "hound", not(feature = "symphonia-wav")))] + /// WAV decoder using hound library + #[cfg(feature = "hound")] Wav(wav::WavDecoder), - #[cfg(all(feature = "lewton", not(feature = "symphonia-vorbis")))] + /// Ogg Vorbis decoder using lewton library + #[cfg(feature = "lewton")] Vorbis(vorbis::VorbisDecoder), - #[cfg(all(feature = "claxon", not(feature = "symphonia-flac")))] + /// FLAC decoder using claxon library + #[cfg(feature = "claxon")] Flac(flac::FlacDecoder), - #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] + /// MP3 decoder using minimp3 library + #[cfg(feature = "minimp3")] Mp3(mp3::Mp3Decoder), + /// Multi-format decoder using symphonia library #[cfg(feature = "symphonia")] Symphonia(symphonia::SymphoniaDecoder, PhantomData), - // This variant is here just to satisfy the compiler when there are no decoders enabled. - // It is unreachable and should never be constructed. + /// Placeholder variant to satisfy compiler when no decoders are enabled. + /// This variant is unreachable and should never be constructed. #[allow(dead_code)] None(Unreachable, PhantomData), } +/// Placeholder type for the None variant that can never be instantiated. enum Unreachable {} impl DecoderImpl { #[inline] fn next(&mut self) -> Option { match self { - #[cfg(all(feature = "hound", not(feature = "symphonia-wav")))] + #[cfg(feature = "hound")] DecoderImpl::Wav(source) => source.next(), - #[cfg(all(feature = "lewton", not(feature = "symphonia-vorbis")))] + #[cfg(feature = "lewton")] DecoderImpl::Vorbis(source) => source.next(), - #[cfg(all(feature = "claxon", not(feature = "symphonia-flac")))] + #[cfg(feature = "claxon")] DecoderImpl::Flac(source) => source.next(), - #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] + #[cfg(feature = "minimp3")] DecoderImpl::Mp3(source) => source.next(), #[cfg(feature = "symphonia")] DecoderImpl::Symphonia(source, PhantomData) => source.next(), @@ -148,13 +168,13 @@ impl DecoderImpl { #[inline] fn size_hint(&self) -> (usize, Option) { match self { - #[cfg(all(feature = "hound", not(feature = "symphonia-wav")))] + #[cfg(feature = "hound")] DecoderImpl::Wav(source) => source.size_hint(), - #[cfg(all(feature = "lewton", not(feature = "symphonia-vorbis")))] + #[cfg(feature = "lewton")] DecoderImpl::Vorbis(source) => source.size_hint(), - #[cfg(all(feature = "claxon", not(feature = "symphonia-flac")))] + #[cfg(feature = "claxon")] DecoderImpl::Flac(source) => source.size_hint(), - #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] + #[cfg(feature = "minimp3")] DecoderImpl::Mp3(source) => source.size_hint(), #[cfg(feature = "symphonia")] DecoderImpl::Symphonia(source, PhantomData) => source.size_hint(), @@ -165,13 +185,13 @@ impl DecoderImpl { #[inline] fn current_span_len(&self) -> Option { match self { - #[cfg(all(feature = "hound", not(feature = "symphonia-wav")))] + #[cfg(feature = "hound")] DecoderImpl::Wav(source) => source.current_span_len(), - #[cfg(all(feature = "lewton", not(feature = "symphonia-vorbis")))] + #[cfg(feature = "lewton")] DecoderImpl::Vorbis(source) => source.current_span_len(), - #[cfg(all(feature = "claxon", not(feature = "symphonia-flac")))] + #[cfg(feature = "claxon")] DecoderImpl::Flac(source) => source.current_span_len(), - #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] + #[cfg(feature = "minimp3")] DecoderImpl::Mp3(source) => source.current_span_len(), #[cfg(feature = "symphonia")] DecoderImpl::Symphonia(source, PhantomData) => source.current_span_len(), @@ -182,13 +202,13 @@ impl DecoderImpl { #[inline] fn channels(&self) -> ChannelCount { match self { - #[cfg(all(feature = "hound", not(feature = "symphonia-wav")))] + #[cfg(feature = "hound")] DecoderImpl::Wav(source) => source.channels(), - #[cfg(all(feature = "lewton", not(feature = "symphonia-vorbis")))] + #[cfg(feature = "lewton")] DecoderImpl::Vorbis(source) => source.channels(), - #[cfg(all(feature = "claxon", not(feature = "symphonia-flac")))] + #[cfg(feature = "claxon")] DecoderImpl::Flac(source) => source.channels(), - #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] + #[cfg(feature = "minimp3")] DecoderImpl::Mp3(source) => source.channels(), #[cfg(feature = "symphonia")] DecoderImpl::Symphonia(source, PhantomData) => source.channels(), @@ -199,13 +219,13 @@ impl DecoderImpl { #[inline] fn sample_rate(&self) -> SampleRate { match self { - #[cfg(all(feature = "hound", not(feature = "symphonia-wav")))] + #[cfg(feature = "hound")] DecoderImpl::Wav(source) => source.sample_rate(), - #[cfg(all(feature = "lewton", not(feature = "symphonia-vorbis")))] + #[cfg(feature = "lewton")] DecoderImpl::Vorbis(source) => source.sample_rate(), - #[cfg(all(feature = "claxon", not(feature = "symphonia-flac")))] + #[cfg(feature = "claxon")] DecoderImpl::Flac(source) => source.sample_rate(), - #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] + #[cfg(feature = "minimp3")] DecoderImpl::Mp3(source) => source.sample_rate(), #[cfg(feature = "symphonia")] DecoderImpl::Symphonia(source, PhantomData) => source.sample_rate(), @@ -222,13 +242,13 @@ impl DecoderImpl { #[inline] fn total_duration(&self) -> Option { match self { - #[cfg(all(feature = "hound", not(feature = "symphonia-wav")))] + #[cfg(feature = "hound")] DecoderImpl::Wav(source) => source.total_duration(), - #[cfg(all(feature = "lewton", not(feature = "symphonia-vorbis")))] + #[cfg(feature = "lewton")] DecoderImpl::Vorbis(source) => source.total_duration(), - #[cfg(all(feature = "claxon", not(feature = "symphonia-flac")))] + #[cfg(feature = "claxon")] DecoderImpl::Flac(source) => source.total_duration(), - #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] + #[cfg(feature = "minimp3")] DecoderImpl::Mp3(source) => source.total_duration(), #[cfg(feature = "symphonia")] DecoderImpl::Symphonia(source, PhantomData) => source.total_duration(), @@ -236,16 +256,36 @@ impl DecoderImpl { } } + /// Returns the bits per sample of this audio source. + /// + /// For lossy formats this should always return `None`. + #[inline] + fn bits_per_sample(&self) -> Option { + match self { + #[cfg(feature = "hound")] + DecoderImpl::Wav(source) => source.bits_per_sample(), + #[cfg(feature = "lewton")] + DecoderImpl::Vorbis(source) => source.bits_per_sample(), + #[cfg(feature = "claxon")] + DecoderImpl::Flac(source) => source.bits_per_sample(), + #[cfg(feature = "minimp3")] + DecoderImpl::Mp3(source) => source.bits_per_sample(), + #[cfg(feature = "symphonia")] + DecoderImpl::Symphonia(source, PhantomData) => source.bits_per_sample(), + DecoderImpl::None(_, _) => unreachable!(), + } + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { match self { - #[cfg(all(feature = "hound", not(feature = "symphonia-wav")))] + #[cfg(feature = "hound")] DecoderImpl::Wav(source) => source.try_seek(pos), - #[cfg(all(feature = "lewton", not(feature = "symphonia-vorbis")))] + #[cfg(feature = "lewton")] DecoderImpl::Vorbis(source) => source.try_seek(pos), - #[cfg(all(feature = "claxon", not(feature = "symphonia-flac")))] + #[cfg(feature = "claxon")] DecoderImpl::Flac(source) => source.try_seek(pos), - #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] + #[cfg(feature = "minimp3")] DecoderImpl::Mp3(source) => source.try_seek(pos), #[cfg(feature = "symphonia")] DecoderImpl::Symphonia(source, PhantomData) => source.try_seek(pos), @@ -290,6 +330,7 @@ impl TryFrom for Decoder> { .with_data(BufReader::new(file)) .with_byte_len(len) .with_seekable(true) + .with_scan_duration(true) .build() } } @@ -377,7 +418,9 @@ impl Decoder { /// Builds a new decoder with default settings. /// - /// Attempts to automatically detect the format of the source of data. + /// Attempts to automatically detect the format of the source of data, but does not determine + /// byte length or enable seeking by default. If you are working with a `File`, then you will + /// probably want to use `Decoder::try_from(file)` instead. /// /// # Errors /// @@ -389,7 +432,10 @@ impl Decoder { /// Builds a new looped decoder with default settings. /// - /// Attempts to automatically detect the format of the source of data. + /// Attempts to automatically detect the format of the source of data, but does not determine + /// byte length or enable seeking by default. If you are working with a `File`, then you will + /// probably want to use `Decoder::try_from(file)` instead. + /// /// The decoder will restart from the beginning when it reaches the end. /// /// # Errors @@ -418,7 +464,10 @@ impl Decoder { /// let file = File::open("audio.wav").unwrap(); /// let decoder = Decoder::new_wav(file).unwrap(); /// ``` - #[cfg(any(feature = "hound", feature = "symphonia-wav"))] + #[cfg(any( + feature = "hound", + all(feature = "symphonia-pcm", feature = "symphonia-wav") + ))] pub fn new_wav(data: R) -> Result { DecoderBuilder::new() .with_data(data) @@ -470,7 +519,10 @@ impl Decoder { /// let file = File::open("audio.ogg").unwrap(); /// let decoder = Decoder::new_vorbis(file).unwrap(); /// ``` - #[cfg(any(feature = "lewton", feature = "symphonia-vorbis"))] + #[cfg(any( + feature = "lewton", + all(feature = "symphonia-ogg", feature = "symphonia-vorbis") + ))] pub fn new_vorbis(data: R) -> Result { DecoderBuilder::new() .with_data(data) @@ -522,7 +574,7 @@ impl Decoder { /// let file = File::open("audio.aac").unwrap(); /// let decoder = Decoder::new_aac(file).unwrap(); /// ``` - #[cfg(feature = "symphonia-aac")] + #[cfg(all(feature = "symphonia-aac", feature = "symphonia-isomp4"))] pub fn new_aac(data: R) -> Result { DecoderBuilder::new() .with_data(data) @@ -548,7 +600,7 @@ impl Decoder { /// let file = File::open("audio.m4a").unwrap(); /// let decoder = Decoder::new_mp4(file).unwrap(); /// ``` - #[cfg(feature = "symphonia-isomp4")] + #[cfg(all(feature = "symphonia-aac", feature = "symphonia-isomp4"))] pub fn new_mp4(data: R) -> Result { DecoderBuilder::new() .with_data(data) @@ -597,12 +649,83 @@ where self.0.total_duration() } + #[inline] + fn bits_per_sample(&self) -> Option { + self.0.bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { self.0.try_seek(pos) } } +impl LoopedDecoder +where + R: Read + Seek, +{ + /// Recreate decoder with cached metadata to avoid expensive file scanning. + fn recreate_decoder_with_cache( + &mut self, + decoder: DecoderImpl, + ) -> Option<(DecoderImpl, Option)> { + // Create settings with cached metadata for fast recreation. + // Note: total_duration is important even though LoopedDecoder::total_duration() returns + // None, because the individual decoder's total_duration() is used for seek saturation + // (clamping seeks beyond the end to the end position). + let mut fast_settings = self.settings.clone(); + fast_settings.total_duration = self.cached_duration; + + let (new_decoder, sample) = match decoder { + #[cfg(feature = "hound")] + DecoderImpl::Wav(source) => { + let mut reader = source.into_inner(); + reader.rewind().ok()?; + let mut source = wav::WavDecoder::new_with_settings(reader, &fast_settings).ok()?; + let sample = source.next(); + (DecoderImpl::Wav(source), sample) + } + #[cfg(feature = "lewton")] + DecoderImpl::Vorbis(source) => { + let mut reader = source.into_inner().into_inner().into_inner(); + reader.rewind().ok()?; + let mut source = + vorbis::VorbisDecoder::new_with_settings(reader, &fast_settings).ok()?; + let sample = source.next(); + (DecoderImpl::Vorbis(source), sample) + } + #[cfg(feature = "claxon")] + DecoderImpl::Flac(source) => { + let mut reader = source.into_inner(); + reader.rewind().ok()?; + let mut source = + flac::FlacDecoder::new_with_settings(reader, &fast_settings).ok()?; + let sample = source.next(); + (DecoderImpl::Flac(source), sample) + } + #[cfg(feature = "minimp3")] + DecoderImpl::Mp3(source) => { + let mut reader = source.into_inner(); + reader.rewind().ok()?; + let mut source = mp3::Mp3Decoder::new_with_settings(reader, &fast_settings).ok()?; + let sample = source.next(); + (DecoderImpl::Mp3(source), sample) + } + #[cfg(feature = "symphonia")] + DecoderImpl::Symphonia(source, PhantomData) => { + let mut reader = source.into_inner(); + reader.rewind().ok()?; + let mut source = + symphonia::SymphoniaDecoder::new_with_settings(reader, &fast_settings).ok()?; + let sample = source.next(); + (DecoderImpl::Symphonia(source, PhantomData), sample) + } + DecoderImpl::None(_, _) => return None, + }; + Some((new_decoder, sample)) + } +} + impl Iterator for LoopedDecoder where R: Read + Seek, @@ -611,63 +734,33 @@ where /// Returns the next sample in the audio stream. /// - /// When the end of the stream is reached, attempts to seek back to the start - /// and continue playing. If seeking fails, or if no decoder is available, - /// returns `None`. + /// When the end of the stream is reached, attempts to seek back to the start and continue + /// playing. For seekable sources with gapless playback, this uses fast seeking. For + /// non-seekable sources or when gapless is disabled, recreates the decoder using cached + /// metadata to avoid expensive file scanning. fn next(&mut self) -> Option { if let Some(inner) = &mut self.inner { if let Some(sample) = inner.next() { return Some(sample); } - // Take ownership of the decoder to reset it + // Cache duration from current decoder before resetting (first time only) + if self.cached_duration.is_none() { + self.cached_duration = inner.total_duration(); + } + + // Try seeking first for seekable sources - this is fast and gapless + // Only use fast seeking when gapless=true, otherwise recreate normally + if self.settings.gapless + && self.settings.is_seekable + && inner.try_seek(Duration::ZERO).is_ok() + { + return inner.next(); + } + + // Fall back to recreation with cached metadata to avoid expensive scanning let decoder = self.inner.take()?; - let (new_decoder, sample) = match decoder { - #[cfg(all(feature = "hound", not(feature = "symphonia-wav")))] - DecoderImpl::Wav(source) => { - let mut reader = source.into_inner(); - reader.seek(SeekFrom::Start(0)).ok()?; - let mut source = wav::WavDecoder::new(reader).ok()?; - let sample = source.next(); - (DecoderImpl::Wav(source), sample) - } - #[cfg(all(feature = "lewton", not(feature = "symphonia-vorbis")))] - DecoderImpl::Vorbis(source) => { - use lewton::inside_ogg::OggStreamReader; - let mut reader = source.into_inner().into_inner(); - reader.seek_bytes(SeekFrom::Start(0)).ok()?; - let mut source = vorbis::VorbisDecoder::from_stream_reader( - OggStreamReader::from_ogg_reader(reader).ok()?, - ); - let sample = source.next(); - (DecoderImpl::Vorbis(source), sample) - } - #[cfg(all(feature = "claxon", not(feature = "symphonia-flac")))] - DecoderImpl::Flac(source) => { - let mut reader = source.into_inner(); - reader.seek(SeekFrom::Start(0)).ok()?; - let mut source = flac::FlacDecoder::new(reader).ok()?; - let sample = source.next(); - (DecoderImpl::Flac(source), sample) - } - #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] - DecoderImpl::Mp3(source) => { - let mut reader = source.into_inner(); - reader.seek(SeekFrom::Start(0)).ok()?; - let mut source = mp3::Mp3Decoder::new(reader).ok()?; - let sample = source.next(); - (DecoderImpl::Mp3(source), sample) - } - #[cfg(feature = "symphonia")] - DecoderImpl::Symphonia(source, PhantomData) => { - let mut reader = source.into_inner(); - reader.seek(SeekFrom::Start(0)).ok()?; - let mut source = - symphonia::SymphoniaDecoder::new(reader, &self.settings).ok()?; - let sample = source.next(); - (DecoderImpl::Symphonia(source, PhantomData), sample) - } - }; + let (new_decoder, sample) = self.recreate_decoder_with_cache(decoder)?; self.inner = Some(new_decoder); sample } else { @@ -678,14 +771,14 @@ where /// Returns the size hint for this iterator. /// /// The lower bound is: - /// - The minimum number of samples remaining in the current iteration if there is an active decoder + /// - The minimum number of samples remaining in the current iteration if there is an active + /// decoder /// - 0 if there is no active decoder (inner is None) /// /// The upper bound is always `None` since the decoder loops indefinitely. - /// This differs from non-looped decoders which may provide a finite upper bound. /// - /// Note that even with an active decoder, reaching the end of the stream may result - /// in the decoder becoming inactive if seeking back to the start fails. + /// Note that even with an active decoder, reaching the end of the stream may result in the + /// decoder becoming inactive if seeking back to the start fails. #[inline] fn size_hint(&self) -> (usize, Option) { ( @@ -735,6 +828,12 @@ where None } + /// Returns the bits per sample of the underlying decoder, if available. + #[inline] + fn bits_per_sample(&self) -> Option { + self.inner.as_ref()?.bits_per_sample() + } + /// Attempts to seek to a specific position in the audio stream. /// /// # Errors diff --git a/src/decoder/mp3.rs b/src/decoder/mp3.rs index a84026db..f8cf60a0 100644 --- a/src/decoder/mp3.rs +++ b/src/decoder/mp3.rs @@ -1,54 +1,445 @@ -use std::io::{Read, Seek, SeekFrom}; -use std::num::NonZero; -use std::time::Duration; +//! MP3 audio decoder implementation. +//! +//! This module provides MP3 decoding capabilities using the `minimp3` library. MP3 is a +//! lossy audio compression format that achieves smaller file sizes by removing audio +//! information that is less audible to human hearing. +//! +//! # Features +//! +//! - **MPEG layers**: Supports MPEG-1/2 Layer III (MP3) +//! - **Bitrates**: Variable and constant bitrate encoding (32-320 kbps) +//! - **Sample rates**: 8kHz to 48kHz (MPEG-1: 32/44.1/48kHz, MPEG-2: 16/22.05/24kHz) +//! - **Channels**: Mono, stereo, joint stereo, and dual channel +//! - **Seeking**: Coarse seeking with optional duration scanning +//! - **Duration**: Calculated via file scanning or metadata when available +//! +//! # Limitations +//! +//! - No bit depth detection (lossy format with dynamic range compression) +//! - Seeking accuracy depends on bitrate variability (VBR vs CBR) +//! - Forward-only seeking without `is_seekable` setting +//! - Duration scanning requires full file analysis for accuracy +//! +//! # Configuration +//! +//! The decoder can be configured through `DecoderBuilder`: +//! - `with_seekable(true)` - Enable backward seeking +//! - `with_scan_duration(true)` - Enable duration scanning (requires `byte_len`) +//! - `with_total_duration(dur)` - Provide known duration to skip scanning +//! - `with_seek_mode(SeekMode::Fastest)` - Use fastest seeking method +//! +//! # Performance Notes +//! +//! - Duration scanning can be slow for large files +//! - Variable bitrate files may have less accurate seeking +//! - Frame-based decoding minimizes memory usage +//! +//! # Example +//! +//! ```ignore +//! use std::fs::File; +//! use rodio::Decoder; +//! +//! let file = File::open("audio.mp3").unwrap(); +//! let decoder = Decoder::builder() +//! .with_data(file) +//! .with_seekable(true) +//! .with_scan_duration(true) +//! .build() +//! .unwrap(); +//! +//! // MP3 format doesn't support bit depth detection +//! assert_eq!(decoder.bits_per_sample(), None); +//! ``` -use crate::common::{ChannelCount, Sample, SampleRate}; -use crate::source::SeekError; -use crate::Source; +use std::{ + io::{Read, Seek, SeekFrom}, + num::NonZero, + sync::Arc, + time::Duration, +}; use dasp_sample::Sample as _; -use minimp3::Decoder; -use minimp3::Frame; +use minimp3::{Decoder, Frame}; use minimp3_fixed as minimp3; +use super::{utils, Settings}; +use crate::{ + common::{ChannelCount, Sample, SampleRate}, + decoder::builder::SeekMode, + source::SeekError, + Source, +}; + +/// Decoder for the MP3 format using the `minimp3` library. +/// +/// Provides lossy audio decoding with frame-based processing and coarse seeking support. +/// Duration calculation may require file scanning for accurate results with variable bitrate files. +/// +/// # Frame-based Processing +/// +/// MP3 audio is organized into variable-size frames containing compressed audio data. +/// Each frame is decoded independently, containing 384 (Layer I) or 1152 (Layer II/III) +/// samples per channel. The decoder maintains the current frame and tracks position +/// within it for efficient sample-by-sample iteration. +/// +/// # Seeking Behavior +/// +/// MP3 seeking accuracy depends on the encoding type: +/// - **Constant Bitrate (CBR)**: Accurate byte-position-based seeking +/// - **Variable Bitrate (VBR)**: Approximate seeking with potential drift +/// - **Average Bitrate (ABR)**: Moderate accuracy depending on variation +/// +/// # Bitrate Adaptation +/// +/// The decoder tracks average bitrate over time to improve seeking accuracy, +/// especially for VBR files where initial estimates may be inaccurate. +/// +/// # Channel Changes +/// +/// MP3 frames can theoretically change channel configuration, though this is +/// rare in practice. The decoder handles such changes dynamically. +/// +/// # Thread Safety +/// +/// This decoder is not thread-safe. Create separate instances for concurrent access +/// or use appropriate synchronization primitives. +/// +/// # Generic Parameters +/// +/// * `R` - The underlying data source type, must implement `Read + Seek` pub struct Mp3Decoder where R: Read + Seek, { - // decoder: SeekDecoder, - decoder: Decoder, - // what minimp3 calls frames rodio calls spans - current_span: Frame, + /// The underlying minimp3 decoder, wrapped in Option for seeking operations. + /// + /// Temporarily set to `None` during stream reset operations for seeking. + /// Always `Some` during normal operation and iteration. + decoder: Option>, + + /// Byte position where audio data begins (after headers/metadata). + /// + /// Used as the base offset for seeking calculations. Accounts for ID3 tags, + /// XING headers, and other metadata that precedes the actual audio frames. + start_byte: u64, + + /// Current decoded MP3 frame (what minimp3 calls frames, rodio calls spans). + /// + /// Contains the raw PCM samples from the current frame. `None` indicates + /// either stream exhaustion or that a new frame needs to be decoded. + current_span: Option, + + /// Current position within the current frame. + /// + /// Tracks the next sample index to return from the current frame's data. + /// When this reaches the frame's sample count, a new frame must be decoded. current_span_offset: usize, + + /// Number of audio channels. + /// + /// Can theoretically change between frames, though this is rare in practice. + /// Updated dynamically when frame channel count changes. + channels: ChannelCount, + + /// Sample rate in Hz. + /// + /// Fixed for the entire MP3 stream. Common rates include 44.1kHz (CD quality), + /// 48kHz (professional), and various rates for different MPEG versions. + sample_rate: SampleRate, + + /// Number of samples read so far (for seeking calculations). + /// + /// Tracks the current playback position in total samples (across all channels). + /// Used to determine if seeking requires stream reset or can skip forward. + samples_read: u64, + + /// Total number of audio samples (estimated from duration). + /// + /// Calculated from total duration when available. For VBR files without + /// metadata, this may be an estimate based on average bitrate calculations. + total_samples: Option, + + /// Total duration calculated from file analysis or metadata. + /// + /// Can be provided explicitly, calculated via duration scanning, or estimated + /// from file size and average bitrate. Most accurate when obtained through + /// full file scanning. + total_duration: Option, + + /// Average bitrate in bytes per second (estimated). + /// + /// Updated dynamically during playback to improve seeking accuracy. + /// Initial value comes from first frame or duration/size calculation. + average_bitrate: u32, + + /// MPEG layer (typically 3 for MP3). + /// + /// Determines frame structure and sample count per frame: + /// - Layer I: 384 samples per frame + /// - Layer II/III: 1152 samples per frame + mpeg_layer: usize, + + /// Seeking precision mode. + /// + /// Controls the trade-off between seeking speed and accuracy: + /// - `Fastest`: Byte-position-based seeking (fastest but potentially inaccurate for VBR) + /// - `Nearest`: Sample-accurate seeking (slower but always accurate) + seek_mode: SeekMode, + + /// Whether random access seeking is enabled. + /// + /// When `true`, enables backward seeking by allowing stream reset operations. + /// When `false`, only forward seeking (sample skipping) is allowed. + is_seekable: bool, } impl Mp3Decoder where R: Read + Seek, { - pub fn new(mut data: R) -> Result { - if !is_mp3(data.by_ref()) { + /// Attempts to decode the data as MP3 with default settings. + /// + /// This method probes the input data to detect MP3 format and initializes the decoder if + /// successful. Uses default settings with no seeking support or duration scanning enabled. + /// + /// # Arguments + /// + /// * `data` - Input stream implementing `Read + Seek` + /// + /// # Returns + /// + /// - `Ok(Mp3Decoder)` if the data contains valid MP3 format + /// - `Err(R)` if the data is not MP3, returning the original stream + /// + /// # Examples + /// + /// ```ignore + /// use std::fs::File; + /// use rodio::decoder::mp3::Mp3Decoder; + /// + /// let file = File::open("audio.mp3").unwrap(); + /// match Mp3Decoder::new(file) { + /// Ok(decoder) => println!("MP3 decoder created"), + /// Err(file) => println!("Not an MP3 file"), + /// } + /// ``` + /// + /// # Performance + /// + /// This method performs format detection which requires decoding the first MP3 frame. + /// The stream position is restored if detection fails, so the original stream + /// can be used for other format detection attempts. + #[allow(dead_code)] + pub fn new(data: R) -> Result { + Self::new_with_settings(data, &Settings::default()) + } + + /// Attempts to decode the data as MP3 with custom settings. + /// + /// This method provides full control over decoder configuration including seeking behavior, + /// duration calculation, and performance optimizations. It performs format detection, + /// analyzes the first frame for stream characteristics, and optionally scans the entire + /// file for accurate duration information. + /// + /// # Arguments + /// + /// * `data` - Input stream implementing `Read + Seek` + /// * `settings` - Configuration settings from `DecoderBuilder` + /// + /// # Returns + /// + /// - `Ok(Mp3Decoder)` if the data contains valid MP3 format + /// - `Err(R)` if the data is not MP3, returning the original stream + /// + /// # Settings Usage + /// + /// - `is_seekable`: Enables backward seeking operations + /// - `scan_duration`: Enables full file duration analysis (requires `byte_len`) + /// - `total_duration`: Provides known duration to skip scanning + /// - `seek_mode`: Controls seeking accuracy vs. speed trade-off + /// - `byte_len`: Total file size used for bitrate and duration calculations + /// + /// # Examples + /// + /// ```ignore + /// use std::fs::File; + /// use std::time::Duration; + /// use rodio::decoder::{mp3::Mp3Decoder, Settings, builder::SeekMode}; + /// + /// let file = File::open("audio.mp3").unwrap(); + /// let mut settings = Settings::default(); + /// settings.is_seekable = true; + /// settings.scan_duration = true; + /// settings.seek_mode = SeekMode::Fastest; + /// + /// let decoder = Mp3Decoder::new_with_settings(file, &settings).unwrap(); + /// ``` + /// + /// # Performance + /// + /// - Duration scanning can significantly slow initialization for large files + /// - Bitrate calculation accuracy improves with `byte_len` availability + /// - First frame analysis provides immediate stream characteristics + /// + /// # Panics + /// + /// Panics if the MP3 stream has invalid characteristics (zero channels or zero sample rate). + /// This should never happen with valid MP3 data that passes format detection. + pub fn new_with_settings(mut data: R, settings: &Settings) -> Result { + if !is_mp3(&mut data) { return Err(data); } - // let mut decoder = SeekDecoder::new(data) + + // Calculate total duration using the new settings approach + let total_duration = if let Some(duration) = settings.total_duration { + // Use provided duration (highest priority) + Some(duration) + } else if settings.scan_duration && settings.is_seekable && settings.byte_len.is_some() { + // All prerequisites met - try scanning + get_mp3_duration(&mut data) + } else { + // Either scanning disabled or prerequisites not met + None + }; + let mut decoder = Decoder::new(data); - // parameters are correct and minimp3 is used correctly - // thus if we crash here one of these invariants is broken: - // .expect("should be able to allocate memory, perform IO"); - // let current_span = decoder.decode_frame() + let current_span = decoder.next_frame().expect("should still be mp3"); + let channels = current_span.channels; + let sample_rate = current_span.sample_rate; + let mpeg_layer = current_span.layer; + + // Calculate total samples if we have duration + let total_samples = total_duration + .map(|dur| (dur.as_secs_f64() * sample_rate as f64 * channels as f64).ceil() as u64); + + // Calculate the start of audio data in bytes (after MP3 headers). + // We're currently positioned after reading the first frame, so we can + // approximate the start of audio data by subtracting the frame size in bytes. + let frame_samples = current_span.data.len(); + let frame_duration_secs = frame_samples as f64 / (sample_rate as f64 * channels as f64); + let initial_bitrate_from_frame = current_span.bitrate as u32 * 1000 / 8; + let frame_size_bytes = (frame_duration_secs * initial_bitrate_from_frame as f64) as u64; + let start_byte = decoder + .reader_mut() + .stream_position() + .map_or(0, |pos| pos.saturating_sub(frame_size_bytes)); - Ok(Mp3Decoder { - decoder, - current_span, + // Calculate average bitrate using byte_len when available + let average_bitrate = + if let (Some(duration), Some(byte_len)) = (total_duration, settings.byte_len) { + let total_duration_secs = duration.as_secs_f64(); + if total_duration_secs > 0.0 { + let calculated = ((byte_len - start_byte) as f64 / total_duration_secs) as u32; + // Clamp average bitrate to reasonable MP3 ranges + if mpeg_layer == 3 { + // 32 to 320 kbps for MPEG-1 Layer III + calculated.clamp(4_000, 40_000) + } else { + // 32 to 384 kbps for MPEG-1 Layer I or II + calculated.clamp(4_000, 48_000) + } + } else { + initial_bitrate_from_frame + } + } else { + // No byte_len available, will use simple averaging during decode + initial_bitrate_from_frame + }; + + Ok(Self { + decoder: Some(decoder), + start_byte, + current_span: Some(current_span), current_span_offset: 0, + channels: NonZero::new(channels as _).expect("mp3's have at least one channel"), + sample_rate: NonZero::new(sample_rate as _).expect("mp3's have a non zero sample rate"), + samples_read: 0, + total_samples, + total_duration, + average_bitrate, + mpeg_layer, + seek_mode: settings.seek_mode, + is_seekable: settings.is_seekable, }) } + /// Consumes the decoder and returns the underlying data stream. + /// + /// This can be useful for recovering the original data source after decoding is complete or + /// when the decoder needs to be replaced. The stream position will be at the current + /// playback position. + /// + /// # Examples + /// + /// ```ignore + /// use std::fs::File; + /// use rodio::decoder::mp3::Mp3Decoder; + /// + /// let file = File::open("audio.mp3").unwrap(); + /// let decoder = Mp3Decoder::new(file).unwrap(); + /// let recovered_file = decoder.into_inner(); + /// ``` + /// + /// # Panics + /// + /// Panics if called during a seeking operation when the decoder is temporarily `None`. + /// This should never happen during normal usage. #[inline] pub fn into_inner(self) -> R { - self.decoder.into_inner() + self.decoder + .expect("decoder should always be Some") + .into_inner() + } + + /// Calculates an approximate byte offset for seeking to a target sample position. + /// + /// This method estimates the byte position in the stream that corresponds to the + /// target sample count using average bitrate and MPEG layer information. + /// The accuracy depends on bitrate consistency throughout the file. + /// + /// # Arguments + /// + /// * `target_samples` - The target sample position (interleaved across all channels) + /// + /// # Returns + /// + /// Estimated byte offset from the start of the file + /// + /// # Accuracy + /// + /// - **CBR files**: High accuracy due to consistent frame sizes + /// - **VBR files**: Moderate accuracy, may require fine-tuning after seeking + /// - **ABR files**: Accuracy depends on actual vs. average bitrate variance + /// + /// # Implementation + /// + /// The calculation is based on: + /// 1. Samples per frame (determined by MPEG layer) + /// 2. Average frame size (calculated from bitrate and sample rate) + /// 3. Number of frames to skip to reach target sample + /// + /// This provides a good starting point for byte-based seeking, though sample-accurate + /// positioning may require additional fine-tuning after the seek operation. + fn approx_byte_offset(&self, target_samples: u64) -> u64 { + let samples_per_frame = if self.mpeg_layer == 1 { + // MPEG-1 Layer I + 384 + } else { + // MPEG-1 Layer II or III + 1152 + }; + let samples_per_frame_total = samples_per_frame * self.channels().get() as u64; + + let frames_to_skip = target_samples / samples_per_frame_total; + + // average frame size in bytes + let avg_frame_size = (self.average_bitrate as f64 * samples_per_frame as f64 + / self.sample_rate().get() as f64) as u64; + + self.start_byte + frames_to_skip * avg_frame_size } } @@ -56,38 +447,230 @@ impl Source for Mp3Decoder where R: Read + Seek, { + /// Returns the number of samples before parameters change. + /// + /// For MP3, this returns `Some(frame_size)` when a frame is available, representing + /// the number of samples in the current frame before needing to decode the next one. + /// Returns `Some(0)` when the stream is exhausted. + /// + /// # Channel Changes + /// + /// While MP3 frames can theoretically change channel configuration, this is + /// extremely rare in practice. Most MP3 files maintain consistent channel + /// configuration throughout. + /// + /// # Frame Sizes + /// + /// Frame sizes depend on the MPEG layer: + /// - Layer I: 384 samples per channel + /// - Layer II/III: 1152 samples per channel + /// + /// Total samples per frame = samples_per_channel × channel_count #[inline] fn current_span_len(&self) -> Option { - Some(self.current_span.data.len()) + // Channel mode can change between MP3 frames. Return Some(0) when exhausted. + self.current_span + .as_ref() + .map(|span| span.data.len()) + .or(Some(0)) } + /// Returns the number of audio channels. + /// + /// MP3 supports various channel configurations: + /// - 1 channel: Mono + /// - 2 channels: Stereo, Joint Stereo, or Dual Channel + /// + /// # Dynamic Changes + /// + /// While the MP3 specification allows channel changes between frames, + /// this is rarely used in practice. When it does occur, the decoder + /// updates this value automatically. + /// + /// # Guarantees + /// + /// The returned value reflects the current frame's channel configuration + /// and may change during playback if the MP3 file uses variable channel modes. #[inline] fn channels(&self) -> ChannelCount { - NonZero::new(self.current_span.channels as _).expect("mp3's have at least one channel") + self.channels } + /// Returns the sample rate in Hz. + /// + /// MP3 supports specific sample rates based on MPEG version: + /// - **MPEG-1**: 32kHz, 44.1kHz, 48kHz + /// - **MPEG-2**: 16kHz, 22.05kHz, 24kHz + /// - **MPEG-2.5**: 8kHz, 11.025kHz, 12kHz + /// + /// # Guarantees + /// + /// The sample rate is fixed for the entire MP3 stream and cannot change + /// between frames, unlike the channel configuration. #[inline] fn sample_rate(&self) -> SampleRate { - NonZero::new(self.current_span.sample_rate as _).expect("mp3's have a non zero sample rate") + self.sample_rate } + /// Returns the total duration of the audio stream. + /// + /// Duration accuracy depends on how it was calculated: + /// - **Provided explicitly**: Most accurate (when available from metadata) + /// - **File scanning**: Very accurate but slow during initialization + /// - **Bitrate estimation**: Approximate, especially for VBR files + /// - **Not available**: Returns `None` when duration cannot be determined + /// + /// # Availability + /// + /// Duration is available when: + /// 1. Explicitly provided via `total_duration` setting + /// 2. Calculated via duration scanning (when enabled and prerequisites met) + /// 3. Estimated from file size and average bitrate (when `byte_len` available) + /// + /// Returns `None` when insufficient information is available for estimation. #[inline] fn total_duration(&self) -> Option { + self.total_duration + } + + /// Returns the bit depth of the audio samples. + /// + /// MP3 is a lossy compression format that doesn't preserve the original bit depth. + /// The decoded output is provided as floating-point samples regardless of the + /// original source material's bit depth. + /// + /// # Lossy Compression + /// + /// Unlike lossless formats like FLAC, MP3 uses psychoacoustic modeling to + /// remove audio information deemed less perceptible, making bit depth + /// information irrelevant for the decoded output. + /// + /// # Always Returns None + /// + /// This method always returns `None` for MP3 streams as bit depth is not + /// a meaningful concept for lossy compressed audio formats. + #[inline] + fn bits_per_sample(&self) -> Option { None } - fn try_seek(&mut self, _pos: Duration) -> Result<(), SeekError> { - // TODO waiting for PR in minimp3_fixed or minimp3 + /// Attempts to seek to the specified position in the audio stream. + /// + /// MP3 seeking behavior varies based on the configured seek mode and bitrate type. + /// The implementation balances speed and accuracy based on user preferences. + /// + /// # Seeking Modes + /// + /// - **`SeekMode::Fastest`**: Uses byte-position estimation for quick seeks + /// - Fast for CBR files with consistent frame sizes + /// - May be inaccurate for VBR files requiring fine-tuning + /// - **`SeekMode::Nearest`**: Guarantees sample-accurate positioning + /// - Slower due to linear sample consumption + /// - Always accurate regardless of bitrate variability + /// + /// # Performance Characteristics + /// + /// - **Forward seeks**: O(1) for byte-seeking, O(n) for sample-accurate + /// - **Backward seeks**: Requires stream reset, then forward positioning + /// - **CBR files**: Fast and accurate byte-based seeking + /// - **VBR files**: May require sample-accurate mode for precision + /// + /// # Arguments + /// + /// * `pos` - Target position as duration from stream start + /// + /// # Errors + /// + /// - `SeekError::ForwardOnly` - Backward seek attempted without `is_seekable` + /// - `SeekError::IoError` - I/O error during stream reset or positioning + /// + /// # Examples + /// + /// ```no_run + /// use std::{fs::File, time::Duration}; + /// use rodio::{Decoder, Source, decoder::builder::SeekMode}; + /// + /// let file = File::open("audio.mp3").unwrap(); + /// let mut decoder = Decoder::builder() + /// .with_data(file) + /// .with_seekable(true) + /// .with_seek_mode(SeekMode::Fastest) + /// .build() + /// .unwrap(); + /// + /// // Quick seek to 30 seconds (may be approximate for VBR) + /// decoder.try_seek(Duration::from_secs(30)).unwrap(); + /// ``` + /// + /// # Implementation Details + /// + /// The seeking implementation preserves channel alignment to ensure that seeking + /// to a specific time position results in the correct channel being returned + /// for the first sample after the seek operation. + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + // Seeking should be "saturating", meaning: target positions beyond the end of the stream + // are clamped to the end. + let mut target = pos; + if let Some(total_duration) = self.total_duration() { + if target > total_duration { + target = total_duration; + } + } - // let pos = (pos.as_secs_f32() * self.sample_rate().get() as f32) as u64; - // // do not trigger a sample_rate, channels and frame/span len update - // // as the seek only takes effect after the current frame/span is done - // self.decoder.seek_samples(pos)?; - // Ok(()) + // Remember the current channel position before seeking (for channel order preservation) + let active_channel = self.current_span_offset % self.channels().get() as usize; - Err(SeekError::NotSupported { - underlying_source: std::any::type_name::(), - }) + // Convert duration to sample number (interleaved samples) + let target_sample = (target.as_secs_f64() + * self.sample_rate().get() as f64 + * self.channels().get() as f64) as u64; + + if !self.is_seekable && target_sample < self.samples_read { + return Err(SeekError::ForwardOnly); + } + + let samples_to_skip = if target_sample > self.samples_read + && (self.seek_mode == SeekMode::Nearest || !self.is_seekable) + { + // Linearly consume samples to reach forward targets + target_sample - self.samples_read + } else { + let mut reader = self + .decoder + .take() + .expect("decoder should always be Some") + .into_inner(); + + let mut samples_to_skip = 0; + if self.seek_mode == SeekMode::Nearest { + // Rewind to start and consume samples to reach target + reader.rewind().map_err(Arc::new)?; + samples_to_skip = target_sample; + } else { + // Seek to approximate byte position + let approximate_byte_pos = self.approx_byte_offset(target_sample); + reader + .seek(SeekFrom::Start(approximate_byte_pos)) + .map_err(Arc::new)?; + + // Clear buffer - let next() handle loading new packets + self.current_span = None; + self.samples_read = target_sample; + } + + // Recreate MP3 decoder - minimp3 will handle frame synchronization + let new_decoder = Decoder::new(reader); + self.decoder = Some(new_decoder); + + samples_to_skip + }; + + // Consume samples to reach correct channel position + for _ in 0..(samples_to_skip + active_channel as u64) { + let _ = self.next(); + } + + Ok(()) } } @@ -95,35 +678,240 @@ impl Iterator for Mp3Decoder where R: Read + Seek, { + /// The type of items yielded by the iterator. + /// + /// Returns `Sample` (typically `f32`) values representing individual audio samples. + /// Samples are interleaved across channels in the order: channel 0, channel 1, etc. type Item = Sample; + /// Returns the next audio sample from the MP3 stream. + /// + /// This method implements efficient frame-based decoding by maintaining the current + /// decoded MP3 frame and returning samples one at a time. It automatically decodes + /// new frames as needed and adapts to changing stream characteristics. + /// + /// # Sample Format Conversion + /// + /// MP3 frames are decoded to PCM samples which are then converted to Rodio's + /// sample format (typically `f32`). The conversion preserves the dynamic range + /// and quality of the decoded audio. + /// + /// # Performance + /// + /// - **Hot path**: Returning samples from current frame (very fast) + /// - **Cold path**: Decoding new frames when buffer is exhausted (slower) + /// + /// # Adaptive Behavior + /// + /// The decoder adapts to changes in the MP3 stream: + /// - **Bitrate tracking**: Updates average bitrate for improved seeking accuracy + /// - **Channel changes**: Handles dynamic channel configuration changes + /// - **Frame synchronization**: Automatically recovers from stream errors + /// + /// # Returns + /// + /// - `Some(sample)` - Next audio sample from the stream + /// - `None` - End of stream reached or unrecoverable decoding error + /// + /// # Channel Order + /// + /// Samples are returned in interleaved order: + /// - **Mono**: [M, M, M, ...] + /// - **Stereo**: [L, R, L, R, ...] + /// - **Dual Channel**: [Ch1, Ch2, Ch1, Ch2, ...] fn next(&mut self) -> Option { - let current_span_len = self.current_span_len()?; - if self.current_span_offset == current_span_len { - if let Ok(span) = self.decoder.next_frame() { - // if let Ok(span) = self.decoder.decode_frame() { - self.current_span = span; - self.current_span_offset = 0; - } else { - return None; + // Hot path: return sample from current frame if available + if let Some(current_span) = &self.current_span { + if self.current_span_offset < current_span.data.len() { + let v = current_span.data[self.current_span_offset]; + self.current_span_offset += 1; + self.samples_read += 1; + return Some(v.to_sample()); } } - let v = self.current_span.data[self.current_span_offset]; - self.current_span_offset += 1; + // Cold path: need to decode next frame + if let Ok(span) = self + .decoder + .as_mut() + .expect("decoder should always be Some") + .next_frame() + { + // Update running average bitrate with running average (when byte_len wasn't available + // during creation) + let frame_bitrate_bps = span.bitrate as u32 * 1000 / 8; // Convert kbps to bytes/sec + self.average_bitrate = ((self.average_bitrate as u64 * self.samples_read + + frame_bitrate_bps as u64) + / (self.samples_read + 1)) as u32; - Some(v.to_sample()) + // Update channels if they changed (can vary between MP3 frames) + self.channels = + NonZero::new(span.channels as _).expect("mp3's have at least one channel"); + // Sample rate is fixed per MP3 stream, so no need to update + self.current_span = Some(span); + self.current_span_offset = 0; + + // Return first sample from the new frame + if let Some(current_span) = &self.current_span { + if !current_span.data.is_empty() { + let v = current_span.data[0]; + self.current_span_offset = 1; + self.samples_read += 1; + return Some(v.to_sample()); + } + } + } + + // Stream exhausted or empty frame - set current_span to None + self.current_span = None; + None } + + /// Returns bounds on the remaining length of the iterator. + /// + /// Provides size estimates based on MP3 metadata and current playback position. + /// The accuracy depends on the availability and reliability of duration information. + /// + /// # Returns + /// + /// A tuple `(lower_bound, upper_bound)` where: + /// - `lower_bound`: Minimum number of samples guaranteed to be available + /// - `upper_bound`: Maximum number of samples that might be available (None if unknown) + /// + /// # Accuracy Levels + /// + /// - **High accuracy**: When total samples calculated from scanned duration + /// - **Moderate accuracy**: When estimated from file size and average bitrate + /// - **Conservative estimate**: When only current frame information available + /// - **Stream exhausted**: (0, Some(0)) when no more data + /// + /// # Implementation + /// + /// The lower bound represents samples currently buffered in the decoded frame. + /// The upper bound uses total sample estimates when available, providing useful + /// information for progress indication and buffer allocation. + /// + /// # Use Cases + /// + /// - **Progress indication**: Upper bound enables percentage calculation + /// - **Buffer allocation**: Lower bound ensures minimum available samples + /// - **End detection**: (0, Some(0)) indicates stream completion + fn size_hint(&self) -> (usize, Option) { + // Samples already decoded and buffered (guaranteed available) + let buffered_samples = self + .current_span + .as_ref() + .map(|span| span.data.len().saturating_sub(self.current_span_offset)) + .unwrap_or(0); + + if let Some(total_samples) = self.total_samples { + let total_remaining = total_samples.saturating_sub(self.samples_read) as usize; + (buffered_samples, Some(total_remaining)) + } else if self.current_span.is_none() { + // Stream exhausted + (0, Some(0)) + } else { + (buffered_samples, None) + } + } +} + +/// Attempts to calculate MP3 duration using metadata headers or file scanning. +/// +/// This function uses the `mp3-duration` crate to calculate duration using the most +/// efficient method available. It first searches for duration metadata in headers +/// like XING, VBRI, or INFO, and only falls back to frame-by-frame scanning if +/// no metadata is found. +/// +/// # Arguments +/// +/// * `data` - Mutable reference to the input stream to analyze +/// +/// # Returns +/// +/// - `Some(duration)` if the file was successfully analyzed +/// - `None` if scanning failed or the file is invalid +/// +/// # Performance +/// +/// Performance varies significantly based on available metadata: +/// - **With headers (XING/VBRI/INFO)**: Very fast, O(1) header lookup +/// - **Without headers**: Slower, O(n) frame-by-frame scanning proportional to file size +/// - **Large VBR files without headers**: Can take several seconds to analyze +/// +/// # Implementation +/// +/// The function: +/// 1. Saves the current stream position +/// 2. Rewinds to the beginning for analysis +/// 3. Uses `mp3-duration` crate which: +/// - First searches for XING, VBRI, or INFO headers containing duration +/// - Falls back to frame-by-frame scanning if no headers found +/// 4. Restores the original stream position +/// 5. Returns the calculated duration +/// +/// # Accuracy +/// +/// - **With metadata headers**: Exact duration from encoder-provided information +/// - **Frame scanning**: Sample-accurate duration from analyzing every frame +/// +/// Both methods provide reliable duration information, with headers being faster. +fn get_mp3_duration(data: &mut R) -> Option { + // Save current position + let original_pos = data.stream_position().ok()?; + + // Seek to start + data.rewind().ok()?; + + // Try to get duration + let duration = mp3_duration::from_read(data).ok(); + + // Restore original position + let _ = data.seek(SeekFrom::Start(original_pos)); + + duration } -/// Returns true if the stream contains mp3 data, then resets it to where it was. -fn is_mp3(mut data: R) -> bool +/// Probes input data to detect MP3 format. +/// +/// This function attempts to decode the first MP3 frame to determine if the +/// data contains a valid MP3 stream. The stream position is restored regardless +/// of the result, making it safe to use for format detection. +/// +/// # Arguments +/// +/// * `data` - Mutable reference to the input stream to probe +/// +/// # Returns +/// +/// - `true` if the data appears to contain a valid MP3 stream +/// - `false` if the data is not MP3 or is corrupted +/// +/// # Implementation +/// +/// Uses the common `utils::probe_format` helper which: +/// 1. Saves the current stream position +/// 2. Attempts MP3 detection using `minimp3::Decoder` +/// 3. Restores the original stream position +/// 4. Returns the detection result +/// +/// # Performance +/// +/// This function only reads the minimum amount of data needed to identify +/// and decode the first MP3 frame, making it efficient for format detection +/// in multi-format scenarios. +/// +/// # Robustness +/// +/// The detection uses actual frame decoding rather than just header checking, +/// providing more reliable format identification at the cost of slightly +/// higher computational overhead. +fn is_mp3(data: &mut R) -> bool where R: Read + Seek, { - let stream_pos = data.stream_position().unwrap_or_default(); - let mut decoder = Decoder::new(data.by_ref()); - let result = decoder.next_frame().is_ok(); - let _ = data.seek(SeekFrom::Start(stream_pos)); - result + utils::probe_format(data, |reader| { + let mut decoder = Decoder::new(reader); + decoder.next_frame().is_ok() + }) } diff --git a/src/decoder/read_seek_source.rs b/src/decoder/read_seek_source.rs index d3a11252..74bbdee8 100644 --- a/src/decoder/read_seek_source.rs +++ b/src/decoder/read_seek_source.rs @@ -1,3 +1,31 @@ +//! Read + Seek adapter for Symphonia MediaSource integration. +//! +//! This module provides a bridge between standard Rust I/O types and Symphonia's +//! MediaSource trait, enabling seamless integration of file handles, cursors, and +//! other I/O sources with Symphonia's audio decoding framework. +//! +//! # Purpose +//! +//! Symphonia requires audio sources to implement its `MediaSource` trait, which +//! provides metadata about stream characteristics like seekability and byte length. +//! This adapter wraps standard Rust I/O types to provide this interface. +//! +//! # Architecture +//! +//! The adapter acts as a thin wrapper that: +//! - Delegates I/O operations to the wrapped type +//! - Provides stream metadata from decoder settings +//! - Maintains compatibility with Symphonia's requirements +//! - Preserves performance characteristics of the underlying source +//! +//! # Performance +//! +//! The wrapper has minimal overhead: +//! - Zero-cost delegation for read/seek operations +//! - Inline functions for optimal performance +//! - No additional buffering or copying +//! - Metadata cached from initial configuration + use std::io::{Read, Result, Seek, SeekFrom}; use symphonia::core::io::MediaSource; @@ -6,24 +34,93 @@ use super::Settings; /// A wrapper around a `Read + Seek` type that implements Symphonia's `MediaSource` trait. /// -/// This type allows standard Rust I/O types to be used with Symphonia's media framework -/// by implementing the required `MediaSource` trait. +/// This adapter enables standard Rust I/O types to be used with Symphonia's media framework +/// by bridging the gap between Rust's I/O traits and Symphonia's requirements. It provides +/// stream metadata while delegating actual I/O operations to the wrapped type. +/// +/// # Use Cases +/// +/// - **File decoding**: Wrapping `std::fs::File` for audio file processing +/// - **Memory streams**: Adapting `std::io::Cursor>` for in-memory audio +/// - **Network streams**: Enabling seekable network streams with known lengths +/// - **Custom sources**: Integrating any `Read + Seek` implementation +/// +/// # Metadata Handling +/// +/// The wrapper provides Symphonia with essential stream characteristics: +/// - **Seekability**: Whether random access operations are supported +/// - **Byte length**: Total stream size for seeking and progress calculations +/// - **Configuration**: Stream properties from decoder builder settings +/// +/// # Thread Safety +/// +/// This wrapper requires `Send + Sync` bounds on the wrapped type to ensure +/// thread safety for Symphonia's internal operations. Most standard I/O types +/// satisfy these requirements. +/// +/// # Generic Parameters +/// +/// * `T` - The wrapped I/O type, must implement `Read + Seek + Send + Sync` pub struct ReadSeekSource { - /// The wrapped reader/seeker + /// The wrapped reader/seeker that provides actual I/O operations. + /// + /// All read and seek operations are delegated directly to this inner type, + /// ensuring that performance characteristics are preserved. inner: T, + /// Optional length of the media source in bytes. - /// When known, this can help with seeking and duration calculations. + /// + /// When known, this enables several optimizations: + /// - **Seeking calculations**: Supports percentage-based and end-relative seeks + /// - **Duration estimation**: Helps estimate playback duration for some formats + /// - **Progress tracking**: Enables accurate progress indication + /// - **Buffer management**: Assists with memory allocation decisions + /// + /// This value comes from the decoder settings and should represent the + /// exact byte length of the audio stream. byte_len: Option, - /// Whether this media source reports as seekable. + + /// Whether this media source reports as seekable to Symphonia. + /// + /// This flag controls Symphonia's seeking behavior and optimization decisions: + /// - **`true`**: Enables random access seeking operations + /// - **`false`**: Restricts to forward-only streaming operations + /// + /// The flag should accurately reflect the underlying stream's capabilities. + /// Incorrect values may lead to seek failures or suboptimal performance. is_seekable: bool, } impl ReadSeekSource { /// Creates a new `ReadSeekSource` by wrapping a reader/seeker. /// + /// This constructor extracts relevant configuration from decoder settings + /// to provide Symphonia with appropriate stream metadata while preserving + /// the original I/O source's functionality. + /// /// # Arguments - /// * `inner` - The reader/seeker to wrap - /// * `settings` - Decoder settings for configuring the source + /// + /// * `inner` - The reader/seeker to wrap (takes ownership) + /// * `settings` - Decoder settings containing stream metadata + /// + /// # Examples + /// + /// ```ignore + /// use std::fs::File; + /// use rodio::decoder::{Settings, read_seek_source::ReadSeekSource}; + /// + /// let file = File::open("audio.mp3").unwrap(); + /// let mut settings = Settings::default(); + /// settings.byte_len = Some(1024000); + /// settings.is_seekable = true; + /// + /// let source = ReadSeekSource::new(file, &settings); + /// ``` + /// + /// # Performance + /// + /// This operation is very lightweight, involving only metadata copying + /// and ownership transfer. No I/O operations are performed. #[inline] pub fn new(inner: T, settings: &Settings) -> Self { ReadSeekSource { @@ -35,13 +132,53 @@ impl ReadSeekSource { } impl MediaSource for ReadSeekSource { - /// Returns whether this media source reports as seekable. + /// Returns whether this media source supports random access seeking. + /// + /// This value is determined from the decoder settings and should accurately + /// reflect the underlying stream's capabilities. Symphonia uses this information + /// to decide whether to attempt seeking operations or restrict to forward-only access. + /// + /// # Returns + /// + /// - `true` if random access seeking is supported + /// - `false` if only forward access is available + /// + /// # Impact on Symphonia + /// + /// When `false`, Symphonia will: + /// - Avoid backward seeking operations + /// - Use streaming-optimized algorithms + /// - May provide degraded seeking functionality #[inline] fn is_seekable(&self) -> bool { self.is_seekable } /// Returns the total length of the media source in bytes, if known. + /// + /// This length information enables various Symphonia optimizations including + /// seeking calculations, progress indication, and memory management decisions. + /// The value should represent the exact byte length of the audio stream. + /// + /// # Returns + /// + /// - `Some(length)` if the total byte length is known + /// - `None` if the length cannot be determined + /// + /// # Usage by Symphonia + /// + /// Symphonia may use this information for: + /// - **Seeking calculations**: Computing byte offsets for time-based seeks + /// - **Progress tracking**: Determining playback progress percentage + /// - **Format detection**: Some formats benefit from knowing stream length + /// - **Buffer optimization**: Memory allocation decisions + /// + /// # Accuracy Requirements + /// + /// The returned length must be accurate, as incorrect values may cause: + /// - Seeking errors or failures + /// - Incorrect duration calculations + /// - Progress indication inaccuracies #[inline] fn byte_len(&self) -> Option { self.byte_len @@ -49,10 +186,34 @@ impl MediaSource for ReadSeekSource { } impl Read for ReadSeekSource { - #[inline] /// Reads bytes from the underlying reader into the provided buffer. /// - /// Delegates to the inner reader's implementation. + /// This method provides a zero-cost delegation to the wrapped reader's + /// implementation, preserving all performance characteristics and behavior + /// of the original I/O source. + /// + /// # Arguments + /// + /// * `buf` - Buffer to read data into + /// + /// # Returns + /// + /// - `Ok(n)` where `n` is the number of bytes read + /// - `Err(error)` if an I/O error occurred + /// + /// # Behavior + /// + /// The behavior is identical to the wrapped type's `read` implementation: + /// - May read fewer bytes than requested + /// - Returns 0 when end of stream is reached + /// - May block if the underlying source blocks + /// - Preserves all error conditions from the wrapped source + /// + /// # Performance + /// + /// This delegation has zero overhead and maintains the performance + /// characteristics of the underlying I/O implementation. + #[inline] fn read(&mut self, buf: &mut [u8]) -> Result { self.inner.read(buf) } @@ -61,7 +222,36 @@ impl Read for ReadSeekSource { impl Seek for ReadSeekSource { /// Seeks to a position in the underlying reader. /// - /// Delegates to the inner reader's implementation. + /// This method provides a zero-cost delegation to the wrapped reader's + /// seek implementation, preserving all seeking behavior and performance + /// characteristics of the original I/O source. + /// + /// # Arguments + /// + /// * `pos` - The position to seek to, relative to various points in the stream + /// + /// # Returns + /// + /// - `Ok(position)` - The new absolute position from the start of the stream + /// - `Err(error)` - If a seek error occurred + /// + /// # Behavior + /// + /// The behavior is identical to the wrapped type's `seek` implementation: + /// - Supports all `SeekFrom` variants (Start, End, Current) + /// - May fail if the underlying source doesn't support seeking + /// - Preserves all error conditions from the wrapped source + /// - Updates the stream position for subsequent read operations + /// + /// # Performance + /// + /// This delegation has zero overhead and maintains the seeking performance + /// characteristics of the underlying I/O implementation. + /// + /// # Thread Safety + /// + /// Seeking operations are not automatically synchronized. If multiple threads + /// access the same source, external synchronization is required. #[inline] fn seek(&mut self, pos: SeekFrom) -> Result { self.inner.seek(pos) diff --git a/src/decoder/symphonia.rs b/src/decoder/symphonia.rs index 4154850f..064dafbb 100644 --- a/src/decoder/symphonia.rs +++ b/src/decoder/symphonia.rs @@ -1,37 +1,314 @@ -use core::time::Duration; -use std::sync::Arc; +//! Symphonia multi-format audio decoder implementation. +//! +//! This module provides comprehensive audio decoding using the `symphonia` library, which +//! supports multiple audio formats and containers. Symphonia is designed for high-performance +//! audio decoding with support for complex features like multi-track files, metadata parsing, +//! and format-specific optimizations. +//! +//! # Supported Formats +//! +//! - **Containers**: MP4, OGG, Matroska (MKV), FLAC, WAV, AIFF, CAF +//! - **Codecs**: AAC, FLAC, MP3, Vorbis, Opus, PCM, ALAC, WavPack +//! - **Features**: Multi-track files, embedded metadata, gapless playback +//! - **Advanced**: Chained Ogg streams, complex MP4 structures, codec changes +//! +//! # Capabilities +//! +//! - **Multi-track**: Automatic track selection for audio content +//! - **Seeking**: Precise seeking with timebase-aware positioning +//! - **Duration**: Metadata-based duration with track-specific timing +//! - **Performance**: Optimized decoding with format-specific implementations +//! - **Metadata**: Rich metadata parsing and handling (not exposed in this decoder) +//! - **Error recovery**: Robust handling of corrupted or incomplete streams +//! +//! # Advantages +//! +//! - **Universal support**: Single decoder for many formats +//! - **High performance**: Format-specific optimizations +//! - **Robust parsing**: Handles complex and non-standard files +//! - **Advanced features**: Multi-track, gapless, precise seeking +//! +//! # Configuration +//! +//! The decoder supports extensive configuration through `DecoderBuilder`: +//! - `with_hint("mp4")` - Format hint for faster detection +//! - `with_mime_type("audio/mp4")` - MIME type hint for format identification +//! - `with_gapless(true)` - Enable gapless playback for supported formats +//! - `with_seekable(true)` - Enable seeking support (required for backward seeks) +//! - `with_byte_len(len)` - Required for reliable seeking in some formats +//! - `with_seek_mode(SeekMode::Fastest)` - Use coarse seeking for speed +//! - `with_seek_mode(SeekMode::Nearest)` - Use precise seeking for accuracy +//! +//! # Performance Considerations +//! +//! - Format detection overhead for unknown formats (hints help) +//! - Memory usage scales with track complexity and buffer sizes +//! - Seeking performance varies significantly by container format +//! - Multi-track files may require additional processing overhead +//! - Some formats require byte length for optimal seeking performance +//! +//! # Seeking Behavior +//! +//! Seeking behavior varies by format and configuration: +//! - **MP3**: Requires byte length for coarse seeking, otherwise uses nearest +//! - **OGG**: Requires seekable flag, may fail silently if not set +//! - **MP4**: No automatic fallback between seek modes +//! - **FLAC/WAV**: Generally reliable seeking with proper configuration +//! +//! # Example +//! +//! ```no_run +//! use std::fs::File; +//! use rodio::{Decoder, Source, decoder::builder::SeekMode}; +//! +//! let file = File::open("audio.m4a").unwrap(); +//! let decoder = Decoder::builder() +//! .with_data(file) +//! .with_hint("m4a") +//! .with_seekable(true) +//! .with_gapless(true) +//! .with_seek_mode(SeekMode::Fastest) +//! .build() +//! .unwrap(); +//! +//! // Symphonia can detect bit depth for some formats +//! if let Some(bits) = decoder.bits_per_sample() { +//! println!("Bit depth: {} bits", bits); +//! } +//! ``` + +use std::{sync::Arc, time::Duration}; + use symphonia::{ core::{ - audio::{AudioBufferRef, SampleBuffer, SignalSpec}, - codecs::{Decoder, DecoderOptions, CODEC_TYPE_NULL}, + audio::{SampleBuffer, SignalSpec}, + codecs::{Decoder, DecoderOptions, CODEC_TYPE_NULL, CODEC_TYPE_VORBIS}, errors::Error, - formats::{FormatOptions, FormatReader, SeekMode, SeekTo, SeekedTo}, - io::MediaSourceStream, + formats::{FormatOptions, FormatReader, SeekMode as SymphoniaSeekMode, SeekTo}, + io::{MediaSource, MediaSourceStream}, meta::MetadataOptions, probe::Hint, - units, }, default::get_probe, }; use super::{DecoderError, Settings}; use crate::{ - common::{assert_error_traits, ChannelCount, Sample, SampleRate}, + common::{ChannelCount, Sample, SampleRate}, + decoder::builder::SeekMode, source, Source, }; -pub(crate) struct SymphoniaDecoder { +/// Multi-format audio decoder using the Symphonia library. +/// +/// This decoder provides comprehensive audio format support through Symphonia's +/// pluggable codec and container architecture. It automatically detects formats, +/// selects appropriate tracks, and handles complex features like multi-track files +/// and codec parameter changes. +/// +/// # Architecture +/// +/// The decoder consists of several key components: +/// - **Format reader/demuxer**: Parses container formats and extracts packets +/// - **Codec decoder**: Decodes audio packets to PCM samples +/// - **Sample buffer**: Holds decoded audio data for iteration +/// - **Track selection**: Automatically selects the first audio track +/// +/// # Multi-track Support +/// +/// For files with multiple tracks, the decoder automatically selects the first +/// track with a supported audio codec. When codec resets occur (rare), it +/// attempts to continue with the next available audio track. +/// +/// # Buffer Management +/// +/// The decoder uses dynamic buffer allocation based on codec capabilities: +/// - Buffer size determined by maximum frame length for the codec +/// - Buffers reused when possible to minimize allocations +/// - Automatic buffer clearing on decode errors for robust operation +/// +/// # Error Recovery +/// +/// The decoder implements sophisticated error recovery: +/// - **Decode errors**: Skip corrupted packets and continue +/// - **Reset required**: Recreate decoder and continue with next track +/// - **I/O errors**: Attempt to continue when possible +/// - **Terminal errors**: Clean shutdown when recovery is impossible +/// +/// # Thread Safety +/// +/// This decoder is not thread-safe. Create separate instances for concurrent access +/// or use appropriate synchronization primitives. +pub struct SymphoniaDecoder { + /// The underlying Symphonia audio decoder. + /// + /// Handles the actual audio decoding from compressed packets to PCM samples. + /// May be recreated during playback if codec parameters change or reset is required. decoder: Box, + + /// Current position within the decoded audio buffer. + /// + /// Tracks the next sample index to return from the current buffer. + /// Reset to 0 when a new packet is decoded and buffered. current_span_offset: usize, - format: Box, + + /// The format reader/demuxer for the container. + /// + /// Responsible for parsing the container format and extracting audio packets. + /// Different implementations exist for each supported container format. + demuxer: Box, + + /// Total duration from track metadata. + /// + /// Calculated from track timebase and frame count when available. + /// May be `None` for streams without duration metadata or live streams. total_duration: Option, - buffer: SampleBuffer, + + /// Current decoded audio buffer. + /// + /// Contains interleaved PCM samples from the most recently decoded packet. + /// `None` indicates that a new packet needs to be decoded. + buffer: Option>, + + /// Audio signal specification (channels, sample rate, etc.). + /// + /// May change during playback if codec parameters change or track switching occurs. + /// Updated automatically when such changes are detected. spec: SignalSpec, + + /// Seeking precision mode. + /// + /// Controls the trade-off between seeking speed and accuracy: + /// - `Fastest`: Uses coarse seeking (faster, less accurate) + /// - `Nearest`: Uses precise seeking (slower, sample-accurate) seek_mode: SeekMode, + + /// Total number of samples (estimated from duration). + /// + /// Calculated from frame count when available, otherwise from duration. + /// Used for progress indication and size hints. + total_samples: Option, + + /// Number of samples read so far. + /// + /// Tracks current playback position for seeking calculations and progress indication. + /// Updated on every sample returned from the iterator. + samples_read: u64, + + /// ID of the currently selected track. + /// + /// Used to filter packets and handle track changes during playback. + /// May change if codec reset occurs and track switching is needed. + track_id: u32, + + /// Whether seeking operations are supported. + /// + /// Determined by the underlying media source stream capabilities. + /// Required for backward seeking in most formats. + is_seekable: bool, + + /// Total byte length of the source (for seeking calculations). + /// + /// Required for some seeking operations, particularly coarse seeking in MP3. + /// When not available, seeking may fall back to less optimal methods. + byte_len: Option, } impl SymphoniaDecoder { - pub(crate) fn new(mss: MediaSourceStream, settings: &Settings) -> Result { + /// Creates a Symphonia decoder with default settings. + /// + /// This method initializes the decoder with default configuration, which includes + /// no format hints, disabled gapless playback, and nearest seeking mode. For better + /// performance and functionality, consider using `new_with_settings`. + /// + /// # Arguments + /// + /// * `mss` - MediaSourceStream containing the audio data + /// + /// # Returns + /// + /// - `Ok(SymphoniaDecoder)` if initialization succeeded + /// - `Err(DecoderError)` if format detection or initialization failed + /// + /// # Examples + /// + /// ```ignore + /// use rodio::decoder::symphonia::SymphoniaDecoder; + /// use symphonia::core::io::MediaSourceStream; + /// use std::fs::File; + /// + /// let file = File::open("audio.mp3").unwrap(); + /// let mss = MediaSourceStream::new(Box::new(file), Default::default()); + /// let decoder = SymphoniaDecoder::new(mss).unwrap(); + /// ``` + /// + /// # Performance + /// + /// Without format hints, the decoder must probe all supported formats, + /// which can be slower than providing hints via `new_with_settings`. + #[allow(dead_code)] + pub fn new(mss: MediaSourceStream) -> Result { + Self::new_with_settings(mss, &Settings::default()) + } + + /// Creates a Symphonia decoder with custom settings. + /// + /// This method provides full control over decoder initialization, including format hints, + /// seeking configuration, and performance optimizations. It performs format detection, + /// track selection, and initial packet decoding to establish stream characteristics. + /// + /// # Arguments + /// + /// * `mss` - MediaSourceStream containing the audio data + /// * `settings` - Configuration settings from `DecoderBuilder` + /// + /// # Returns + /// + /// - `Ok(SymphoniaDecoder)` if initialization succeeded + /// - `Err(DecoderError)` if format detection or initialization failed + /// + /// # Settings Usage + /// + /// - `hint`: Format hint (e.g., "mp3", "m4a") for faster detection + /// - `mime_type`: MIME type hint for format identification + /// - `gapless`: Enable gapless playback for supported formats + /// - `seek_mode`: Control seeking precision vs. speed trade-off + /// - Additional settings affect seeking and performance behavior + /// + /// # Error Handling + /// + /// Various initialization errors are mapped to appropriate `DecoderError` variants: + /// - `UnrecognizedFormat`: No suitable format found + /// - `NoStreams`: File contains no audio tracks + /// - `IoError`: I/O error during initialization + /// - `DecodeError`: Error decoding initial packet + /// + /// # Examples + /// + /// ```ignore + /// use rodio::decoder::{symphonia::SymphoniaDecoder, Settings, builder::SeekMode}; + /// use symphonia::core::io::MediaSourceStream; + /// use std::fs::File; + /// + /// let file = File::open("audio.m4a").unwrap(); + /// let mss = MediaSourceStream::new(Box::new(file), Default::default()); + /// + /// let mut settings = Settings::default(); + /// settings.hint = Some("m4a".to_string()); + /// settings.gapless = true; + /// settings.seek_mode = SeekMode::Fastest; + /// + /// let decoder = SymphoniaDecoder::new_with_settings(mss, &settings).unwrap(); + /// ``` + /// + /// # Performance + /// + /// Providing accurate format hints significantly improves initialization speed + /// by reducing the number of format probes required. + pub fn new_with_settings( + mss: MediaSourceStream, + settings: &Settings, + ) -> Result { match SymphoniaDecoder::init(mss, settings) { Err(e) => match e { Error::IoError(e) => Err(DecoderError::IoError(e.to_string())), @@ -48,11 +325,75 @@ impl SymphoniaDecoder { } } + /// Consumes the decoder and returns the underlying media source stream. + /// + /// This can be useful for recovering the original data source after decoding + /// is complete or when the decoder needs to be replaced. The stream will be + /// positioned at the current playback location. + /// + /// # Examples + /// + /// ```ignore + /// use rodio::decoder::symphonia::SymphoniaDecoder; + /// use std::fs::File; + /// + /// let file = File::open("audio.mp3").unwrap(); + /// let mss = MediaSourceStream::new(Box::new(file), Default::default()); + /// let decoder = SymphoniaDecoder::new(mss).unwrap(); + /// let recovered_mss = decoder.into_inner(); + /// ``` + /// + /// # Stream Position + /// + /// The returned MediaSourceStream will be positioned at the current + /// packet location, which may be useful for advanced processing or + /// manual demuxing operations. #[inline] - pub(crate) fn into_inner(self) -> MediaSourceStream { - self.format.into_inner() + pub fn into_inner(self) -> MediaSourceStream { + self.demuxer.into_inner() } + /// Initializes the Symphonia decoder with format detection and track selection. + /// + /// This internal method handles the complex initialization process including: + /// format probing, track selection, decoder creation, and initial packet decoding + /// to establish stream characteristics. It implements robust error handling + /// for various initialization scenarios. + /// + /// # Arguments + /// + /// * `mss` - MediaSourceStream containing the audio data + /// * `settings` - Configuration settings affecting initialization + /// + /// # Returns + /// + /// - `Ok(Some(decoder))` if initialization succeeded + /// - `Ok(None)` if no suitable audio tracks found + /// - `Err(Error)` if format detection or initialization failed + /// + /// # Initialization Process + /// + /// 1. **Format probing**: Detect container format using hints and probing + /// 2. **Track selection**: Find first track with supported audio codec + /// 3. **Decoder creation**: Create appropriate codec decoder for track + /// 4. **Initial decode**: Decode first packet to establish signal specification + /// 5. **Buffer allocation**: Allocate sample buffer based on codec capabilities + /// 6. **Duration calculation**: Calculate total duration from metadata + /// + /// # Error Recovery + /// + /// The method handles various error conditions during initialization: + /// - **Reset required**: Recreates decoder and continues + /// - **Decode errors**: Skips corrupted packets during initialization + /// - **Empty packets**: Continues searching for valid audio data + /// - **Track changes**: Adapts to multi-track scenarios + /// + /// # Performance Optimizations + /// + /// - Uses format hints to reduce probing overhead + /// - Allocates buffers based on codec maximum frame size + /// - Caches duration and sample count calculations + /// - Reuses existing allocations when possible fn init( mss: MediaSourceStream, settings: &Settings, @@ -69,51 +410,40 @@ impl SymphoniaDecoder { ..Default::default() }; let metadata_opts: MetadataOptions = Default::default(); - let seek_mode = if settings.coarse_seek { - SeekMode::Coarse - } else { - SeekMode::Accurate - }; - let mut probed = get_probe().format(&hint, mss, &format_opts, &metadata_opts)?; + let is_seekable = mss.is_seekable(); + let byte_len = mss.byte_len(); - let stream = match probed.format.default_track() { - Some(stream) => stream, - None => return Ok(None), - }; - - // Select the first supported track - let track_id = probed + // Select the first supported track (non-null codec) + let mut probed = get_probe().format(&hint, mss, &format_opts, &metadata_opts)?; + let track = probed .format .tracks() .iter() .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) - .ok_or(symphonia::core::errors::Error::Unsupported( - "No track with supported codec", - ))? - .id; - - let track = match probed - .format - .tracks() - .iter() - .find(|track| track.id == track_id) - { - Some(track) => track, - None => return Ok(None), - }; + .ok_or(Error::Unsupported("No track with supported codec"))?; + let mut track_id = track.id; let mut decoder = symphonia::default::get_codecs() .make(&track.codec_params, &DecoderOptions::default())?; - let total_duration = stream + let total_duration: Option = track .codec_params .time_base - .zip(stream.codec_params.n_frames) + .zip(track.codec_params.n_frames) .map(|(base, spans)| base.calc_time(spans).into()); - let decoded = loop { + // Find the first decodable packet and initialize spec from it + let (spec, buffer) = loop { let current_span = match probed.format.next_packet() { Ok(packet) => packet, - Err(Error::IoError(_)) => break decoder.last_decoded(), + + // If ResetRequired is returned, then the track list must be re-examined and all + // Decoders re-created. + Err(Error::ResetRequired) => { + track_id = recreate_decoder(&mut probed.format, &mut decoder, None, None)?; + continue; + } + + // All other errors are unrecoverable. Err(e) => return Err(e), }; @@ -123,46 +453,118 @@ impl SymphoniaDecoder { } match decoder.decode(¤t_span) { - Ok(decoded) => break decoded, - Err(e) => match e { - Error::DecodeError(_) => { - // Decode errors are intentionally ignored with no retry limit. - // This behavior ensures that the decoder skips over problematic packets - // and continues processing the rest of the stream. + Ok(decoded) => { + // Only accept packets with actual audio frames + if decoded.frames() > 0 { + // Set spec from first successful decode + let spec = decoded.spec().to_owned(); + + // Allocate buffer based on maximum frame length for codec + let mut sample_buffer = + SampleBuffer::::new(decoded.capacity() as u64, *decoded.spec()); + sample_buffer.copy_interleaved_ref(decoded); + let buffer = Some(sample_buffer); + break (spec, buffer); + } + continue; // Empty packet - try the next one + } + Err(e) => { + if should_continue_on_decode_error(&e, &mut decoder) { continue; + } else { + return Err(e); } - _ => return Err(e), - }, + } } }; - let spec = decoded.spec().to_owned(); - let buffer = SymphoniaDecoder::get_buffer(decoded, &spec); - Ok(Some(SymphoniaDecoder { + + // Calculate total samples + let total_samples = { + // Try frame-based calculation first (most accurate) + if let (Some(n_frames), Some(max_frame_length)) = ( + decoder.codec_params().n_frames, + decoder.codec_params().max_frames_per_packet, + ) { + n_frames.checked_mul(max_frame_length) + } else if let Some(duration) = total_duration { + // Fallback to duration-based calculation + let total_secs = duration.as_secs_f64(); + let sample_rate = spec.rate as f64; + let channels = spec.channels.count() as f64; + Some((total_secs * sample_rate * channels).ceil() as u64) + } else { + None + } + }; + + Ok(Some(Self { decoder, current_span_offset: 0, - format: probed.format, + demuxer: probed.format, total_duration, buffer, spec, - seek_mode, + seek_mode: settings.seek_mode, + total_samples, + samples_read: 0, + track_id, + is_seekable, + byte_len, })) } - - #[inline] - fn get_buffer(decoded: AudioBufferRef, spec: &SignalSpec) -> SampleBuffer { - let duration = units::Duration::from(decoded.capacity() as u64); - let mut buffer = SampleBuffer::::new(duration, *spec); - buffer.copy_interleaved_ref(decoded); - buffer - } } impl Source for SymphoniaDecoder { + /// Returns the number of samples before parameters change. + /// + /// For Symphonia, this returns the number of samples in the current buffer + /// when available, or `Some(0)` when the stream is exhausted. Unlike formats + /// with fixed parameters, Symphonia streams may have parameter changes during + /// playback due to track switches or codec resets. + /// + /// # Parameter Changes + /// + /// Symphonia may encounter parameter changes in several scenarios: + /// - **Codec resets**: Required when stream parameters change + /// - **Track switching**: Multi-track files with different specifications + /// - **Chained streams**: Different specifications in concatenated streams + /// + /// # Buffer Sizes + /// + /// Buffer sizes are determined by the codec's maximum frame length and + /// may vary between packets based on encoding complexity and format + /// characteristics. #[inline] fn current_span_len(&self) -> Option { - Some(self.buffer.len()) + // Audio spec remains stable for the length of the buffer. Return Some(0) when exhausted. + self.buffer.as_ref().map(SampleBuffer::len).or(Some(0)) } + /// Returns the number of audio channels. + /// + /// The channel count comes from Symphonia's signal specification which is + /// established during initialization and may change during playback if + /// codec parameters change or track switching occurs. + /// + /// # Dynamic Changes + /// + /// While most files have consistent channel configuration, Symphonia handles + /// cases where channel count may change: + /// - **Multi-track files**: Different tracks with different channel counts + /// - **Codec resets**: Parameter changes requiring decoder recreation + /// - **Chained streams**: Concatenated streams with different specifications + /// + /// # Channel Mapping + /// + /// Symphonia follows format-specific channel mapping conventions, which + /// vary between container formats and codecs. The decoder preserves the + /// original channel order from the source material. + /// + /// # Guarantees + /// + /// The returned value reflects the current signal specification and is + /// valid for the current buffer. It may change between buffers if stream + /// parameters change. #[inline] fn channels(&self) -> ChannelCount { ChannelCount::new( @@ -175,118 +577,287 @@ impl Source for SymphoniaDecoder { .expect("audio should always have at least one channel") } + /// Returns the sample rate in Hz. + /// + /// The sample rate comes from Symphonia's signal specification which is + /// established during initialization and may change during playback if + /// codec parameters change. + /// + /// # Dynamic Changes + /// + /// While most files have consistent sample rates, Symphonia handles cases + /// where sample rate may change during playback: + /// - **Multi-track files**: Different tracks with different sample rates + /// - **Codec resets**: Parameter changes requiring decoder recreation + /// - **Chained streams**: Concatenated streams with different sample rates + /// + /// # Format Support + /// + /// Supported sample rates depend on the underlying format and codec: + /// - **MP3**: 8kHz, 11.025kHz, 12kHz, 16kHz, 22.05kHz, 24kHz, 32kHz, 44.1kHz, 48kHz + /// - **AAC**: 8kHz to 96kHz (format-dependent) + /// - **FLAC**: 1Hz to 655.35kHz (though practical range is smaller) + /// - **Vorbis**: 8kHz to 192kHz (encoder-dependent) + /// + /// # Guarantees + /// + /// The returned value reflects the current signal specification and is + /// valid for the current buffer. It may change between buffers if stream + /// parameters change. #[inline] fn sample_rate(&self) -> SampleRate { SampleRate::new(self.spec.rate).expect("audio should always have a non zero SampleRate") } + /// Returns the total duration of the audio stream. + /// + /// Duration is calculated from track metadata when available, providing accurate + /// timing information. The calculation uses timebase and frame count information + /// from the container or codec metadata. + /// + /// # Availability + /// + /// Duration is available when: + /// 1. Track contains timebase and frame count metadata + /// 2. Container format provides duration information + /// 3. Format reader successfully extracts timing metadata + /// + /// Returns `None` for: + /// - Live streams without predetermined duration + /// - Malformed files missing duration metadata + /// - Streams where duration cannot be determined + /// + /// # Accuracy + /// + /// Duration accuracy depends on the source: + /// - **Metadata-based**: Exact duration from container or codec information + /// - **Calculated**: Derived from frame count and timebase (very accurate) + /// - **Missing**: No duration information available + /// + /// # Multi-track Files + /// + /// For multi-track files, duration represents the length of the currently + /// selected audio track, not the entire file duration. #[inline] fn total_duration(&self) -> Option { self.total_duration } - fn try_seek(&mut self, pos: Duration) -> Result<(), source::SeekError> { - if matches!(self.seek_mode, SeekMode::Accurate) - && self.decoder.codec_params().time_base.is_none() - { - return Err(source::SeekError::SymphoniaDecoder( - SeekError::AccurateSeekNotSupported, - )); - } + /// Returns the bit depth of the audio samples. + /// + /// The bit depth comes from the codec parameters when available. Not all + /// formats preserve or report original bit depth information, particularly + /// lossy formats that use different internal representations. + /// + /// # Format Support + /// + /// Bit depth availability varies by format: + /// - **FLAC**: Always available (8, 16, 24, 32-bit) + /// - **WAV/PCM**: Always available (8, 16, 24, 32-bit integer/float) + /// - **ALAC**: Available (16, 20, 24, 32-bit) + /// - **MP3**: Not available (lossy compression) + /// - **AAC**: Not available (lossy compression) + /// - **Vorbis**: Not available (floating-point processing) + /// + /// # Implementation Note + /// + /// For lossy formats, bit depth is not meaningful as the audio undergoes + /// compression that removes the original bit depth information. Lossless + /// formats preserve and report the original bit depth. + /// + /// # Returns + /// + /// - `Some(depth)` for formats that preserve bit depth information + /// - `None` for lossy formats or when bit depth is not determinable + #[inline] + fn bits_per_sample(&self) -> Option { + self.decoder.codec_params().bits_per_sample + } + /// Attempts to seek to the specified position in the audio stream. + /// + /// Symphonia seeking behavior varies significantly by format and configuration. + /// The implementation provides both coarse (fast) and accurate (precise) seeking + /// modes, with automatic fallbacks for optimal compatibility. + /// + /// # Seeking Modes + /// + /// - **`SeekMode::Fastest`**: Uses coarse seeking when possible + /// - Faster performance with larger tolerances + /// - May require fine-tuning for exact positioning + /// - Falls back to accurate seeking if byte length unavailable + /// - **`SeekMode::Nearest`**: Uses accurate seeking for precision + /// - Sample-accurate positioning when supported + /// - Slower performance due to precise calculations + /// - Always attempts exact positioning + /// + /// # Format-Specific Behavior + /// + /// Different formats have varying seeking requirements and capabilities: + /// + /// | Format | Direction | Mode | Requirements | Notes | + /// |--------|-----------|------|--------------|-------| + /// | AAC | Backward | Any | is_seekable | Standard behavior | + /// | FLAC | Backward | Any | is_seekable | Reliable seeking | + /// | MP3 | Backward | Any | is_seekable | Good compatibility | + /// | MP3 | Any | Coarse | byte_len | Unique requirement | + /// | MP4 | Backward | Any | is_seekable | No auto fallback | + /// | OGG | Any | Any | is_seekable | May fail silently | + /// | WAV | Backward | Any | is_seekable | Excellent performance | + /// + /// # Performance Characteristics + /// + /// - **Coarse seeks**: O(log n) performance for most formats + /// - **Accurate seeks**: Variable performance, format-dependent + /// - **Forward seeks**: Often optimized by skipping packets + /// - **Backward seeks**: Require stream reset and repositioning + /// + /// # Error Handling + /// + /// The method handles various seeking scenarios: + /// - **Forward-only mode**: Prevents backward seeks when not seekable + /// - **Vorbis workaround**: Uses linear seeking for problematic streams + /// - **Automatic fallbacks**: Switches seek modes when needed + /// - **Boundary clamping**: Limits seeks to valid stream range + /// + /// # Arguments + /// + /// * `pos` - Target position as duration from stream start + /// + /// # Errors + /// + /// - `SeekError::ForwardOnly` - Backward seek attempted without seekable flag + /// - `SeekError::Demuxer` - Underlying demuxer error during seek operation + /// + /// # Examples + /// + /// ```no_run + /// use std::{fs::File, time::Duration}; + /// use rodio::{Decoder, Source, decoder::builder::SeekMode}; + /// + /// let file = File::open("audio.m4a").unwrap(); + /// let mut decoder = Decoder::builder() + /// .with_data(file) + /// .with_seekable(true) + /// .with_seek_mode(SeekMode::Fastest) + /// .build() + /// .unwrap(); + /// + /// // Fast seek to 30 seconds + /// decoder.try_seek(Duration::from_secs(30)).unwrap(); + /// ``` + /// + /// # Implementation Details + /// + /// The seeking implementation includes several optimizations: + /// - **Channel preservation**: Maintains correct channel alignment after seeks + /// - **Decoder reset**: Ensures clean state after demuxer seeks + /// - **Position tracking**: Updates sample counters based on actual seek results + /// - **Fine-tuning**: Sample-accurate positioning when precise mode is used + fn try_seek(&mut self, pos: Duration) -> Result<(), source::SeekError> { // Seeking should be "saturating", meaning: target positions beyond the end of the stream // are clamped to the end. let mut target = pos; - if let Some(total_duration) = self.total_duration { + if let Some(total_duration) = self.total_duration() { if target > total_duration { target = total_duration; } } + let target_samples = (target.as_secs_f64() + * self.sample_rate().get() as f64 + * self.channels().get() as f64) as u64; + // Remember the current channel, so we can restore it after seeking. let active_channel = self.current_span_offset % self.channels().get() as usize; - let seek_res = match self.format.seek( - self.seek_mode, - SeekTo::Time { - time: target.into(), - track_id: None, - }, - ) { - Err(Error::SeekError(symphonia::core::errors::SeekErrorKind::ForwardOnly)) => { - return Err(source::SeekError::SymphoniaDecoder( - SeekError::RandomAccessNotSupported, - )); + // | Format | Direction | SymphoniaSeekMode | Requires | Remarks | + // |--------|-----------|-------------------|--------------------|-----------------------| + // | AAC | Backward | Any | is_seekable | | + // | AIFF | Backward | Any | is_seekable | | + // | CAF | Backward | Any | is_seekable | | + // | FLAC | Backward | Any | is_seekable | | + // | MKV | Backward | Any | is_seekable | | + // | MP3 | Backward | Any | is_seekable | | + // | MP3 | Any | Coarse | byte_len.is_some() | No other coarse impls | + // | MP4 | Backward | Any | is_seekable | No automatic fallback | + // | OGG | Any | Any | is_seekable | Fails silently if not | + // | WAV | Backward | Any | is_seekable | | + if !self.is_seekable { + if target_samples < self.samples_read { + return Err(source::SeekError::ForwardOnly); } - other => other.map_err(Arc::new).map_err(SeekError::Demuxer), - }?; - // Seeking is a demuxer operation without the decoder knowing about it, - // so we need to reset the decoder to make sure it's in sync and prevent - // audio glitches. - self.decoder.reset(); + // TODO: remove when Symphonia has fixed linear seeking for Vorbis + if self.decoder.codec_params().codec == CODEC_TYPE_VORBIS { + for _ in self.samples_read..target_samples { + let _ = self.next(); + } + return Ok(()); + } + } - // Force the iterator to decode the next packet. - self.current_span_offset = usize::MAX; + let seek_mode = if self.seek_mode == SeekMode::Fastest && self.byte_len.is_none() { + // Fallback to accurate (nearest) seeking if no byte length is known + SymphoniaSeekMode::Accurate + } else { + self.seek_mode.into() + }; - // Symphonia does not seek to the exact position, it seeks to the closest keyframe. - // If accurate seeking is required, fast-forward to the exact position. - if matches!(self.seek_mode, SeekMode::Accurate) { - self.refine_position(seek_res)?; - } + let seek_res = self + .demuxer + .seek( + seek_mode, + SeekTo::Time { + time: target.into(), + track_id: Some(self.track_id), + }, + ) + .map_err(Arc::new)?; - // After seeking, we are at the beginning of an inter-sample frame, i.e. the first - // channel. We need to advance the iterator to the right channel. - for _ in 0..active_channel { - self.next(); - } + // Seeking is a demuxer operation without the decoder knowing about it, so we need to reset + // the decoder to make sure it's in sync and prevent audio glitches. + self.decoder.reset(); - Ok(()) - } -} + // Clear buffer - let next() handle loading new packets + self.buffer = None; -/// Error returned when the try_seek implementation of the symphonia decoder fails. -#[derive(Debug, thiserror::Error, Clone)] -pub enum SeekError { - /// Accurate seeking is not supported - /// - /// This error occurs when the decoder cannot extract time base information from the source. - /// You may catch this error to try a coarse seek instead. - #[error("Accurate seeking is not supported on this file/byte stream that lacks time base information")] - AccurateSeekNotSupported, - /// The decoder does not support random access seeking - /// - /// This error occurs when the source is not seekable or does not have a known byte length. - #[error("The decoder needs to know the length of the file/byte stream to be able to seek backwards. You can set that by using the `DecoderBuilder` or creating a decoder using `Decoder::try_from(some_file)`.")] - RandomAccessNotSupported, - /// Demuxer failed to seek - #[error("Demuxer failed to seek")] - Demuxer(#[source] Arc), -} -assert_error_traits!(SeekError); + // Update samples_read counter based on actual seek position + self.samples_read = if let Some(time_base) = self.decoder.codec_params().time_base { + let actual_time = Duration::from(time_base.calc_time(seek_res.actual_ts)); + (actual_time.as_secs_f64() + * self.sample_rate().get() as f64 + * self.channels().get() as f64) as u64 + } else { + // Fallback in the unexpected case that the format has no base time set + seek_res.actual_ts * self.sample_rate().get() as u64 * self.channels().get() as u64 + }; -impl SymphoniaDecoder { - /// Note span offset must be set after - fn refine_position(&mut self, seek_res: SeekedTo) -> Result<(), source::SeekError> { - // Calculate the number of samples to skip. - let mut samples_to_skip = (Duration::from( - self.decoder - .codec_params() - .time_base - .expect("time base availability guaranteed by caller") - .calc_time(seek_res.required_ts.saturating_sub(seek_res.actual_ts)), - ) - .as_secs_f32() - * self.sample_rate().get() as f32 - * self.channels().get() as f32) - .ceil() as usize; + // Symphonia does not seek to the exact position, it seeks to the closest keyframe. + // If nearest seeking is required, fast-forward to the exact position. + let mut samples_to_skip = 0; + if self.seek_mode == SeekMode::Nearest { + // Calculate the number of samples to skip. + samples_to_skip = (Duration::from( + self.decoder + .codec_params() + .time_base + .expect("time base availability guaranteed by caller") + .calc_time(seek_res.required_ts.saturating_sub(seek_res.actual_ts)), + ) + .as_secs_f32() + * self.sample_rate().get() as f32 + * self.channels().get() as f32) + .ceil() as usize; - // Re-align the seek position to the first channel. - samples_to_skip -= samples_to_skip % self.channels().get() as usize; + // Re-align the seek position to the first channel. + samples_to_skip -= samples_to_skip % self.channels().get() as usize + }; - // Skip ahead to the precise position. - for _ in 0..samples_to_skip { - self.next(); + // After seeking, we are at the beginning of an inter-sample frame, i.e. the first channel. + // We need to advance the iterator to the right channel. + for _ in 0..(samples_to_skip + active_channel) { + let _ = self.next(); } Ok(()) @@ -294,41 +865,433 @@ impl SymphoniaDecoder { } impl Iterator for SymphoniaDecoder { + /// The type of items yielded by the iterator. + /// + /// Returns `Sample` (typically `f32`) values representing individual audio samples. + /// Samples are interleaved across channels in the order determined by the format's + /// channel mapping specification. type Item = Sample; + /// Returns the next audio sample from the multi-format stream. + /// + /// This method implements sophisticated packet-based decoding with robust error recovery. + /// It automatically handles format-specific details, codec resets, track changes, + /// and various error conditions while maintaining optimal performance. + /// + /// # Decoding Process + /// + /// The method follows a two-phase approach: + /// 1. **Hot path**: Return samples from current buffer (very fast) + /// 2. **Cold path**: Decode new packets when buffer is exhausted (slower) + /// + /// # Buffer Management + /// + /// The decoder uses intelligent buffer management: + /// - **Reuse existing buffers**: Minimizes allocations during playback + /// - **Dynamic allocation**: Creates buffers based on codec capabilities + /// - **Capacity-based sizing**: Uses maximum frame length for optimal performance + /// - **Automatic clearing**: Handles error conditions gracefully + /// + /// # Error Recovery + /// + /// The method implements comprehensive error recovery: + /// - **Decode errors**: Skip corrupted packets and continue playback + /// - **Reset required**: Recreate decoder and attempt track switching + /// - **I/O errors**: Attempt continuation when possible + /// - **Empty packets**: Skip metadata-only packets automatically + /// - **Track changes**: Handle multi-track scenarios transparently + /// + /// # Performance Optimizations + /// + /// - **Buffer reuse**: Minimizes memory allocations + /// - **Error classification**: Quick decisions on error handling + /// - **Packet filtering**: Efficient track-specific packet processing + /// - **Lazy allocation**: Buffers created only when needed + /// + /// # Format Adaptation + /// + /// The decoder adapts to various format characteristics: + /// - **Variable packet sizes**: Handles dynamic content-dependent sizes + /// - **Codec changes**: Supports streams with changing codecs + /// - **Multi-track streams**: Automatically filters relevant packets + /// - **Parameter changes**: Adapts to changing signal specifications + /// + /// # Returns + /// + /// - `Some(sample)` - Next audio sample from the stream + /// - `None` - End of stream reached or unrecoverable error occurred + /// + /// # Channel Order + /// + /// Samples are returned in format-specific channel order: + /// - **WAV/PCM**: Standard channel mapping (L, R, C, LFE, ...) + /// - **MP4/AAC**: AAC channel configuration standards + /// - **OGG/Vorbis**: Vorbis channel mapping specification + /// - **FLAC**: FLAC channel assignment standards fn next(&mut self) -> Option { - if self.current_span_offset >= self.buffer.len() { - let decoded = loop { - let packet = self.format.next_packet().ok()?; - let decoded = match self.decoder.decode(&packet) { - Ok(decoded) => decoded, - Err(Error::DecodeError(_)) => { - // Skip over packets that cannot be decoded. This ensures the iterator - // continues processing subsequent packets instead of terminating due to - // non-critical decode errors. + // Hot path: return sample from current buffer if available + if let Some(buffer) = &self.buffer { + if self.current_span_offset < buffer.len() { + let sample = buffer.samples()[self.current_span_offset]; + self.current_span_offset += 1; + self.samples_read += 1; + return Some(sample); + } + } + + // Cold path: need to decode next packet + let decoded = loop { + let packet = match self.demuxer.next_packet() { + Ok(packet) => packet, + + // If ResetRequired is returned, then the track list must be re-examined and all + // Decoders re-created. + Err(Error::ResetRequired) => { + self.track_id = recreate_decoder( + &mut self.demuxer, + &mut self.decoder, + Some(self.track_id), + Some(&mut self.spec), + ) + .ok()?; + + // Clear buffer after decoder reset - spec may have been updated + self.buffer = None; + continue; + } + + // All other errors are unrecoverable. + Err(_) => return None, + }; + + match self.decoder.decode(&packet) { + Ok(decoded) => { + // Only accept packets with actual audio frames + if decoded.frames() > 0 { + break decoded; + } + continue; // Empty packet - try the next one + } + Err(e) => { + if should_continue_on_decode_error(&e, &mut self.decoder) { + // For recoverable errors, just clear buffer contents but keep allocation + if let Some(buffer) = self.buffer.as_mut() { + buffer.clear(); + } continue; + } else { + // Internal buffer *must* be cleared if an error occurs. + self.buffer = None; + return None; // Terminal error - end of iteration } - Err(_) => return None, - }; - - // Loop until we get a packet with audio frames. This is necessary because some - // formats can have packets with only metadata, particularly when rewinding, in - // which case the iterator would otherwise end with `None`. - // Note: checking `decoded.frames()` is more reliable than `packet.dur()`, which - // can resturn non-zero durations for packets without audio frames. - if decoded.frames() > 0 { - break decoded; } - }; + } + }; + + // Reuse buffer when possible + let buffer = match self.buffer.as_mut() { + Some(buffer) => buffer, + None => { + // Although packet sizes are not guaranteed to be constant, the buffer + // size is based on the maximum frame length for the codec, so we can + // allocate once and reuse it for as long as the codec specifications + // remain the same. + self.buffer.insert(SampleBuffer::new( + decoded.capacity() as u64, + *decoded.spec(), + )) + } + }; + buffer.copy_interleaved_ref(decoded); + self.current_span_offset = 0; - decoded.spec().clone_into(&mut self.spec); - self.buffer = SymphoniaDecoder::get_buffer(decoded, &self.spec); - self.current_span_offset = 0; + // Successfully fetched next packet + if !buffer.is_empty() { + // Buffer now has samples - return the first one. This is a bit redundant + // but faster than calling next() recursively. + let sample = buffer.samples()[0]; + self.current_span_offset = 1; + self.samples_read += 1; + Some(sample) + } else { + // Empty buffer after successful packet - could be that this packet contains metadata + // only. Recursively try again until we hit the end of the stream. + self.next() } + } - let sample = *self.buffer.samples().get(self.current_span_offset)?; - self.current_span_offset += 1; + /// Returns bounds on the remaining length of the iterator. + /// + /// Provides size estimates based on Symphonia's format analysis and current + /// playback position. The accuracy depends on the availability and reliability + /// of metadata from the underlying format. + /// + /// # Returns + /// + /// A tuple `(lower_bound, upper_bound)` where: + /// - `lower_bound`: Minimum number of samples guaranteed to be available + /// - `upper_bound`: Maximum number of samples that might be available (None if unknown) + /// + /// # Accuracy Levels + /// + /// - **High accuracy**: When total samples calculated from frame count metadata + /// - **Moderate accuracy**: When estimated from duration and signal specification + /// - **Conservative estimate**: When only current buffer information available + /// - **Stream exhausted**: (0, Some(0)) when no more data + /// + /// # Format Variations + /// + /// Different formats provide varying levels of size information: + /// - **FLAC**: Exact frame count in metadata (highest accuracy) + /// - **WAV**: Sample count in header (perfect accuracy) + /// - **MP4**: Duration-based estimation (good accuracy) + /// - **MP3**: Variable accuracy depending on encoding type + /// - **OGG**: Duration-based when available + /// + /// # Implementation + /// + /// The lower bound represents samples currently buffered in memory. + /// The upper bound uses the most accurate available method: + /// 1. Frame-based calculation (when available) + /// 2. Duration-based estimation (fallback) + /// 3. No estimate (when insufficient information) + /// + /// # Use Cases + /// + /// - **Progress indication**: Upper bound enables percentage calculation + /// - **Buffer allocation**: Lower bound ensures minimum available samples + /// - **End detection**: (0, Some(0)) indicates stream completion + /// - **Memory planning**: Helps optimize buffer sizes for processing + /// + /// # Multi-track Considerations + /// + /// For multi-track files, estimates represent the currently selected audio + /// track, not the entire file duration or all tracks combined. + fn size_hint(&self) -> (usize, Option) { + // Samples already decoded and buffered (guaranteed available) + let buffered_samples = self + .current_span_len() + .unwrap_or(0) + .saturating_sub(self.current_span_offset); - Some(sample) + if let Some(total_samples) = self.total_samples { + let total_remaining = total_samples.saturating_sub(self.samples_read) as usize; + (buffered_samples, Some(total_remaining)) + } else if self.buffer.is_none() { + // Stream exhausted + (0, Some(0)) + } else { + (buffered_samples, None) + } + } +} + +/// Recreates decoder after ResetRequired error from format reader. +/// +/// This function handles the complex process of decoder recreation when Symphonia +/// determines that stream parameters have changed significantly enough to require +/// a complete decoder reset. It implements intelligent track selection and error +/// recovery strategies. +/// +/// # Arguments +/// +/// * `format` - Mutable reference to the format reader/demuxer +/// * `decoder` - Mutable reference to the current decoder (will be replaced) +/// * `current_track_id` - Optional current track ID for track switching logic +/// * `spec` - Optional signal specification to update during recreation +/// +/// # Returns +/// +/// - `Ok(track_id)` - ID of the newly selected track +/// * `Err(Error)` - If no suitable track found or decoder creation failed +/// +/// # Track Selection Strategy +/// +/// The function implements different strategies based on context: +/// - **Initialization**: Selects first supported track (current_track_id is None) +/// - **During playback**: Attempts to find next supported track after current one +/// - **No fallback during playback**: Prevents unexpected track jumping +/// +/// # Decoder Recreation Process +/// +/// 1. **Track selection**: Find appropriate audio track with supported codec +/// 2. **Decoder creation**: Create new decoder for selected track +/// 3. **Specification update**: Update signal spec if provided +/// 4. **State consistency**: Ensure new decoder is properly initialized +/// +/// # Error Handling +/// +/// The function handles various error scenarios: +/// - **No supported tracks**: Returns appropriate error +/// - **Codec creation failure**: Propagates codec errors +/// - **Track not found**: Handles missing track scenarios +/// - **Specification updates**: Updates spec when track parameters available +/// +/// # Performance Considerations +/// +/// Decoder recreation is an expensive operation that involves: +/// - Track list analysis +/// - Codec instantiation +/// - Parameter validation +/// - State initialization +/// +/// It should be used sparingly and only when required by Symphonia. +fn recreate_decoder( + format: &mut Box, + decoder: &mut Box, + current_track_id: Option, + spec: Option<&mut SignalSpec>, +) -> Result { + let track = if let Some(current_id) = current_track_id { + // During playback: find the next supported track after the current one + let tracks = format.tracks(); + let current_index = tracks.iter().position(|t| t.id == current_id); + + if let Some(idx) = current_index { + // Look for the next supported track after current index + tracks + .iter() + .skip(idx + 1) + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + } else { + // Current track not found in tracks list + None + } + // Note: No fallback during playback - if we can't find next track, stop playing + } else { + // Initialization case: find first supported track + format + .tracks() + .iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + } + .ok_or(Error::Unsupported( + "No supported track found after current track", + ))?; + + let new_track_id = track.id; + + // Create new decoder + *decoder = + symphonia::default::get_codecs().make(&track.codec_params, &DecoderOptions::default())?; + + // Update spec if provided - this will be refined on next successful decode + if let Some(spec) = spec { + if let Some(sample_rate) = track.codec_params.sample_rate { + if let Some(channels) = track.codec_params.channels { + *spec = SignalSpec::new(sample_rate, channels); + } + } + } + + Ok(new_track_id) +} + +/// Determines whether to continue decoding after a decode error. +/// +/// This function implements Symphonia's error handling recommendations, +/// classifying errors into recoverable and terminal categories. It enables +/// robust audio playback by gracefully handling common error conditions. +/// +/// # Arguments +/// +/// * `error` - The Symphonia error that occurred during decoding +/// * `decoder` - Mutable reference to the decoder (may be reset) +/// +/// # Returns +/// +/// - `true` if decoding should continue with the next packet +/// - `false` if the error is terminal and decoding should stop +/// +/// # Error Classification +/// +/// - **Recoverable errors**: Can be handled by skipping the problematic packet +/// - `DecodeError`: Corrupted or malformed packet data +/// - `IoError`: Temporary I/O issues during packet reading +/// - `ResetRequired`: Decoder parameter changes (handled with reset) +/// - **Terminal errors**: Indicate unrecoverable conditions +/// - `Unsupported`: Codec or format not supported +/// - `LimitError`: Resource or format limits exceeded +/// - Other unspecified errors +/// +/// # Decoder Reset Handling +/// +/// When `ResetRequired` is encountered, the function automatically resets +/// the decoder state to handle parameter changes. This is essential for +/// maintaining audio quality across parameter transitions. +/// +/// # Performance Impact +/// +/// Error handling is designed to be lightweight: +/// - Quick error classification +/// - Minimal decoder state changes +/// - Efficient recovery strategies +/// - No unnecessary processing for terminal errors +/// +/// # Robustness Strategy +/// +/// The function implements a conservative approach: +/// - Favor continuation when safe +/// - Reset decoder state when needed +/// - Terminate only on truly unrecoverable errors +/// - Preserve audio quality over maximum compatibility +fn should_continue_on_decode_error( + error: &symphonia::core::errors::Error, + decoder: &mut Box, +) -> bool { + match error { + // If a `DecodeError` or `IoError` is returned, the packet is + // undecodeable and should be discarded. Decoding may be continued + // with the next packet. + Error::DecodeError(_) | Error::IoError(_) => true, + + // If `ResetRequired` is returned, consumers of the decoded audio data + // should expect the duration and `SignalSpec` of the decoded audio + // buffer to change. + Error::ResetRequired => { + decoder.reset(); + true + } + + // All other errors are unrecoverable. + _ => false, + } +} + +impl From for SymphoniaSeekMode { + /// Converts Rodio's SeekMode to Symphonia's SeekMode. + /// + /// This conversion maps Rodio's seeking preferences to Symphonia's + /// internal seeking modes, enabling consistent seeking behavior + /// across different audio processing layers. + /// + /// # Mapping + /// + /// - `SeekMode::Fastest` → `SymphoniaSeekMode::Coarse` + /// - Prioritizes speed over precision + /// - Uses keyframe-based seeking when available + /// - Suitable for user scrubbing and fast navigation + /// - `SeekMode::Nearest` → `SymphoniaSeekMode::Accurate` + /// - Prioritizes precision over speed + /// - Attempts sample-accurate positioning + /// - Suitable for gapless playback and precise positioning + /// + /// # Performance Implications + /// + /// The choice between modes affects performance significantly: + /// - **Coarse**: Fast seeks but may require fine-tuning + /// - **Accurate**: Slower seeks but precise positioning + /// + /// # Format Compatibility + /// + /// Not all formats support both modes equally: + /// - Some formats only implement one mode effectively + /// - Automatic fallbacks may occur when preferred mode unavailable + /// - Mode availability may depend on stream characteristics + fn from(mode: SeekMode) -> Self { + match mode { + SeekMode::Fastest => SymphoniaSeekMode::Coarse, + SeekMode::Nearest => SymphoniaSeekMode::Accurate, + } } } diff --git a/src/decoder/utils.rs b/src/decoder/utils.rs new file mode 100644 index 00000000..40ced726 --- /dev/null +++ b/src/decoder/utils.rs @@ -0,0 +1,215 @@ +//! Common utilities and helper functions for decoder implementations. +//! +//! This module provides shared functionality to reduce code duplication across +//! different decoder implementations. It contains generic algorithms and utilities +//! that are format-agnostic and can be safely reused across multiple audio formats. +//! +//! # Purpose +//! +//! The utilities in this module serve several key purposes: +//! - **Code reuse**: Eliminate duplication of common patterns across decoders +//! - **Consistency**: Ensure uniform behavior for similar operations +//! - **Performance**: Provide optimized implementations of common algorithms +//! - **Maintainability**: Centralize common logic for easier maintenance +//! +//! # Categories +//! +//! The utilities are organized into functional categories: +//! - **Duration calculations**: Converting sample counts to time durations +//! - **Format probing**: Safe format detection with stream position restoration +//! - **Mathematical operations**: Sample rate and timing calculations +//! +//! # Design Principles +//! +//! All utilities follow these design principles: +//! - **Zero overhead**: Inline functions where appropriate for performance +//! - **Safety first**: Handle edge cases like zero sample rates gracefully +//! - **Stream preservation**: Always restore stream positions after probing +//! - **Format agnostic**: Work with any audio format without assumptions + +#[cfg(any(feature = "claxon", feature = "hound"))] +use std::time::Duration; + +#[cfg(any( + feature = "claxon", + feature = "hound", + feature = "lewton", + feature = "minimp3", +))] +use std::io::{Read, Seek, SeekFrom}; + +/// Converts sample count and sample rate to precise duration. +/// +/// This function calculates the exact duration represented by a given number of +/// audio samples at a specific sample rate. It provides nanosecond precision +/// by properly handling the fractional component of the division. +/// +/// # Arguments +/// +/// * `samples` - Number of audio samples (typically frames × channels) +/// * `sample_rate` - Sample rate in Hz +/// +/// # Returns +/// +/// A `Duration` representing the exact time span of the samples +/// +/// # Precision +/// +/// The calculation provides nanosecond precision by: +/// 1. Computing whole seconds from the sample count +/// 2. Converting remainder samples to nanoseconds +/// 3. Properly scaling based on the sample rate +/// +/// # Edge Cases +/// +/// - **Zero sample rate**: Returns `Duration::ZERO` to prevent division by zero +/// - **Zero samples**: Returns `Duration::ZERO` (mathematically correct) +/// - **Large values**: Handles overflow gracefully within Duration limits +/// +/// # Examples +/// +/// ```ignore +/// use std::time::Duration; +/// # use rodio::decoder::utils::samples_to_duration; +/// +/// // 1 second at 44.1kHz +/// assert_eq!(samples_to_duration(44100, 44100), Duration::from_secs(1)); +/// +/// // 0.5 seconds at 44.1kHz +/// assert_eq!(samples_to_duration(22050, 44100), Duration::from_millis(500)); +/// ``` +/// +/// # Performance +/// +/// This function is optimized for common audio sample rates and performs +/// integer arithmetic only, making it suitable for real-time applications. +#[cfg(any(feature = "claxon", feature = "hound",))] +pub(super) fn samples_to_duration(samples: u64, sample_rate: u64) -> Duration { + if sample_rate == 0 { + return Duration::ZERO; + } + + let secs = samples / sample_rate; + let nanos = ((samples % sample_rate) * 1_000_000_000) / sample_rate; + Duration::new(secs, nanos as u32) +} + +/// Safe format detection with automatic stream position restoration. +/// +/// This utility provides a standardized pattern for format detection that ensures +/// the stream position is always restored regardless of the probe outcome. This is +/// essential for format detection chains where multiple decoders attempt to identify +/// the format sequentially. +/// +/// # Algorithm +/// +/// The function follows this sequence: +/// 1. **Save position**: Record current stream position +/// 2. **Probe format**: Execute the provided probe function +/// 3. **Restore position**: Return stream to original position +/// 4. **Return result**: Pass through the probe function's result +/// +/// # Arguments +/// +/// * `data` - Mutable reference to the stream to probe +/// * `probe_fn` - Function that attempts format detection and returns success/failure +/// +/// # Returns +/// +/// The boolean result from the probe function, indicating whether the format +/// was successfully detected +/// +/// # Guarantees +/// +/// - **Position restoration**: Stream position is always restored, even if probe panics +/// - **No side effects**: Stream state is unchanged after the call +/// - **Error handling**: Gracefully handles streams that don't support position queries +/// +/// # Examples +/// +/// ```ignore +/// use std::fs::File; +/// # use rodio::decoder::utils::probe_format; +/// +/// let mut file = File::open("audio.unknown").unwrap(); +/// +/// let is_wav = probe_format(&mut file, |reader| { +/// // Attempt WAV detection logic here +/// reader.read(&mut [0u8; 4]).is_ok() // Simplified example +/// }); +/// +/// // File position is restored, ready for next probe +/// ``` +/// +/// # Error Handling +/// +/// If the stream doesn't support position queries, the function defaults to +/// position 0, which is suitable for most format detection scenarios. Seek +/// failures during restoration are ignored to prevent probe failures from +/// affecting the detection process. +/// +/// # Performance +/// +/// This function has minimal overhead, performing only position save/restore +/// operations around the actual probe logic. The cost is dominated by the +/// probe function implementation. +#[cfg(any( + feature = "claxon", + feature = "hound", + feature = "lewton", + feature = "minimp3", +))] +pub(super) fn probe_format(data: &mut R, probe_fn: F) -> bool +where + R: Read + Seek, + F: FnOnce(&mut R) -> bool, +{ + let original_pos = data.stream_position().unwrap_or_default(); + let result = probe_fn(data); + let _ = data.seek(SeekFrom::Start(original_pos)); + result +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Tests for the samples_to_duration function. + /// + /// These tests verify correct duration calculation across various scenarios + /// including edge cases and common audio configurations. + #[cfg(any(feature = "hound", feature = "claxon"))] + #[test] + fn test_samples_to_duration() { + // Standard CD quality: 1 second at 44.1kHz + assert_eq!(samples_to_duration(44100, 44100), Duration::from_secs(1)); + + // Half second at CD quality + assert_eq!( + samples_to_duration(22050, 44100), + Duration::from_millis(500) + ); + + // Professional audio: 1 second at 48kHz + assert_eq!(samples_to_duration(48000, 48000), Duration::from_secs(1)); + + // High resolution: 1 second at 96kHz + assert_eq!(samples_to_duration(96000, 96000), Duration::from_secs(1)); + + // Edge case: Zero samples should return zero duration + assert_eq!(samples_to_duration(0, 44100), Duration::ZERO); + + // Edge case: Zero sample rate should not panic and return zero + assert_eq!(samples_to_duration(44100, 0), Duration::ZERO); + + // Precision test: Fractional milliseconds + // 441 samples at 44.1kHz = 10ms exactly + assert_eq!(samples_to_duration(441, 44100), Duration::from_millis(10)); + + // Very small durations should have nanosecond precision + // 1 sample at 44.1kHz ≈ 22.676 microseconds + let one_sample_duration = samples_to_duration(1, 44100); + assert!(one_sample_duration.as_nanos() > 22000); + assert!(one_sample_duration.as_nanos() < 23000); + } +} diff --git a/src/decoder/vorbis.rs b/src/decoder/vorbis.rs index 8d81fbf5..0f2ba663 100644 --- a/src/decoder/vorbis.rs +++ b/src/decoder/vorbis.rs @@ -1,60 +1,454 @@ -use std::io::{Read, Seek, SeekFrom}; -use std::time::Duration; +//! Ogg Vorbis audio decoder implementation. +//! +//! This module provides Ogg Vorbis decoding capabilities using the `lewton` library. +//! Vorbis is a lossy audio compression format contained within Ogg containers, designed +//! for high-quality audio compression with lower bitrates than MP3. +//! +//! # Features +//! +//! - **Quality**: High-quality lossy compression with advanced psychoacoustic modeling +//! - **Bitrates**: Variable bitrate encoding optimized for quality per bit +//! - **Sample rates**: 8kHz to 192kHz (commonly 44.1kHz and 48kHz) +//! - **Channels**: Mono to 8-channel surround sound support +//! - **Seeking**: Granule-based seeking with binary search optimization +//! - **Duration**: Calculated via last granule position scanning +//! - **Streaming**: Supports chained Ogg streams with parameter changes +//! +//! # Limitations +//! +//! - No bit depth detection (lossy format with floating-point processing) +//! - Duration calculation requires scanning to last granule position +//! - Seeking accuracy depends on granule position availability +//! - Forward-only seeking without `is_seekable` setting +//! +//! # Configuration +//! +//! The decoder can be configured through `DecoderBuilder`: +//! - `with_seekable(true)` - Enable backward seeking with granule search +//! - `with_scan_duration(true)` - Enable duration scanning (requires `byte_len`) +//! - `with_total_duration(dur)` - Provide known duration to skip scanning +//! - `with_seek_mode(SeekMode::Fastest)` - Use granule-based seeking for speed +//! - `with_seek_mode(SeekMode::Nearest)` - Use linear seeking for accuracy +//! +//! # Performance Notes +//! +//! - Duration scanning uses binary search optimization for large files +//! - Granule-based seeking provides O(log n) performance vs. O(n) linear +//! - Variable packet sizes require careful buffer management +//! - Chained streams may cause parameter changes requiring adaptation +//! +//! # Example +//! +//! ```ignore +//! use std::fs::File; +//! use rodio::{Decoder, decoder::builder::SeekMode}; +//! +//! let file = File::open("audio.ogg").unwrap(); +//! let decoder = Decoder::builder() +//! .with_data(file) +//! .with_seekable(true) +//! .with_scan_duration(true) +//! .with_seek_mode(SeekMode::Fastest) +//! .build() +//! .unwrap(); +//! +//! // Vorbis format doesn't support bit depth detection +//! assert_eq!(decoder.bits_per_sample(), None); +//! ``` -use crate::source::SeekError; -use crate::Source; +use std::{ + io::{Read, Seek, SeekFrom}, + num::NonZero, + sync::Arc, + time::Duration, +}; -use crate::common::{ChannelCount, Sample, SampleRate}; -use lewton::inside_ogg::OggStreamReader; -use lewton::samples::InterleavedSamples; +use lewton::{ + audio::AudioReadError::AudioIsHeader, + inside_ogg::OggStreamReader, + samples::InterleavedSamples, + OggReadError::NoCapturePatternFound, + VorbisError::{BadAudio, OggError}, +}; -/// Decoder for an OGG file that contains Vorbis sound format. +use super::{utils, Settings}; +use crate::{ + common::{ChannelCount, Sample, SampleRate}, + decoder::builder::SeekMode, + source::SeekError, + Source, +}; + +/// Decoder for Ogg Vorbis format using the `lewton` library. +/// +/// Provides high-quality lossy audio decoding with granule-based seeking and duration +/// calculation through Ogg stream analysis. The decoder handles variable packet sizes +/// efficiently and supports chained Ogg streams with parameter changes. +/// +/// # Granule-based Architecture +/// +/// Vorbis uses granule positions as timing references, where each granule represents +/// a sample position in the decoded audio stream. This enables precise seeking and +/// duration calculation without requiring constant bitrate assumptions. +/// +/// # Packet Processing +/// +/// Ogg Vorbis audio is organized into variable-size packets containing compressed +/// audio data. Each packet decodes to a variable number of samples, requiring +/// dynamic buffer management for efficient sample-by-sample iteration. +/// +/// # Seeking Strategies +/// +/// The decoder implements two seeking approaches: +/// - **Granule seeking**: Fast binary search using lewton's native mechanism +/// - **Linear seeking**: Sample-accurate positioning via forward iteration +/// +/// # Stream Chaining +/// +/// Ogg supports chained streams where multiple Vorbis streams are concatenated. +/// The decoder adapts to parameter changes (sample rate, channels) between streams. +/// +/// # Thread Safety +/// +/// This decoder is not thread-safe. Create separate instances for concurrent access +/// or use appropriate synchronization primitives. +/// +/// # Generic Parameters +/// +/// * `R` - The underlying data source type, must implement `Read + Seek` pub struct VorbisDecoder where R: Read + Seek, { - stream_reader: OggStreamReader, - current_data: Vec, - next: usize, + /// The underlying lewton Ogg stream reader, wrapped for seeking operations. + /// + /// Temporarily set to `None` during stream reset operations for linear seeking. + /// Always `Some` during normal operation and iteration. + stream_reader: Option>, + + /// Current decoded audio packet data. + /// + /// Contains interleaved PCM samples from the current Vorbis packet. `None` indicates + /// either stream exhaustion or that a new packet needs to be decoded. Packet sizes + /// vary based on audio content and encoder settings. + current_data: Option>, + + /// Current position within the current packet. + /// + /// Tracks the next sample index to return from the current packet's data. + /// When this reaches the packet's sample count, a new packet must be decoded. + current_data_offset: usize, + + /// Total duration calculated from last granule position. + /// + /// Calculated by scanning to the final granule position in the stream or + /// provided explicitly via settings. For chained streams, represents the + /// total duration across all chains. + total_duration: Option, + + /// Total number of audio samples (estimated from duration). + /// + /// Calculated from total duration and stream parameters when available. + /// Represents total interleaved samples across all channels and chains. + total_samples: Option, + + /// Number of samples read so far (for seeking calculations). + /// + /// Tracks the current playback position in total samples (across all channels). + /// Used to determine if seeking requires stream reset or can skip forward. + samples_read: u64, + + /// Seeking precision mode. + /// + /// Controls the trade-off between seeking speed and accuracy: + /// - `Fastest`: Granule-based seeking using lewton's binary search + /// - `Nearest`: Linear seeking for sample-accurate positioning + seek_mode: SeekMode, + + /// Whether random access seeking is enabled. + /// + /// When `true`, enables backward seeking by allowing stream reset operations. + /// When `false`, only forward seeking (sample skipping) is allowed. + is_seekable: bool, } impl VorbisDecoder where R: Read + Seek, { - /// Attempts to decode the data as ogg/vorbis. - pub fn new(mut data: R) -> Result, R> { - if !is_vorbis(data.by_ref()) { + /// Attempts to decode the data as Ogg Vorbis with default settings. + /// + /// This method probes the input data to detect Ogg Vorbis format and initializes + /// the decoder if successful. Uses default settings with no seeking support or + /// duration scanning enabled. + /// + /// # Arguments + /// + /// * `data` - Input stream implementing `Read + Seek` + /// + /// # Returns + /// + /// - `Ok(VorbisDecoder)` if the data contains valid Ogg Vorbis format + /// - `Err(R)` if the data is not Ogg Vorbis, returning the original stream + /// + /// # Examples + /// + /// ```ignore + /// use std::fs::File; + /// use rodio::decoder::vorbis::VorbisDecoder; + /// + /// let file = File::open("audio.ogg").unwrap(); + /// match VorbisDecoder::new(file) { + /// Ok(decoder) => println!("Vorbis decoder created"), + /// Err(file) => println!("Not an Ogg Vorbis file"), + /// } + /// ``` + /// + /// # Performance + /// + /// This method performs format detection which requires parsing Ogg headers + /// and Vorbis identification. The stream position is restored if detection fails, + /// so the original stream can be used for other format detection attempts. + #[allow(dead_code)] + pub fn new(data: R) -> Result { + Self::new_with_settings(data, &Settings::default()) + } + + /// Attempts to decode the data as Ogg Vorbis with custom settings. + /// + /// This method provides full control over decoder configuration including seeking + /// behavior, duration calculation, and performance optimizations. It performs format + /// detection, analyzes stream characteristics, and optionally scans for accurate + /// duration information. + /// + /// # Arguments + /// + /// * `data` - Input stream implementing `Read + Seek` + /// * `settings` - Configuration settings from `DecoderBuilder` + /// + /// # Returns + /// + /// - `Ok(VorbisDecoder)` if the data contains valid Ogg Vorbis format + /// - `Err(R)` if the data is not Ogg Vorbis, returning the original stream + /// + /// # Settings Usage + /// + /// - `is_seekable`: Enables backward seeking operations + /// - `scan_duration`: Enables granule position scanning (requires `byte_len`) + /// - `total_duration`: Provides known duration to skip scanning + /// - `seek_mode`: Controls seeking accuracy vs. speed trade-off + /// - `byte_len`: Total file size used for duration scanning optimization + /// + /// # Examples + /// + /// ```ignore + /// use std::fs::File; + /// use std::time::Duration; + /// use rodio::decoder::{vorbis::VorbisDecoder, Settings, builder::SeekMode}; + /// + /// let file = File::open("audio.ogg").unwrap(); + /// let mut settings = Settings::default(); + /// settings.is_seekable = true; + /// settings.scan_duration = true; + /// settings.seek_mode = SeekMode::Fastest; + /// + /// let decoder = VorbisDecoder::new_with_settings(file, &settings).unwrap(); + /// ``` + /// + /// # Performance + /// + /// - Duration scanning uses binary search optimization for large files + /// - Stream initialization requires parsing Vorbis headers and identification + /// - First packet decoding provides immediate stream characteristics + /// + /// # Panics + /// + /// Panics if the Ogg Vorbis stream has invalid characteristics (zero channels or + /// zero sample rate). This should never happen with valid Vorbis data that passes + /// format detection. + pub fn new_with_settings(mut data: R, settings: &Settings) -> Result { + if !is_vorbis(&mut data) { return Err(data); } - let stream_reader = OggStreamReader::new(data).expect("should still be vorbis"); - Ok(Self::from_stream_reader(stream_reader)) - } - pub fn from_stream_reader(mut stream_reader: OggStreamReader) -> Self { - let mut data = match stream_reader.read_dec_packet_generic::>() { - Ok(Some(d)) => d.samples, - _ => Vec::new(), + // Calculate total duration using the new settings approach (before consuming data) + let last_granule = if settings.total_duration.is_some() { + None // Use provided duration directly + } else if settings.scan_duration && settings.is_seekable && settings.byte_len.is_some() { + find_last_granule(&mut data, settings.byte_len.unwrap()) // Scan for duration + } else { + None // Either scanning disabled or prerequisites not met }; - // The first packet is always empty, therefore - // we need to read the second frame to get some data - if let Ok(Some(mut d)) = - stream_reader.read_dec_packet_generic::>() - { - data.append(&mut d.samples); - } + let mut stream_reader = OggStreamReader::new(data).expect("should still be vorbis"); + let current_data = read_next_non_empty_packet(&mut stream_reader); - VorbisDecoder { - stream_reader, - current_data: data, - next: 0, - } + let sample_rate = NonZero::new(stream_reader.ident_hdr.audio_sample_rate) + .expect("vorbis has non-zero sample rate"); + let channels = stream_reader.ident_hdr.audio_channels; + + let total_duration = settings + .total_duration + .or_else(|| last_granule.map(|granule| granules_to_duration(granule, sample_rate))); + + let total_samples = total_duration.map(|dur| { + let total_secs = dur.as_secs_f64(); + (total_secs * sample_rate.get() as f64 * channels as f64).ceil() as u64 + }); + + Ok(Self { + stream_reader: Some(stream_reader), + current_data, + current_data_offset: 0, + total_duration, + total_samples, + samples_read: 0, + seek_mode: settings.seek_mode, + is_seekable: settings.is_seekable, + }) } + /// Consumes the decoder and returns the underlying Ogg stream reader. + /// + /// This can be useful for accessing the underlying lewton stream reader directly + /// or when the decoder needs to be replaced. The reader will be positioned at + /// the current playback location. + /// + /// # Examples + /// + /// ```ignore + /// use std::fs::File; + /// use rodio::decoder::vorbis::VorbisDecoder; + /// + /// let file = File::open("audio.ogg").unwrap(); + /// let decoder = VorbisDecoder::new(file).unwrap(); + /// let stream_reader = decoder.into_inner(); + /// ``` + /// + /// # Panics + /// + /// Panics if called during a seeking operation when the stream reader is + /// temporarily `None`. This should never happen during normal usage. #[inline] pub fn into_inner(self) -> OggStreamReader { self.stream_reader + .expect("stream_reader should always be Some") + } + + /// Performs linear seeking by iteration through samples. + /// + /// This method provides sample-accurate seeking by linearly consuming samples + /// until the target position is reached. It's slower than granule-based seeking + /// but guarantees precise positioning. + /// + /// # Arguments + /// + /// * `target_granule_pos` - Target granule position (per-channel sample number) + /// + /// # Returns + /// + /// The number of samples to skip to reach exact channel alignment after seeking + /// + /// # Errors + /// + /// - `SeekError::IoError` - I/O error during stream reset operations + /// + /// # Performance + /// + /// - **Forward seeks**: O(n) where n is samples to skip + /// - **Backward seeks**: O(target_position) due to stream reset + /// - **Always accurate**: Guarantees sample-perfect positioning + /// + /// # Implementation + /// + /// For backward seeks, the stream is reset to the beginning and the decoder + /// is reinitialized to ensure consistent state. Forward seeks skip samples + /// from the current position. + fn linear_seek(&mut self, target_granule_pos: u64) -> Result { + let target_samples = target_granule_pos * self.channels().get() as u64; + let current_samples = self.samples_read; + + let samples_to_skip = if target_samples < current_samples { + // Backwards seek: reset to start by recreating stream reader + let mut reader = self + .stream_reader + .take() + .expect("stream_reader should always be Some") + .into_inner(); + + reader.seek_bytes(SeekFrom::Start(0)).map_err(Arc::new)?; + + // Recreate stream reader and reinitialize like a fresh decoder + let mut new_stream_reader = + OggStreamReader::new(reader.into_inner()).map_err(Arc::new)?; + + self.current_data = read_next_non_empty_packet(&mut new_stream_reader); + self.stream_reader = Some(new_stream_reader); + self.current_data_offset = 0; + self.samples_read = 0; + + // Consume exactly target_samples to position at the target + target_samples + } else { + // Forward seek: skip from current position + target_samples - current_samples + }; + + Ok(samples_to_skip) + } + + /// Performs granule-based seeking using lewton's native mechanism. + /// + /// This method uses lewton's built-in binary search algorithm to quickly locate + /// the target granule position. It's faster than linear seeking but may not be + /// sample-accurate due to the granular nature of Ogg page boundaries. + /// + /// # Arguments + /// + /// * `target_granule_pos` - Target granule position (per-channel sample number) + /// + /// # Returns + /// + /// The number of samples to skip to reach exact positioning after coarse seek + /// + /// # Errors + /// + /// - `SeekError::LewtonDecoder` - Lewton decoder error during granule seeking + /// + /// # Performance + /// + /// - **Seeking time**: O(log n) binary search through Ogg pages + /// - **Accuracy**: Positions at or before target granule (requires fine-tuning) + /// - **Optimal for**: Large files with frequent seeking requirements + /// + /// # Implementation + /// + /// Uses lewton's `seek_absgp_pg` function which performs binary search through + /// Ogg pages to find the page containing the target granule position. The + /// decoder may position slightly before the target, requiring sample skipping + /// for exact positioning. + fn granule_seek(&mut self, target_granule_pos: u64) -> Result { + let reader = self + .stream_reader + .as_mut() + .expect("stream_reader should always be Some"); + + // Use lewton's bisection-based granule seeking + reader.seek_absgp_pg(target_granule_pos).map_err(Arc::new)?; + + // Clear buffer - let next() handle loading new packets + self.current_data = None; + + // Update samples_read to reflect approximate new position (interleaved samples) + // In ogg 0.9.2 get_last_absgp always returns 0: https://github.com/RustAudio/ogg/pull/22 + let current_granule_pos = reader.get_last_absgp().unwrap_or(target_granule_pos); + self.samples_read = current_granule_pos * self.channels().get() as u64; + + // lewton does not seek to the exact position, it seeks to a granule position at or before. + let samples_to_skip = + target_granule_pos.saturating_sub(current_granule_pos) * self.channels().get() as u64; + + Ok(samples_to_skip) } } @@ -62,42 +456,249 @@ impl Source for VorbisDecoder where R: Read + Seek, { + /// Returns the number of samples before parameters change. + /// + /// For Ogg Vorbis, this returns `Some(packet_size)` when a packet is available, + /// representing the number of samples in the current packet. Returns `Some(0)` + /// when the stream is exhausted. + /// + /// # Chained Streams + /// + /// Ogg supports chained streams where multiple Vorbis streams are concatenated. + /// When stream parameters change (sample rate, channels), the span length + /// reflects the current stream's characteristics. + /// + /// # Packet Sizes + /// + /// Vorbis packets have variable sizes depending on: + /// - Audio content complexity + /// - Encoder settings and optimization + /// - Bitrate allocation decisions + /// + /// Typical packet sizes range from hundreds to thousands of samples. #[inline] fn current_span_len(&self) -> Option { - Some(self.current_data.len()) + // Chained Ogg streams are supported by lewton, so parameters can change. + // Return current buffer length, or Some(0) when exhausted. + self.current_data + .as_ref() + .map(|data| data.len()) + .or(Some(0)) } + /// Returns the number of audio channels. + /// + /// Ogg Vorbis supports various channel configurations: + /// - 1 channel: Mono + /// - 2 channels: Stereo + /// - 3 channels: Stereo + center + /// - 4 channels: Quadraphonic + /// - 5 channels: 5.0 surround + /// - 6 channels: 5.1 surround + /// - 7 channels: 6.1 surround + /// - 8 channels: 7.1 surround + /// + /// # Chained Streams + /// + /// In chained Ogg streams, channel configuration can change between streams. + /// The decoder adapts to these changes automatically, though this is uncommon + /// in practice. + /// + /// # Guarantees + /// + /// The returned value reflects the current stream's channel configuration and + /// may change during playback if chained streams with different parameters + /// are encountered. #[inline] fn channels(&self) -> ChannelCount { - ChannelCount::new(self.stream_reader.ident_hdr.audio_channels.into()) - .expect("audio should have at least one channel") + ChannelCount::new( + self.stream_reader + .as_ref() + .expect("stream_reader should always be Some") + .ident_hdr + .audio_channels + .into(), + ) + .expect("audio should have at least one channel") } + /// Returns the sample rate in Hz. + /// + /// Ogg Vorbis supports a wide range of sample rates from 8kHz to 192kHz: + /// - **8kHz-16kHz**: Speech and low-quality audio + /// - **22.05kHz**: Low-quality music + /// - **44.1kHz**: CD quality (most common) + /// - **48kHz**: Professional audio standard + /// - **96kHz**: High-resolution audio + /// - **192kHz**: Ultra high-resolution audio + /// + /// # Chained Streams + /// + /// Sample rate can change between chained streams, though this is rare in + /// practice. The decoder handles such changes automatically. + /// + /// # Guarantees + /// + /// The returned value reflects the current stream's sample rate and may change + /// during playback if chained streams with different parameters are encountered. #[inline] fn sample_rate(&self) -> SampleRate { - SampleRate::new(self.stream_reader.ident_hdr.audio_sample_rate) - .expect("audio should always have a non zero SampleRate") + SampleRate::new( + self.stream_reader + .as_ref() + .expect("stream_reader should always be Some") + .ident_hdr + .audio_sample_rate, + ) + .expect("audio should always have a non zero SampleRate") } + /// Returns the total duration of the audio stream. + /// + /// Duration accuracy depends on how it was calculated: + /// - **Provided explicitly**: Most accurate (when available from metadata) + /// - **Granule scanning**: Very accurate, calculated from final granule position + /// - **Not available**: Returns `None` when duration cannot be determined + /// + /// # Granule Position Method + /// + /// The most accurate method scans to the final granule position in the stream, + /// which represents the exact number of decoded samples. This provides + /// sample-accurate duration information. + /// + /// # Chained Streams + /// + /// For chained streams, duration represents the total across all chains. + /// Individual chain durations are not separately tracked. + /// + /// # Availability + /// + /// Duration is available when: + /// 1. Explicitly provided via `total_duration` setting + /// 2. Calculated via granule position scanning (when enabled and prerequisites met) + /// + /// Returns `None` when scanning is disabled or prerequisites are not met. #[inline] fn total_duration(&self) -> Option { - None + self.total_duration } - /// seek is broken, https://github.com/RustAudio/lewton/issues/73. - // We could work around it by: - // - using unsafe to create an instance of Self - // - use mem::swap to turn the &mut self into a mut self - // - take out the underlying Read+Seek - // - make a new self and seek - // - // If this issue is fixed use the implementation in - // commit: 3bafe32388b4eb7a48c6701e6c65044dc8c555e6 + /// Returns the bit depth of the audio samples. + /// + /// Ogg Vorbis is a lossy compression format that doesn't preserve the original + /// bit depth. The decoded output uses floating-point processing internally and + /// is provided as floating-point samples regardless of the original source + /// material's bit depth. + /// + /// # Lossy Compression + /// + /// Unlike lossless formats like FLAC, Vorbis uses advanced psychoacoustic + /// modeling to remove audio information deemed less perceptible, making + /// bit depth information irrelevant for the decoded output. + /// + /// # Always Returns None + /// + /// This method always returns `None` for Ogg Vorbis streams as bit depth is + /// not a meaningful concept for lossy compressed audio formats with + /// floating-point processing. #[inline] - fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> { - Err(SeekError::NotSupported { - underlying_source: std::any::type_name::(), - }) + fn bits_per_sample(&self) -> Option { + None + } + + /// Attempts to seek to the specified position in the audio stream. + /// + /// Ogg Vorbis seeking uses granule positions for precise timing references. + /// The implementation provides both fast granule-based seeking and slower + /// but sample-accurate linear seeking. + /// + /// # Seeking Modes + /// + /// - **`SeekMode::Fastest`**: Uses lewton's granule-based binary search + /// - Fast O(log n) performance for large files + /// - May require fine-tuning for exact positioning + /// - **`SeekMode::Nearest`**: Uses linear sample consumption + /// - Slower O(n) performance but always sample-accurate + /// - Guarantees exact positioning regardless of granule boundaries + /// + /// # Performance Characteristics + /// + /// - **Granule seeks**: Fast for large files, optimal for frequent seeking + /// - **Linear seeks**: Slower but always accurate, good for precise positioning + /// - **Forward seeks**: Efficient skipping from current position + /// - **Backward seeks**: Requires stream reset, then forward positioning + /// + /// # Arguments + /// + /// * `pos` - Target position as duration from stream start + /// + /// # Errors + /// + /// - `SeekError::ForwardOnly` - Backward seek attempted without `is_seekable` + /// - `SeekError::LewtonDecoder` - Lewton decoder error during granule seeking + /// - `SeekError::IoError` - I/O error during stream reset or positioning + /// + /// # Examples + /// + /// ```no_run + /// use std::{fs::File, time::Duration}; + /// use rodio::{Decoder, Source, decoder::builder::SeekMode}; + /// + /// let file = File::open("audio.ogg").unwrap(); + /// let mut decoder = Decoder::builder() + /// .with_data(file) + /// .with_seekable(true) + /// .with_seek_mode(SeekMode::Fastest) + /// .build() + /// .unwrap(); + /// + /// // Fast granule-based seek to 30 seconds + /// decoder.try_seek(Duration::from_secs(30)).unwrap(); + /// ``` + /// + /// # Implementation Details + /// + /// The seeking implementation preserves channel alignment to ensure that seeking + /// to a specific time position results in the correct channel being returned + /// for the first sample after the seek operation. + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + // Seeking should be "saturating", meaning: target positions beyond the end of the stream + // are clamped to the end. + let mut target = pos; + if let Some(total_duration) = self.total_duration() { + if target > total_duration { + target = total_duration; + } + } + + // Remember the current channel position before seeking (for channel order preservation) + let active_channel = self.current_data_offset % self.channels().get() as usize; + + // Convert duration to granule position (per-channel sample number) + // lewton's seek_absgp_pg expects absolute granule position + let target_granule_pos = (target.as_secs_f64() * self.sample_rate().get() as f64) as u64; + let target_sample = target_granule_pos * self.channels().get() as u64; + + let samples_to_skip = if !self.is_seekable { + if target_sample < self.samples_read { + return Err(SeekError::ForwardOnly); + } else { + // Linearly consume samples to reach forward targets + target_sample - self.samples_read + } + } else if self.seek_mode == SeekMode::Nearest { + self.linear_seek(target_granule_pos)? + } else { + self.granule_seek(target_granule_pos)? + }; + + // After seeking, we're always positioned at the start of an audio frame (channel 0). + // Skip samples to reach the desired channel position. + for _ in 0..(samples_to_skip + active_channel as u64) { + let _ = self.next(); + } + + Ok(()) } } @@ -105,49 +706,409 @@ impl Iterator for VorbisDecoder where R: Read + Seek, { + /// The type of items yielded by the iterator. + /// + /// Returns `Sample` (typically `f32`) values representing individual audio samples. + /// Samples are interleaved across channels in the order: channel 0, channel 1, etc. type Item = Sample; + /// Returns the next audio sample from the Ogg Vorbis stream. + /// + /// This method implements efficient packet-based decoding by maintaining the current + /// decoded Vorbis packet and returning samples one at a time. It automatically decodes + /// new packets as needed and handles various Ogg/Vorbis stream conditions. + /// + /// # Sample Format + /// + /// Vorbis packets are decoded to interleaved PCM samples using lewton's floating-point + /// processing. The samples are provided in Rodio's sample format (typically `f32`) + /// preserving the quality and dynamic range of the decoded audio. + /// + /// # Performance + /// + /// - **Hot path**: Returning samples from current packet (very fast) + /// - **Cold path**: Decoding new packets when buffer is exhausted (slower) + /// + /// # Error Handling + /// + /// The decoder gracefully handles various Ogg/Vorbis stream conditions: + /// - **Header packets**: Automatically skipped during audio playback + /// - **Empty packets**: Ignored, decoder continues to next packet + /// - **Stream errors**: Most errors result in stream termination + /// - **Capture pattern errors**: Handled for robust stream processing + /// + /// # Returns + /// + /// - `Some(sample)` - Next audio sample from the stream + /// - `None` - End of stream reached or unrecoverable decoding error + /// + /// # Channel Order + /// + /// Samples are returned in interleaved order based on Vorbis channel mapping: + /// - **Mono**: [M, M, M, ...] + /// - **Stereo**: [L, R, L, R, ...] + /// - **5.1 Surround**: [FL, FR, C, LFE, BL, BR, FL, FR, C, LFE, BL, BR, ...] #[inline] fn next(&mut self) -> Option { - if let Some(sample) = self.current_data.get(self.next).copied() { - self.next += 1; - if self.current_data.is_empty() { - if let Ok(Some(data)) = self - .stream_reader - .read_dec_packet_generic::>() - { - self.current_data = data.samples; - self.next = 0; - } + // Hot path: read from current buffer if available + if let Some(data) = &self.current_data { + if self.current_data_offset < data.len() { + let sample = data[self.current_data_offset]; + self.current_data_offset += 1; + self.samples_read += 1; + return Some(sample); } - Some(sample) - } else { - if let Ok(Some(data)) = self - .stream_reader - .read_dec_packet_generic::>() - { - self.current_data = data.samples; - self.next = 0; + } + + // Cold path: need to decode next packet + let stream_reader = self + .stream_reader + .as_mut() + .expect("stream_reader should always be Some"); + + if let Some(samples) = read_next_non_empty_packet(stream_reader) { + self.current_data = Some(samples); + self.current_data_offset = 0; + + // Return first sample from new buffer + if let Some(data) = &self.current_data { + let sample = data[0]; + self.current_data_offset = 1; + self.samples_read += 1; + return Some(sample); } - let sample = self.current_data.get(self.next).copied(); - self.next += 1; - sample } + + // Stream exhausted - set buffer to None + self.current_data = None; + None } + /// Returns bounds on the remaining length of the iterator. + /// + /// Provides size estimates based on Ogg Vorbis stream characteristics and current + /// playback position. The accuracy depends on the availability of duration information + /// from granule position scanning or explicit duration settings. + /// + /// # Returns + /// + /// A tuple `(lower_bound, upper_bound)` where: + /// - `lower_bound`: Minimum number of samples guaranteed to be available + /// - `upper_bound`: Maximum number of samples that might be available (None if unknown) + /// + /// # Accuracy Levels + /// + /// - **High accuracy**: When total samples calculated from granule scanning + /// - **Conservative estimate**: When only current packet information available + /// - **Stream exhausted**: (0, Some(0)) when no more data + /// + /// # Implementation + /// + /// The lower bound represents samples currently buffered in the decoded packet. + /// The upper bound uses total sample estimates when available, providing useful + /// information for progress indication and buffer allocation. + /// + /// # Use Cases + /// + /// - **Progress indication**: Upper bound enables percentage calculation + /// - **Buffer allocation**: Lower bound ensures minimum available samples + /// - **End detection**: (0, Some(0)) indicates stream completion + /// + /// # Chained Streams + /// + /// For chained streams, estimates represent the remaining samples across all + /// remaining chains in the file. #[inline] fn size_hint(&self) -> (usize, Option) { - (self.current_data.len(), None) + // Samples already decoded and buffered (guaranteed available) + let buffered_samples = self + .current_data + .as_ref() + .map(|data| data.len().saturating_sub(self.current_data_offset)) + .unwrap_or(0); + + if let Some(total_samples) = self.total_samples { + let total_remaining = total_samples.saturating_sub(self.samples_read) as usize; + (buffered_samples, Some(total_remaining)) + } else if self.current_data.is_none() { + // Stream exhausted + (0, Some(0)) + } else { + (buffered_samples, None) + } + } +} + +/// Reads the next non-empty packet with proper error handling. +/// +/// This function handles the complexity of Ogg Vorbis packet reading, filtering out +/// header packets and empty audio packets while gracefully handling various error +/// conditions that can occur during stream processing. +/// +/// # Arguments +/// +/// * `stream_reader` - Mutable reference to the lewton OggStreamReader +/// +/// # Returns +/// +/// - `Some(samples)` - Vector of interleaved audio samples from the next valid packet +/// - `None` - Stream exhausted or unrecoverable error occurred +/// +/// # Error Handling +/// +/// The function handles several error conditions gracefully: +/// - **Header packets**: Skipped automatically (not audio data) +/// - **Capture pattern errors**: Ignored for robustness during seeking +/// - **Empty packets**: Skipped to find packets with actual audio +/// - **Terminal errors**: Result in stream termination +/// +/// # Performance +/// +/// This function optimizes for the common case of valid audio packets while +/// providing robust error recovery for edge cases and stream boundary conditions. +fn read_next_non_empty_packet( + stream_reader: &mut OggStreamReader, +) -> Option> { + loop { + match stream_reader.read_dec_packet_generic::>() { + Ok(Some(packet)) => { + // Only accept packets with actual audio samples + if !packet.samples.is_empty() { + return Some(packet.samples); + } + // Empty packet - continue to next one + continue; + } + Ok(None) => { + // Stream exhausted + return None; + } + + // Ignore header-related errors and continue + Err(BadAudio(AudioIsHeader)) => continue, + Err(OggError(NoCapturePatternFound)) => continue, + + // All other errors are terminal + Err(_) => return None, + } + } +} + +/// Finds the last granule position in an Ogg Vorbis stream using optimized scanning. +/// +/// This function implements an efficient algorithm to locate the final granule position +/// in an Ogg stream, which represents the total number of audio samples. It uses binary +/// search optimization to minimize I/O operations for large files. +/// +/// # Arguments +/// +/// * `data` - Mutable reference to the input stream to scan +/// * `byte_len` - Total file size used for binary search optimization +/// +/// # Returns +/// +/// - `Some(granule_pos)` - The final granule position if found +/// - `None` - If scanning failed or no valid granule positions found +/// +/// # Algorithm +/// +/// 1. **Binary search phase**: Quickly locate a region containing granule positions +/// 2. **Linear scan phase**: Thoroughly scan from the optimized start position +/// 3. **Position restoration**: Return stream to original position +/// +/// # Performance +/// +/// - **Large files**: Significantly faster than linear scanning +/// - **Small files**: Minimal overhead compared to linear scanning +/// - **I/O optimization**: Reduces read operations through intelligent positioning +/// +/// # Implementation Details +/// +/// The binary search phase stops when the search range is smaller than 4KB, at which +/// point a final linear scan ensures all granule positions are found. The packet +/// limit during binary search prevents excessive scanning in dense regions. +fn find_last_granule(data: &mut R, byte_len: u64) -> Option { + // Save current position + let original_pos = data.stream_position().unwrap_or_default(); + let _ = data.rewind(); + + // Binary search through byte positions to find optimal start position + let mut left = 0; + let mut right = byte_len; + let mut best_start_position = 0; + while right - left > 4096 { + // Stop when range is small enough + let mid = left + (right - left) / 2; + + // Try to find a granule from this position (limited packet scan during binary search) + match find_granule_from_position(data, mid, Some(50)) { + Some(_granule) => { + // Found a granule, this means there's content at or after this position + best_start_position = mid; + left = mid; // Search in the right half + } + None => { + // No granule found, search in the left half + right = mid; + } + } } + + // Now do the final linear scan from the optimized start position (no packet limit) + let result = find_granule_from_position(data, best_start_position, None); + + // Restore original position + let _ = data.seek(SeekFrom::Start(original_pos)); + + result } -/// Returns true if the stream contains Vorbis data, then resets it to where it was. -fn is_vorbis(mut data: R) -> bool +/// Finds granule positions by scanning forward from a specific byte position. +/// +/// This function scans forward through Ogg packets from a given byte position to find +/// valid granule positions. It's used both during binary search optimization and for +/// final linear scanning to locate the last granule position. +/// +/// # Arguments +/// +/// * `data` - The data source to read from +/// * `start_pos` - Starting byte position in the file +/// * `max_packets` - Maximum packets to read before giving up (None = scan until end) +/// +/// # Returns +/// +/// - `Some(granule_pos)` - The last valid granule position found in the scan +/// - `None` - If no valid granule positions were found or I/O error occurred +/// +/// # Packet Limit Rationale +/// +/// When used during binary search, the packet limit prevents excessive scanning: +/// - **Typical Ogg pages**: Contain 1-10 packets depending on content +/// - **50 packet limit**: Covers roughly 5-50 pages (~20-400KB depending on bitrate) +/// - **Balance**: Finding granules quickly vs. avoiding excessive I/O during binary search +/// - **Final scan**: No limit ensures complete coverage from optimized position +/// +/// # Granule Position Validation +/// +/// The function validates granule positions by: +/// - Checking for the "unset" marker (0xFFFFFFFFFFFFFFFF) +/// - Ensuring positions are greater than 0 +/// - Only considering end-of-page packets for granule information +/// +/// # Performance +/// +/// Scanning performance depends on: +/// - **Bitrate**: Lower bitrates have larger packets, fewer reads needed +/// - **Content**: Complex audio may have more variable packet sizes +/// - **Position**: Later positions in file may scan less data +fn find_granule_from_position( + data: &mut R, + start_pos: u64, + max_packets: Option, +) -> Option { + if data.seek(SeekFrom::Start(start_pos)).is_err() { + return None; + } + + let mut packet_reader = ogg::PacketReader::new(data.by_ref()); + let mut last_granule = None; + let mut packets_read = 0; + + // Scan forward from start position to find granules + while let Ok(Some(packet)) = packet_reader.read_packet() { + if packet.last_in_page() { + let granule = packet.absgp_page(); + // Check if granule position is valid (not unset marker and greater than 0) + // 0xFFFFFFFFFFFFFFFF is the "unset" marker in Ogg specification + if granule != 0xFFFFFFFFFFFFFFFF && granule > 0 { + last_granule = Some(granule); + } + } + + packets_read += 1; + + // Stop if we've hit the packet limit (used during binary search) + if let Some(max) = max_packets { + if packets_read >= max { + break; + } + } + } + + last_granule +} + +/// Calculates duration from granule position and sample rate. +/// +/// This function converts a granule position (which represents the total number of +/// audio samples in a Vorbis stream) to a precise duration value. It provides +/// sample-accurate timing information for duration calculation and seeking operations. +/// +/// # Arguments +/// +/// * `granules` - The granule position representing total audio samples +/// * `sample_rate` - The sample rate of the audio stream +/// +/// # Returns +/// +/// A `Duration` representing the exact time corresponding to the granule position +/// +/// # Precision +/// +/// The calculation provides nanosecond precision by: +/// 1. Calculating whole seconds from sample count +/// 2. Computing remainder samples for sub-second precision +/// 3. Converting remainder to nanoseconds based on sample rate +/// +/// # Implementation +/// +/// This is used specifically for Ogg-based formats where granule position +/// represents the total number of samples, providing more accurate timing +/// than bitrate-based estimations. +fn granules_to_duration(granules: u64, sample_rate: SampleRate) -> Duration { + let sample_rate = sample_rate.get() as u64; + let secs = granules / sample_rate; + let nanos = ((granules % sample_rate) * 1_000_000_000) / sample_rate; + Duration::new(secs, nanos as u32) +} + +/// Probes input data to detect Ogg Vorbis format. +/// +/// This function attempts to initialize a lewton OggStreamReader to determine if the +/// data contains a valid Ogg Vorbis stream. The stream position is restored regardless +/// of the result, making it safe to use for format detection. +/// +/// # Arguments +/// +/// * `data` - Mutable reference to the input stream to probe +/// +/// # Returns +/// +/// - `true` if the data appears to contain a valid Ogg Vorbis stream +/// - `false` if the data is not Ogg Vorbis or is corrupted +/// +/// # Implementation +/// +/// Uses the common `utils::probe_format` helper which: +/// 1. Saves the current stream position +/// 2. Attempts Ogg Vorbis detection using `lewton::OggStreamReader` +/// 3. Restores the original stream position +/// 4. Returns the detection result +/// +/// # Performance +/// +/// This function reads the minimum amount of data needed to identify the Ogg +/// container format and Vorbis codec headers, making it efficient for format +/// detection in multi-format scenarios. +/// +/// # Robustness +/// +/// The detection uses actual stream reader initialization rather than just header +/// checking, providing reliable format identification with proper Ogg/Vorbis +/// validation at the cost of slightly higher computational overhead. +fn is_vorbis(data: &mut R) -> bool where R: Read + Seek, { - let stream_pos = data.stream_position().unwrap_or_default(); - let result = OggStreamReader::new(data.by_ref()).is_ok(); - let _ = data.seek(SeekFrom::Start(stream_pos)); - result + utils::probe_format(data, |reader| OggStreamReader::new(reader).is_ok()) } diff --git a/src/decoder/wav.rs b/src/decoder/wav.rs index 5fbd08c3..fbbdb9e7 100644 --- a/src/decoder/wav.rs +++ b/src/decoder/wav.rs @@ -1,55 +1,276 @@ -use std::io::{Read, Seek, SeekFrom}; -use std::sync::Arc; -use std::time::Duration; +//! WAV audio decoder implementation. +//! +//! This module provides WAV decoding capabilities using the `hound` library. WAV is an +//! uncompressed audio format that stores PCM audio data with various bit depths and sample rates. +//! The format uses the RIFF (Resource Interchange File Format) container with WAVE chunks +//! containing audio metadata and raw PCM samples. +//! +//! # Features +//! +//! - **Bit depths**: Full support for 8, 16, 24, and 32-bit integer PCM plus 32-bit float +//! - **Sample rates**: Supports all WAV-compatible sample rates (typically 8kHz to 192kHz) +//! - **Channels**: Supports mono, stereo, and multi-channel audio up to system limits +//! - **Seeking**: Fast random access seeking with sample-accurate positioning +//! - **Duration**: Instant duration calculation from WAV header information +//! - **Performance**: Direct sample access with optimized buffering and no decompression +//! +//! # Advantages +//! +//! - **Zero latency**: No compression/decompression overhead +//! - **Perfect quality**: Lossless storage preserves original audio fidelity +//! - **Fast seeking**: Direct sample access without stream scanning +//! - **Simple format**: Reliable parsing with well-defined structure +//! - **Universal support**: Widely supported across all audio applications +//! +//! # Limitations +//! +//! - No support for compressed WAV variants (ADPCM, μ-law, A-law, etc.) +//! - Forward-only seeking without `is_seekable` setting +//! - Limited to 32-bit sample indexing (4.2 billion samples max) +//! - Large file sizes due to uncompressed storage +//! - No embedded metadata beyond basic audio parameters +//! +//! # Configuration +//! +//! The decoder can be configured through `DecoderBuilder`: +//! - `with_seekable(true)` - Enable random access seeking (recommended for WAV) +//! - Other settings are informational and don't affect WAV decoding performance +//! +//! # Performance Notes +//! +//! - Header parsing is extremely fast (single read operation) +//! - No buffering overhead - samples read directly from file +//! - Seeking operations have O(1) complexity via direct file positioning +//! - Memory usage scales only with iterator state, not file size +//! +//! # Example +//! +//! ```ignore +//! use std::fs::File; +//! use rodio::Decoder; +//! +//! let file = File::open("audio.wav").unwrap(); +//! let decoder = Decoder::builder() +//! .with_data(file) +//! .with_seekable(true) +//! .build() +//! .unwrap(); +//! +//! // WAV supports seeking and bit depth detection +//! println!("Bit depth: {:?}", decoder.bits_per_sample()); +//! println!("Duration: {:?}", decoder.total_duration()); +//! println!("Sample rate: {}", decoder.sample_rate().get()); +//! ``` -use crate::source::SeekError; -use crate::{Sample, Source}; - -use crate::common::{ChannelCount, SampleRate}; +use std::{ + io::{Read, Seek}, + sync::Arc, + time::Duration, +}; use dasp_sample::Sample as _; use dasp_sample::I24; use hound::{SampleFormat, WavReader}; -/// Decoder for the WAV format. +use super::utils; +use crate::{ + common::{ChannelCount, SampleRate}, + decoder::Settings, + source::SeekError, + Sample, Source, +}; + +/// Decoder for the WAV format using the `hound` library. +/// +/// This decoder provides uncompressed PCM audio decoding with fast seeking and instant +/// duration calculation. WAV files contain header information that enables immediate +/// access to format parameters and file length without requiring stream analysis. +/// +/// # RIFF/WAVE Structure +/// +/// WAV files use the RIFF container format with WAVE chunks: +/// - **RIFF header**: File identification and size information +/// - **fmt chunk**: Audio format parameters (sample rate, channels, bit depth) +/// - **data chunk**: Raw PCM audio samples in little-endian format +/// - **Optional chunks**: May contain additional metadata (ignored by decoder) +/// +/// # Sample Format Support +/// +/// The decoder handles various PCM formats seamlessly: +/// - **8-bit integer**: Unsigned values (0-255) converted to signed range +/// - **16-bit integer**: Standard CD-quality signed samples +/// - **24-bit integer**: High-resolution audio packed in 32-bit containers +/// - **32-bit integer**: Maximum precision integer samples +/// - **32-bit float**: IEEE 754 floating-point samples (-1.0 to +1.0 range) +/// +/// # Performance Characteristics +/// +/// - **Header-based duration**: No file scanning required (instant calculation) +/// - **Direct random access**: O(1) seeking via hound's seek functionality +/// - **Optimized sample conversion**: Efficient bit depth handling +/// - **Minimal memory overhead**: Iterator-based sample access +/// - **Zero decompression**: Direct PCM data access +/// +/// # Seeking Behavior +/// +/// - **Random access seeking**: Direct positioning via hound's seek API +/// - **Forward seeking**: Linear sample skipping when not seekable +/// - **Beyond end**: Seeking past file end is clamped to actual length +/// - **Channel preservation**: Maintains correct channel order across seeks +/// - **Sample accuracy**: Precise positioning without approximation +/// +/// # Thread Safety +/// +/// This decoder is not thread-safe. Create separate instances for concurrent access +/// or use appropriate synchronization primitives. +/// +/// # Generic Parameters +/// +/// * `R` - The underlying data source type, must implement `Read + Seek` pub struct WavDecoder where R: Read + Seek, { + /// Iterator over audio samples with position tracking. + /// + /// Wraps the hound WavReader and provides sample-by-sample iteration + /// with position tracking for seeking operations and size hints. reader: SamplesIterator, + + /// Total duration calculated from WAV header. + /// + /// Computed from sample count and sample rate information in the WAV header. + /// Always available immediately upon decoder creation without file scanning. total_duration: Duration, + + /// Sample rate in Hz from WAV header. + /// + /// Fixed for the entire WAV file. Common rates include 44.1kHz (CD), + /// 48kHz (professional), 96kHz/192kHz (high-resolution). sample_rate: SampleRate, + + /// Number of audio channels from WAV header. + /// + /// Fixed for the entire WAV file. Common configurations include + /// mono (1), stereo (2), and various surround sound formats. channels: ChannelCount, + + /// Whether random access seeking is enabled. + /// + /// When `true`, enables backward seeking using hound's direct seek functionality. + /// When `false`, only forward seeking (sample skipping) is allowed. + is_seekable: bool, } impl WavDecoder where R: Read + Seek, { - /// Attempts to decode the data as WAV. - pub fn new(mut data: R) -> Result, R> { - if !is_wave(data.by_ref()) { + /// Attempts to decode the data as WAV with default settings. + /// + /// This method probes the input data to detect WAV format and initializes the decoder if + /// successful. Uses default settings with no seeking support enabled, though WAV seeking + /// is highly recommended due to its efficiency. + /// + /// # Arguments + /// + /// * `data` - Input stream implementing `Read + Seek` + /// + /// # Returns + /// + /// - `Ok(WavDecoder)` if the data contains valid WAV format + /// - `Err(R)` if the data is not WAV, returning the original stream + /// + /// # Examples + /// + /// ```ignore + /// use std::fs::File; + /// use rodio::decoder::wav::WavDecoder; + /// + /// let file = File::open("audio.wav").unwrap(); + /// match WavDecoder::new(file) { + /// Ok(decoder) => println!("WAV decoder created"), + /// Err(file) => println!("Not a WAV file"), + /// } + /// ``` + /// + /// # Performance + /// + /// This method performs format detection which requires parsing the RIFF/WAVE headers. + /// WAV header parsing is very fast as it only requires reading the first few bytes. + /// The stream position is restored if detection fails. + #[allow(dead_code)] + pub fn new(data: R) -> Result, R> { + Self::new_with_settings(data, &Settings::default()) + } + + /// Attempts to decode the data as WAV with custom settings. + /// + /// This method provides control over decoder configuration, particularly seeking behavior. + /// It performs format detection, parses WAV headers, and initializes the decoder with + /// immediate access to all stream characteristics. + /// + /// # Arguments + /// + /// * `data` - Input stream implementing `Read + Seek` + /// * `settings` - Configuration settings from `DecoderBuilder` + /// + /// # Returns + /// + /// - `Ok(WavDecoder)` if the data contains valid WAV format + /// - `Err(R)` if the data is not WAV, returning the original stream + /// + /// # Settings Usage + /// + /// - `is_seekable`: Enables random access seeking (highly recommended for WAV) + /// - Other settings are informational and don't affect WAV decoding + /// + /// # Examples + /// + /// ```ignore + /// use std::fs::File; + /// use rodio::decoder::{wav::WavDecoder, Settings}; + /// + /// let file = File::open("audio.wav").unwrap(); + /// let mut settings = Settings::default(); + /// settings.is_seekable = true; + /// + /// let decoder = WavDecoder::new_with_settings(file, &settings).unwrap(); + /// ``` + /// + /// # Performance + /// + /// WAV initialization is extremely fast as all information is available in the header: + /// - Format parameters: Immediate access from fmt chunk + /// - Duration calculation: Direct from sample count and rate + /// - No scanning required: Unlike compressed formats + /// + /// # Panics + /// + /// Panics if the WAV file has invalid characteristics (zero sample rate or zero channels). + /// This should never happen with valid WAV data that passes format detection. + pub fn new_with_settings(mut data: R, settings: &Settings) -> Result, R> { + if !is_wave(&mut data) { return Err(data); } let reader = WavReader::new(data).expect("should still be wav"); let spec = reader.spec(); let len = reader.len() as u64; + let total_samples = reader.len(); let reader = SamplesIterator { reader, samples_read: 0, + total_samples, }; let sample_rate = spec.sample_rate; let channels = spec.channels; - assert!(channels > 0); - let total_duration = { - let data_rate = sample_rate as u64 * channels as u64; - let secs = len / data_rate; - let nanos = ((len % data_rate) * 1_000_000_000) / data_rate; - Duration::new(secs, nanos as u32) - }; + // len is number of samples, not bytes, so use samples_to_duration + // Note: hound's len() returns total samples across all channels + let samples_per_channel = len / (channels as u64); + let total_duration = utils::samples_to_duration(samples_per_channel, sample_rate as u64); Ok(WavDecoder { reader, @@ -57,29 +278,121 @@ where sample_rate: SampleRate::new(sample_rate) .expect("wav should have a sample rate higher then zero"), channels: ChannelCount::new(channels).expect("wav should have a least one channel"), + is_seekable: settings.is_seekable, }) } + /// Consumes the decoder and returns the underlying data stream. + /// + /// This can be useful for recovering the original data source after decoding is complete + /// or when the decoder needs to be replaced. The stream position will be at the current + /// playback position within the WAV data chunk. + /// + /// # Examples + /// + /// ```ignore + /// use std::fs::File; + /// use rodio::decoder::wav::WavDecoder; + /// + /// let file = File::open("audio.wav").unwrap(); + /// let decoder = WavDecoder::new(file).unwrap(); + /// let recovered_file = decoder.into_inner(); + /// ``` + /// + /// # Stream Position + /// + /// The returned stream will be positioned at the current sample location within + /// the WAV file's data chunk, which may be useful for manual data processing + /// or format conversion operations. #[inline] pub fn into_inner(self) -> R { self.reader.reader.into_inner() } } +/// Internal iterator for WAV sample reading with position tracking. +/// +/// This struct wraps the hound `WavReader` and tracks the current position +/// for seeking operations and size hints. It handles the complexity of different +/// sample formats while providing a unified interface to the decoder. +/// +/// # Position Tracking +/// +/// Maintains accurate sample count for: +/// - Seeking calculations and channel alignment +/// - Size hint accuracy for buffer allocation +/// - Progress tracking for long files +/// +/// # Sample Format Handling +/// +/// Automatically handles conversion from WAV's various sample formats to +/// Rodio's unified sample format, including proper scaling and sign conversion. struct SamplesIterator where R: Read + Seek, { + /// The underlying hound WAV reader. + /// + /// Provides access to WAV header information and sample data. + /// Handles RIFF/WAVE parsing and validates format compliance. reader: WavReader, + + /// Number of samples read so far (for seeking calculations). + /// + /// Used to track current position for seeking operations and + /// to calculate remaining samples for size hints. Limited to + /// u32 range matching WAV format limitations. samples_read: u32, // wav header is u32 so this suffices + + /// Total number of samples in the file (cached from reader.len()). + /// + /// Cached from the WAV header for efficient size hint calculations + /// without repeatedly querying the reader. + total_samples: u32, } impl Iterator for SamplesIterator where R: Read + Seek, { + /// The type of items yielded by the iterator. + /// + /// Returns `Sample` (typically `f32`) values representing individual audio samples. + /// Samples are interleaved across channels in the order: channel 0, channel 1, etc. type Item = Sample; + /// Returns the next audio sample from the WAV stream. + /// + /// This method handles conversion from various WAV sample formats to Rodio's + /// unified sample format. It reads samples directly from the WAV file without + /// any buffering or decompression overhead. + /// + /// # Sample Format Conversion + /// + /// The method handles different WAV formats: + /// - **8-bit integer**: Converts unsigned (0-255) to signed range + /// - **16-bit integer**: Direct conversion from signed samples + /// - **24-bit integer**: Extracts from 32-bit container using I24 type + /// - **32-bit integer**: Direct conversion from signed samples + /// - **32-bit float**: Direct use of IEEE 754 floating-point values + /// - **Other integer depths**: Bit-shifting for unofficial formats + /// + /// # Error Handling + /// + /// - **Unsupported formats**: Logs error and returns None (stream termination) + /// - **Read errors**: Returns None (end of stream or I/O error) + /// - **Invalid samples**: Skipped with error logging when possible + /// + /// # Performance + /// + /// Direct sample access without intermediate buffering provides optimal + /// performance for WAV files. Sample format conversion is optimized for + /// each bit depth with minimal computational overhead. + /// + /// # Returns + /// + /// - `Some(sample)` - Next audio sample from the WAV file + /// - `None` - End of file reached or unrecoverable error occurred #[inline] fn next(&mut self) -> Option { self.samples_read += 1; @@ -136,59 +449,223 @@ where next_sample } + /// Returns bounds on the remaining length of the iterator. + /// + /// For WAV files, this provides exact remaining sample count based on + /// header information and current position. This enables accurate + /// buffer pre-allocation and progress indication. + /// + /// # Returns + /// + /// A tuple `(lower_bound, upper_bound)` where: + /// - `lower_bound`: Always 0 (no samples guaranteed without I/O) + /// - `upper_bound`: Exact remaining sample count from WAV header + /// + /// # Accuracy + /// + /// WAV files provide exact sample counts in their headers, making the + /// upper bound completely accurate. This is more precise than compressed + /// formats that require estimation or scanning. + /// + /// # Implementation + /// + /// Uses cached total sample count and current position to calculate + /// remaining samples with no I/O overhead. #[inline] fn size_hint(&self) -> (usize, Option) { - let len = (self.reader.len() - self.samples_read) as usize; - (len, Some(len)) + let remaining = self.total_samples.saturating_sub(self.samples_read) as usize; + (0, Some(remaining)) } } -impl ExactSizeIterator for SamplesIterator where R: Read + Seek {} - impl Source for WavDecoder where R: Read + Seek, { + /// Returns the number of samples before parameters change. + /// + /// For WAV files, this always returns `None` because audio parameters + /// (sample rate, channels, bit depth) never change during the stream. + /// WAV files have fixed parameters throughout their duration, enabling + /// optimizations in the audio pipeline. + /// + /// # Implementation Note + /// + /// WAV files have a single fmt chunk that defines parameters for the + /// entire file, unlike some formats that may have parameter changes + /// at specific points. This enables optimizations in audio processing. #[inline] fn current_span_len(&self) -> Option { None } + /// Returns the number of audio channels. + /// + /// WAV supports various channel configurations: + /// - 1 channel: Mono + /// - 2 channels: Stereo + /// - 3+ channels: Multi-channel configurations (5.1, 7.1, etc.) + /// + /// # Guarantees + /// + /// The returned value is constant for the lifetime of the decoder and + /// matches the channel count specified in the WAV file's fmt chunk. + /// This value is available immediately upon decoder creation. #[inline] fn channels(&self) -> ChannelCount { self.channels } + /// Returns the sample rate in Hz. + /// + /// WAV supports a wide range of sample rates: + /// - **8kHz-16kHz**: Speech and telephone quality + /// - **22.05kHz**: Lower quality music + /// - **44.1kHz**: CD quality (most common) + /// - **48kHz**: Professional audio standard + /// - **96kHz/192kHz**: High-resolution audio + /// + /// # Guarantees + /// + /// The returned value is constant for the lifetime of the decoder and + /// matches the sample rate specified in the WAV file's fmt chunk. + /// This value is available immediately upon decoder creation. #[inline] fn sample_rate(&self) -> SampleRate { self.sample_rate } + /// Returns the total duration of the audio stream. + /// + /// For WAV files, this is calculated directly from the header information + /// and is always available immediately upon decoder creation. The calculation + /// uses exact sample counts for perfect accuracy. + /// + /// # Accuracy + /// + /// WAV duration is sample-accurate because: + /// - Sample count is stored in the header + /// - Sample rate is fixed throughout the file + /// - No compression artifacts or estimation required + /// + /// # Always Available + /// + /// Unlike compressed formats that may require scanning, WAV duration is + /// instantly available from header information with no I/O overhead. + /// + /// # Guarantees + /// + /// Always returns `Some(duration)` for valid WAV files. The duration + /// represents the exact playback time based on sample count and rate. #[inline] fn total_duration(&self) -> Option { Some(self.total_duration) } + /// Returns the bit depth of the audio samples. + /// + /// WAV files preserve the original bit depth from the source material: + /// - **8-bit**: Basic quality, often used for legacy applications + /// - **16-bit**: CD quality, most common for music + /// - **24-bit**: High-resolution audio, professional recordings + /// - **32-bit integer**: Maximum precision integer samples + /// - **32-bit float**: Floating-point samples with extended dynamic range + /// + /// # Guarantees + /// + /// Always returns `Some(depth)` for valid WAV files. The bit depth is + /// constant throughout the file and matches the fmt chunk specification. + /// + /// # Implementation Note + /// + /// The bit depth information is preserved from the original WAV file and + /// used for proper sample scaling during conversion to Rodio's sample format. + #[inline] + fn bits_per_sample(&self) -> Option { + Some(self.reader.reader.spec().bits_per_sample as u32) + } + + /// Attempts to seek to the specified position in the audio stream. + /// + /// WAV seeking is highly efficient due to the uncompressed format and + /// direct sample access. The implementation provides both fast random + /// access seeking and forward-only seeking based on configuration. + /// + /// # Seeking Modes + /// + /// - **Random access** (when `is_seekable`): Direct positioning using hound's seek + /// - O(1) performance via direct file positioning + /// - Sample-accurate positioning + /// - **Forward-only** (when not `is_seekable`): Linear sample skipping + /// - O(n) performance where n is samples to skip + /// - Prevents backward seeks for streaming scenarios + /// + /// # Performance Characteristics + /// + /// - **Random access seeks**: Extremely fast, direct file positioning + /// - **Forward seeks**: Efficient sample skipping with minimal overhead + /// - **Channel alignment**: Preserves correct channel order after seeking + /// - **Boundary handling**: Seeks beyond end are clamped to file length + /// + /// # Arguments + /// + /// * `pos` - Target position as duration from stream start + /// + /// # Errors + /// + /// - `SeekError::ForwardOnly` - Backward seek attempted without `is_seekable` + /// - `SeekError::IoError` - I/O error during seek operation (rare for valid files) + /// + /// # Examples + /// + /// ```no_run + /// use std::{fs::File, time::Duration}; + /// use rodio::{Decoder, Source}; + /// + /// let file = File::open("audio.wav").unwrap(); + /// let mut decoder = Decoder::builder() + /// .with_data(file) + /// .with_seekable(true) + /// .build() + /// .unwrap(); + /// + /// // Instant seek to 30 seconds + /// decoder.try_seek(Duration::from_secs(30)).unwrap(); + /// ``` + /// + /// # Implementation Details + /// + /// The seeking implementation preserves channel alignment to ensure that seeking + /// to a specific time position results in the correct channel being returned + /// for the first sample after the seek operation. #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { let file_len = self.reader.reader.duration(); - let new_pos = pos.as_secs_f32() * self.sample_rate().get() as f32; - let new_pos = new_pos as u32; + let new_pos = (pos.as_secs_f64() * self.sample_rate().get() as f64) as u32; let new_pos = new_pos.min(file_len); // saturate pos at the end of the source - // make sure the next sample is for the right channel - let to_skip = self.reader.samples_read % self.channels().get() as u32; + let target_sample = new_pos * self.channels().get() as u32; + let samples_to_skip = if !self.is_seekable { + if target_sample < self.reader.samples_read { + return Err(SeekError::ForwardOnly); + } else { + // we can only skip forward, so calculate how many samples to skip + target_sample - self.reader.samples_read + } + } else { + // seekable, so we can jump directly to the target sample + // make sure the next sample is for the right channel + let active_channel = self.reader.samples_read % self.channels().get() as u32; + + self.reader.reader.seek(new_pos).map_err(Arc::new)?; + self.reader.samples_read = new_pos * self.channels().get() as u32; - self.reader - .reader - .seek(new_pos) - .map_err(Arc::new) - .map_err(SeekError::HoundDecoder)?; - self.reader.samples_read = new_pos * self.channels().get() as u32; + active_channel + }; - for _ in 0..to_skip { - self.next(); + for _ in 0..samples_to_skip { + let _ = self.next(); } Ok(()) @@ -199,28 +676,96 @@ impl Iterator for WavDecoder where R: Read + Seek, { + /// The type of items yielded by the iterator. + /// + /// Returns `Sample` (typically `f32`) values representing individual audio samples. + /// Samples are interleaved across channels in the order: channel 0, channel 1, etc. type Item = Sample; + /// Returns the next audio sample from the WAV stream. + /// + /// This method delegates to the internal `SamplesIterator` which handles + /// sample format conversion and position tracking. WAV iteration is highly + /// efficient due to direct sample access without decompression. + /// + /// # Performance + /// + /// WAV sample iteration has minimal overhead: + /// - Direct file reads without decompression + /// - Efficient sample format conversion + /// - No intermediate buffering required + /// - Optimal for real-time audio processing + /// + /// # Returns + /// + /// - `Some(sample)` - Next audio sample from the WAV file + /// - `None` - End of file reached or I/O error occurred #[inline] fn next(&mut self) -> Option { self.reader.next() } + /// Returns bounds on the remaining length of the iterator. + /// + /// Delegates to the internal `SamplesIterator` which provides exact + /// remaining sample count based on WAV header information. + /// + /// # Returns + /// + /// A tuple `(lower_bound, upper_bound)` where: + /// - `lower_bound`: Always 0 (no samples guaranteed without I/O) + /// - `upper_bound`: Exact remaining sample count from WAV header + /// + /// # Accuracy + /// + /// WAV files provide perfect size hint accuracy due to exact sample + /// counts in file headers, enabling optimal buffer allocation and + /// progress indication. #[inline] fn size_hint(&self) -> (usize, Option) { self.reader.size_hint() } } -impl ExactSizeIterator for WavDecoder where R: Read + Seek {} - -/// Returns true if the stream contains WAV data, then resets it to where it was. -fn is_wave(mut data: R) -> bool +/// Probes input data to detect WAV format. +/// +/// This function attempts to parse the RIFF/WAVE headers to determine if the +/// data contains a valid WAV file. The stream position is restored regardless +/// of the result, making it safe to use for format detection. +/// +/// # Arguments +/// +/// * `data` - Mutable reference to the input stream to probe +/// +/// # Returns +/// +/// - `true` if the data appears to contain a valid WAV file +/// - `false` if the data is not WAV or has invalid headers +/// +/// # Implementation +/// +/// Uses the common `utils::probe_format` helper which: +/// 1. Saves the current stream position +/// 2. Attempts WAV detection using `hound::WavReader` +/// 3. Restores the original stream position +/// 4. Returns the detection result +/// +/// # Performance +/// +/// This function only reads the minimal amount of data needed to identify +/// the RIFF/WAVE headers, making it very efficient for format detection +/// in multi-format scenarios. +/// +/// # Robustness +/// +/// The detection uses hound's robust RIFF/WAVE parsing which validates: +/// - RIFF container format compliance +/// - WAVE format identification +/// - fmt chunk presence and validity +/// - Basic structural integrity +fn is_wave(data: &mut R) -> bool where R: Read + Seek, { - let stream_pos = data.stream_position().unwrap_or_default(); - let result = WavReader::new(data.by_ref()).is_ok(); - let _ = data.seek(SeekFrom::Start(stream_pos)); - result + utils::probe_format(data, |reader| WavReader::new(reader).is_ok()) } diff --git a/src/math.rs b/src/math.rs index f84c0a65..55a50811 100644 --- a/src/math.rs +++ b/src/math.rs @@ -139,10 +139,8 @@ mod test { /// - Practical audio range (-60dB to +40dB): max errors ~1x ε /// - Extended range (-100dB to +100dB): max errors ~2.3x ε /// - Extreme edge cases beyond ±100dB have larger errors but are rarely used - - /// Based on [Wikipedia's Decibel article]. /// - /// [Wikipedia's Decibel article]: https://web.archive.org/web/20230810185300/https://en.wikipedia.org/wiki/Decibel + /// Based on [Wikipedia's Decibel article]( https://web.archive.org/web/20230810185300/https://en.wikipedia.org/wiki/Decibel) const DECIBELS_LINEAR_TABLE: [(f32, f32); 27] = [ (100., 100000.), (90., 31623.), diff --git a/src/mixer.rs b/src/mixer.rs index b434dba9..435cab7c 100644 --- a/src/mixer.rs +++ b/src/mixer.rs @@ -105,9 +105,17 @@ impl Source for MixerSource { None } + #[inline] + fn bits_per_sample(&self) -> Option { + self.current_sources + .iter() + .flat_map(|s| s.bits_per_sample()) + .max() + } + #[inline] fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> { - Err(SeekError::NotSupported { + Err(SeekError::SeekingNotSupported { underlying_source: std::any::type_name::(), }) diff --git a/src/queue.rs b/src/queue.rs index 495606b4..27b191c3 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -167,6 +167,11 @@ impl Source for SourcesQueueOutput { None } + #[inline] + fn bits_per_sample(&self) -> Option { + self.current.bits_per_sample() + } + /// Only seeks within the current source. // We can not go back to previous sources. We could implement seek such // that it advances the queue if the position is beyond the current song. diff --git a/src/source/agc.rs b/src/source/agc.rs index 4ac2f538..befffe75 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -489,6 +489,11 @@ where self.input.total_duration() } + #[inline] + fn bits_per_sample(&self) -> Option { + self.input.bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { self.input.try_seek(pos) diff --git a/src/source/amplify.rs b/src/source/amplify.rs index 985c6be3..3e6a0716 100644 --- a/src/source/amplify.rs +++ b/src/source/amplify.rs @@ -96,6 +96,11 @@ where self.input.total_duration() } + #[inline] + fn bits_per_sample(&self) -> Option { + self.input.bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { self.input.try_seek(pos) diff --git a/src/source/blt.rs b/src/source/blt.rs index 151b0bee..174339d2 100644 --- a/src/source/blt.rs +++ b/src/source/blt.rs @@ -173,6 +173,11 @@ where self.input.total_duration() } + #[inline] + fn bits_per_sample(&self) -> Option { + self.input.bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { self.input.try_seek(pos) diff --git a/src/source/buffered.rs b/src/source/buffered.rs index 974389dc..8b269fc2 100644 --- a/src/source/buffered.rs +++ b/src/source/buffered.rs @@ -62,6 +62,7 @@ where data: Vec, channels: ChannelCount, rate: SampleRate, + bits_per_sample: Option, next: Mutex>>, } @@ -107,6 +108,7 @@ where let channels = input.channels(); let rate = input.sample_rate(); + let bits_per_sample = input.bits_per_sample(); let data: Vec = input .by_ref() .take(cmp::min(span_len.unwrap_or(32768), 32768)) @@ -120,6 +122,7 @@ where data, channels, rate, + bits_per_sample, next: Mutex::new(Arc::new(Span::Input(Mutex::new(Some(input))))), })) } @@ -234,11 +237,22 @@ where self.total_duration } + #[inline] + fn bits_per_sample(&self) -> Option { + match *self.current_span { + Span::Data(SpanData { + bits_per_sample, .. + }) => bits_per_sample, + Span::End => None, + Span::Input(_) => unreachable!(), + } + } + /// Can not support seek, in the end state we lose the underlying source /// which makes seeking back impossible. #[inline] fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> { - Err(SeekError::NotSupported { + Err(SeekError::SeekingNotSupported { underlying_source: std::any::type_name::(), }) } diff --git a/src/source/channel_volume.rs b/src/source/channel_volume.rs index c80c4d73..60fb10ef 100644 --- a/src/source/channel_volume.rs +++ b/src/source/channel_volume.rs @@ -119,6 +119,11 @@ where self.input.total_duration() } + #[inline] + fn bits_per_sample(&self) -> Option { + self.input.bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { self.input.try_seek(pos) diff --git a/src/source/chirp.rs b/src/source/chirp.rs index e7bff520..9216b036 100644 --- a/src/source/chirp.rs +++ b/src/source/chirp.rs @@ -101,4 +101,9 @@ impl Source for Chirp { let secs = self.total_samples as f64 / self.sample_rate.get() as f64; Some(Duration::from_secs_f64(secs)) } + + #[inline] + fn bits_per_sample(&self) -> Option { + Some(f32::MANTISSA_DIGITS) + } } diff --git a/src/source/delay.rs b/src/source/delay.rs index 1f2d3fb4..90fde8f6 100644 --- a/src/source/delay.rs +++ b/src/source/delay.rs @@ -111,6 +111,11 @@ where .map(|val| val + self.requested_duration) } + #[inline] + fn bits_per_sample(&self) -> Option { + self.input.bits_per_sample() + } + /// Pos is seen from the perspective of the api user. /// /// # Example diff --git a/src/source/distortion.rs b/src/source/distortion.rs index c3e06ed8..a13e88e8 100644 --- a/src/source/distortion.rs +++ b/src/source/distortion.rs @@ -103,6 +103,11 @@ where self.input.total_duration() } + #[inline] + fn bits_per_sample(&self) -> Option { + self.input.bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { self.input.try_seek(pos) diff --git a/src/source/done.rs b/src/source/done.rs index 5bc922b7..8738eca9 100644 --- a/src/source/done.rs +++ b/src/source/done.rs @@ -90,6 +90,11 @@ where self.input.total_duration() } + #[inline] + fn bits_per_sample(&self) -> Option { + self.input.bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { self.input.try_seek(pos) diff --git a/src/source/empty.rs b/src/source/empty.rs index bfdf422e..36040e47 100644 --- a/src/source/empty.rs +++ b/src/source/empty.rs @@ -52,12 +52,17 @@ impl Source for Empty { #[inline] fn total_duration(&self) -> Option { - Some(Duration::new(0, 0)) + Some(Duration::ZERO) + } + + #[inline] + fn bits_per_sample(&self) -> Option { + None } #[inline] fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> { - Err(SeekError::NotSupported { + Err(SeekError::SeekingNotSupported { underlying_source: std::any::type_name::(), }) } diff --git a/src/source/empty_callback.rs b/src/source/empty_callback.rs index 4ae62437..c05b51ce 100644 --- a/src/source/empty_callback.rs +++ b/src/source/empty_callback.rs @@ -49,12 +49,17 @@ impl Source for EmptyCallback { #[inline] fn total_duration(&self) -> Option { - Some(Duration::new(0, 0)) + Some(Duration::ZERO) + } + + #[inline] + fn bits_per_sample(&self) -> Option { + None } #[inline] fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> { - Err(SeekError::NotSupported { + Err(SeekError::SeekingNotSupported { underlying_source: std::any::type_name::(), }) } diff --git a/src/source/fadein.rs b/src/source/fadein.rs index 85e19293..27d30226 100644 --- a/src/source/fadein.rs +++ b/src/source/fadein.rs @@ -86,6 +86,11 @@ where self.inner().total_duration() } + #[inline] + fn bits_per_sample(&self) -> Option { + self.inner().bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { self.inner_mut().try_seek(pos) diff --git a/src/source/fadeout.rs b/src/source/fadeout.rs index 14a41569..c83c4e75 100644 --- a/src/source/fadeout.rs +++ b/src/source/fadeout.rs @@ -86,6 +86,11 @@ where self.inner().total_duration() } + #[inline] + fn bits_per_sample(&self) -> Option { + self.inner().bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { self.inner_mut().try_seek(pos) diff --git a/src/source/from_iter.rs b/src/source/from_iter.rs index 59845758..a979e887 100644 --- a/src/source/from_iter.rs +++ b/src/source/from_iter.rs @@ -137,6 +137,15 @@ where None } + #[inline] + fn bits_per_sample(&self) -> Option { + if let Some(src) = &self.current_source { + src.bits_per_sample() + } else { + None + } + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { if let Some(source) = self.current_source.as_mut() { diff --git a/src/source/limit.rs b/src/source/limit.rs index 37ca282a..55c32fad 100644 --- a/src/source/limit.rs +++ b/src/source/limit.rs @@ -595,6 +595,11 @@ where self.0.total_duration() } + #[inline] + fn bits_per_sample(&self) -> Option { + self.0.bits_per_sample() + } + #[inline] fn try_seek(&mut self, position: Duration) -> Result<(), SeekError> { self.0.try_seek(position) @@ -1080,6 +1085,11 @@ where self.inner().total_duration() } + #[inline] + fn bits_per_sample(&self) -> Option { + self.inner().bits_per_sample() + } + /// Attempts to seek to the specified position. /// /// Resets limiter state to prevent artifacts after seeking: diff --git a/src/source/linear_ramp.rs b/src/source/linear_ramp.rs index eb0aad8a..22ddfecf 100644 --- a/src/source/linear_ramp.rs +++ b/src/source/linear_ramp.rs @@ -127,6 +127,11 @@ where self.input.total_duration() } + #[inline] + fn bits_per_sample(&self) -> Option { + self.input.bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { self.elapsed_ns = pos.as_nanos() as f32; diff --git a/src/source/mix.rs b/src/source/mix.rs index c5fedfb6..66ab808c 100644 --- a/src/source/mix.rs +++ b/src/source/mix.rs @@ -107,6 +107,21 @@ where match (f1, f2) { (Some(f1), Some(f2)) => Some(cmp::max(f1, f2)), + (Some(f1), None) => Some(f1), + (None, Some(f2)) => Some(f2), + _ => None, + } + } + + #[inline] + fn bits_per_sample(&self) -> Option { + let f1 = self.input1.bits_per_sample(); + let f2 = self.input2.bits_per_sample(); + + match (f1, f2) { + (Some(f1), Some(f2)) => Some(cmp::max(f1, f2)), + (Some(f1), None) => Some(f1), + (None, Some(f2)) => Some(f2), _ => None, } } @@ -114,12 +129,11 @@ where /// Will only attempt a seek if both underlying sources support seek. #[inline] fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> { - Err(SeekError::NotSupported { + Err(SeekError::SeekingNotSupported { underlying_source: std::any::type_name::(), }) // uncomment when #510 is implemented (query position of playback) - // TODO use source_intact to check if rollback makes sense // let org_pos = self.input1.playback_pos(); // self.input1.try_seek(pos)?; diff --git a/src/source/mod.rs b/src/source/mod.rs index 52dcc2f0..d33641a5 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -153,8 +153,8 @@ pub use self::noise::{Pink, WhiteUniform}; /// > transition between the two files. /// /// However, for optimization purposes rodio supposes that the number of channels and the frequency -/// stay the same for long periods of time and avoids calling `channels()` and -/// `sample_rate` too frequently. +/// stay the same for long periods of time and avoids calling `channels()` and `sample_rate` too +/// frequently. /// /// In order to properly handle this situation, the `current_span_len()` method should return /// the number of samples that remain in the iterator before the samples rate and number of @@ -162,8 +162,8 @@ pub use self::noise::{Pink, WhiteUniform}; /// pub trait Source: Iterator { /// Returns the number of samples before the current span ends. `None` means "infinite" or - /// "until the sound ends". - /// Should never return 0 unless there's no more data. + /// "until the sound ends". Sources that return `Some(x)` should return `Some(0)` if and + /// if when there's no more data. /// /// After the engine has finished reading the specified number of samples, it will check /// whether the value of `channels()` and/or `sample_rate()` have changed. @@ -181,6 +181,9 @@ pub trait Source: Iterator { /// `None` indicates at the same time "infinite" or "unknown". fn total_duration(&self) -> Option; + /// Returns the number of bits per sample, if known. + fn bits_per_sample(&self) -> Option; + /// Stores the source in a buffer in addition to returning it. This iterator can be cloned. #[inline] fn buffered(self) -> Buffered @@ -696,7 +699,7 @@ pub trait Source: Iterator { /// the source is not known. #[allow(unused_variables)] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - Err(SeekError::NotSupported { + Err(SeekError::SeekingNotSupported { underlying_source: std::any::type_name::(), }) } @@ -711,49 +714,57 @@ pub trait Source: Iterator { pub enum SeekError { /// One of the underlying sources does not support seeking #[error("Seeking is not supported by source: {underlying_source}")] - NotSupported { + SeekingNotSupported { /// The source that did not support seek underlying_source: &'static str, }, - #[cfg(feature = "symphonia")] - /// The symphonia decoder ran into an issue - #[error("Symphonia decoder returned an error")] - SymphoniaDecoder(#[source] crate::decoder::symphonia::SeekError), + + /// The source only supports forward seeking + #[error("The source only supports forward seeking")] + ForwardOnly, + + #[cfg(feature = "claxon")] + #[cfg_attr(docsrs, doc(cfg(feature = "claxon")))] + /// The claxon (Flac) decoder ran into an issue + #[error("Claxon decoder returned an error: {0}")] + ClaxonDecoder(#[from] Arc), + #[cfg(feature = "hound")] #[cfg_attr(docsrs, doc(cfg(feature = "hound")))] - /// The hound (wav) decoder ran into an issue - #[error("Hound decoder returned an error")] - HoundDecoder(#[source] Arc), - // Prefer adding an enum variant to using this. It's meant for end users their - // own `try_seek` implementations. + /// The Hound (Wav) decoder ran into an issue + #[error("Hound decoder returned an error: {0}")] + HoundDecoder(#[from] Arc), + + #[cfg(feature = "lewton")] + #[cfg_attr(docsrs, doc(cfg(feature = "lewton")))] + /// The Lewton (Ogg Vorbis) decoder ran into an issue + #[error("Lewton decoder returned an error: {0}")] + LewtonDecoder(#[from] Arc), + + #[cfg(feature = "minimp3")] + #[cfg_attr(docsrs, doc(cfg(feature = "minimp3")))] + /// The minimp3 (Mp3) decoder ran into an issue + #[error("Minimp3 decoder returned an error: {0}")] + Minimp3Decoder(#[from] Arc), + + #[cfg(feature = "symphonia")] + #[cfg_attr(docsrs, doc(cfg(feature = "symphonia")))] + /// The Symphonia decoder ran into an issue + #[error("Symphonia decoder returned an error: {0}")] + SymphoniaDecoder(#[from] Arc), + + /// An I/O error occurred while seeking + #[error("An I/O error occurred while seeking: {0}")] + IoError(#[from] Arc), + + // Prefer adding an enum variant to using this. It's meant for end users' own `try_seek` + // implementations. /// Any other error probably in a custom Source #[error(transparent)] Other(Arc), } assert_error_traits!(SeekError); -#[cfg(feature = "symphonia")] -impl From for SeekError { - fn from(source: crate::decoder::symphonia::SeekError) -> Self { - SeekError::SymphoniaDecoder(source) - } -} - -impl SeekError { - /// Will the source remain playing at its position before the seek or is it - /// broken? - pub fn source_intact(&self) -> bool { - match self { - SeekError::NotSupported { .. } => true, - #[cfg(feature = "symphonia")] - SeekError::SymphoniaDecoder(_) => false, - #[cfg(feature = "hound")] - SeekError::HoundDecoder(_) => false, - SeekError::Other(_) => false, - } - } -} - macro_rules! source_pointer_impl { ($($sig:tt)+) => { impl $($sig)+ { @@ -777,6 +788,11 @@ macro_rules! source_pointer_impl { (**self).total_duration() } + #[inline] + fn bits_per_sample(&self) -> Option { + (**self).bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { (**self).try_seek(pos) diff --git a/src/source/noise.rs b/src/source/noise.rs index b9987a8d..b35fb160 100644 --- a/src/source/noise.rs +++ b/src/source/noise.rs @@ -85,6 +85,10 @@ macro_rules! impl_noise_source { None } + fn bits_per_sample(&self) -> Option { + Some(f32::MANTISSA_DIGITS) + } + fn try_seek(&mut self, _pos: Duration) -> Result<(), crate::source::SeekError> { // Stateless noise generators can seek to any position since all positions // are equally random and don't depend on previous state @@ -747,6 +751,10 @@ impl Source for Brownian { None } + fn bits_per_sample(&self) -> Option { + Some(f32::MANTISSA_DIGITS) + } + fn try_seek(&mut self, _pos: Duration) -> Result<(), crate::source::SeekError> { // Stateless noise generators can seek to any position since all positions // are equally random and don't depend on previous state @@ -824,6 +832,10 @@ impl Source for Red { None } + fn bits_per_sample(&self) -> Option { + Some(f32::MANTISSA_DIGITS) + } + fn try_seek(&mut self, _pos: Duration) -> Result<(), crate::source::SeekError> { // Stateless noise generators can seek to any position since all positions // are equally random and don't depend on previous state diff --git a/src/source/pausable.rs b/src/source/pausable.rs index 5143b622..6e885254 100644 --- a/src/source/pausable.rs +++ b/src/source/pausable.rs @@ -126,6 +126,11 @@ where self.input.total_duration() } + #[inline] + fn bits_per_sample(&self) -> Option { + self.input.bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { self.input.try_seek(pos) diff --git a/src/source/periodic.rs b/src/source/periodic.rs index 09fc5477..3c8a33cc 100644 --- a/src/source/periodic.rs +++ b/src/source/periodic.rs @@ -117,6 +117,11 @@ where self.input.total_duration() } + #[inline] + fn bits_per_sample(&self) -> Option { + self.input.bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { self.input.try_seek(pos) diff --git a/src/source/position.rs b/src/source/position.rs index 1788625b..fa9b0b82 100644 --- a/src/source/position.rs +++ b/src/source/position.rs @@ -142,6 +142,11 @@ where self.input.total_duration() } + #[inline] + fn bits_per_sample(&self) -> Option { + self.input.bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { let result = self.input.try_seek(pos); diff --git a/src/source/repeat.rs b/src/source/repeat.rs index 12a6b947..73e9300e 100644 --- a/src/source/repeat.rs +++ b/src/source/repeat.rs @@ -83,6 +83,14 @@ where None } + #[inline] + fn bits_per_sample(&self) -> Option { + match self.inner.current_span_len() { + Some(0) => self.next.bits_per_sample(), + _ => self.inner.bits_per_sample(), + } + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { self.inner.try_seek(pos) diff --git a/src/source/sawtooth.rs b/src/source/sawtooth.rs index a0e812e4..f96a61ca 100644 --- a/src/source/sawtooth.rs +++ b/src/source/sawtooth.rs @@ -59,6 +59,11 @@ impl Source for SawtoothWave { None } + #[inline] + fn bits_per_sample(&self) -> Option { + Some(f32::MANTISSA_DIGITS) + } + #[inline] fn try_seek(&mut self, duration: Duration) -> Result<(), SeekError> { self.test_saw.try_seek(duration) diff --git a/src/source/signal_generator.rs b/src/source/signal_generator.rs index 6f448d11..c07d51f2 100644 --- a/src/source/signal_generator.rs +++ b/src/source/signal_generator.rs @@ -158,6 +158,11 @@ impl Source for SignalGenerator { None } + #[inline] + fn bits_per_sample(&self) -> Option { + Some(f32::MANTISSA_DIGITS) + } + #[inline] fn try_seek(&mut self, duration: Duration) -> Result<(), SeekError> { let seek = duration.as_secs_f32() * (self.sample_rate.get() as f32) / self.period; diff --git a/src/source/sine.rs b/src/source/sine.rs index a85fcb63..67f5c1df 100644 --- a/src/source/sine.rs +++ b/src/source/sine.rs @@ -59,6 +59,11 @@ impl Source for SineWave { None } + #[inline] + fn bits_per_sample(&self) -> Option { + Some(f32::MANTISSA_DIGITS) + } + #[inline] fn try_seek(&mut self, duration: Duration) -> Result<(), SeekError> { self.test_sine.try_seek(duration) diff --git a/src/source/skip.rs b/src/source/skip.rs index 36f8d210..164acc4a 100644 --- a/src/source/skip.rs +++ b/src/source/skip.rs @@ -153,6 +153,11 @@ where }) } + #[inline] + fn bits_per_sample(&self) -> Option { + self.input.bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { self.input.try_seek(pos) diff --git a/src/source/skippable.rs b/src/source/skippable.rs index ba4d43d9..eacd2b52 100644 --- a/src/source/skippable.rs +++ b/src/source/skippable.rs @@ -94,6 +94,11 @@ where self.input.total_duration() } + #[inline] + fn bits_per_sample(&self) -> Option { + self.input.bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { self.input.try_seek(pos) diff --git a/src/source/spatial.rs b/src/source/spatial.rs index 6ea3555d..fbd84241 100644 --- a/src/source/spatial.rs +++ b/src/source/spatial.rs @@ -112,6 +112,11 @@ where self.input.total_duration() } + #[inline] + fn bits_per_sample(&self) -> Option { + self.input.bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { self.input.try_seek(pos) diff --git a/src/source/speed.rs b/src/source/speed.rs index e38c2b99..9dc384b2 100644 --- a/src/source/speed.rs +++ b/src/source/speed.rs @@ -137,6 +137,11 @@ where self.input.total_duration().map(|d| d.div_f32(self.factor)) } + #[inline] + fn bits_per_sample(&self) -> Option { + self.input.bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { let pos_accounting_for_speedup = pos.mul_f32(self.factor); diff --git a/src/source/square.rs b/src/source/square.rs index 4beaf33d..298a9e33 100644 --- a/src/source/square.rs +++ b/src/source/square.rs @@ -59,6 +59,11 @@ impl Source for SquareWave { None } + #[inline] + fn bits_per_sample(&self) -> Option { + Some(f32::MANTISSA_DIGITS) + } + #[inline] fn try_seek(&mut self, duration: Duration) -> Result<(), SeekError> { self.test_square.try_seek(duration) diff --git a/src/source/stoppable.rs b/src/source/stoppable.rs index 57cc7dae..96b4765a 100644 --- a/src/source/stoppable.rs +++ b/src/source/stoppable.rs @@ -90,6 +90,11 @@ where self.input.total_duration() } + #[inline] + fn bits_per_sample(&self) -> Option { + self.input.bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { self.input.try_seek(pos) diff --git a/src/source/take.rs b/src/source/take.rs index 60ee9c6d..98c34eb3 100644 --- a/src/source/take.rs +++ b/src/source/take.rs @@ -170,6 +170,11 @@ where } } + #[inline] + fn bits_per_sample(&self) -> Option { + self.input.bits_per_sample() + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { self.input.try_seek(pos) diff --git a/src/source/triangle.rs b/src/source/triangle.rs index a35df3af..5309de1a 100644 --- a/src/source/triangle.rs +++ b/src/source/triangle.rs @@ -59,6 +59,11 @@ impl Source for TriangleWave { None } + #[inline] + fn bits_per_sample(&self) -> Option { + Some(f32::MANTISSA_DIGITS) + } + #[inline] fn try_seek(&mut self, duration: Duration) -> Result<(), SeekError> { self.test_tri.try_seek(duration) diff --git a/src/source/uniform.rs b/src/source/uniform.rs index b92642fa..0e06b6fc 100644 --- a/src/source/uniform.rs +++ b/src/source/uniform.rs @@ -20,6 +20,7 @@ where target_channels: ChannelCount, target_sample_rate: SampleRate, total_duration: Option, + bits_per_sample: Option, } impl UniformSourceIterator @@ -35,6 +36,7 @@ where target_sample_rate: SampleRate, ) -> UniformSourceIterator { let total_duration = input.total_duration(); + let bits_per_sample = input.bits_per_sample(); let input = UniformSourceIterator::bootstrap(input, target_channels, target_sample_rate); UniformSourceIterator { @@ -42,6 +44,7 @@ where target_channels, target_sample_rate, total_duration, + bits_per_sample, } } @@ -119,6 +122,11 @@ where self.total_duration } + #[inline] + fn bits_per_sample(&self) -> Option { + self.bits_per_sample + } + #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { if let Some(input) = self.inner.as_mut() { diff --git a/src/source/zero.rs b/src/source/zero.rs index aba88aba..26e50e63 100644 --- a/src/source/zero.rs +++ b/src/source/zero.rs @@ -78,6 +78,11 @@ impl Source for Zero { None } + #[inline] + fn bits_per_sample(&self) -> Option { + Some(f32::MANTISSA_DIGITS) + } + #[inline] fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> { Ok(()) diff --git a/src/static_buffer.rs b/src/static_buffer.rs index bd5130fe..05dc189e 100644 --- a/src/static_buffer.rs +++ b/src/static_buffer.rs @@ -91,9 +91,14 @@ impl Source for StaticSamplesBuffer { Some(self.duration) } + #[inline] + fn bits_per_sample(&self) -> Option { + None + } + #[inline] fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> { - Err(SeekError::NotSupported { + Err(SeekError::SeekingNotSupported { underlying_source: std::any::type_name::(), }) } diff --git a/tests/seek.rs b/tests/seek.rs index 7a96eeb2..60759094 100644 --- a/tests/seek.rs +++ b/tests/seek.rs @@ -1,120 +1,86 @@ #![allow(dead_code)] #![allow(unused_imports)] -#[cfg(feature = "symphonia-mp3")] -use rodio::{decoder::symphonia, source::SeekError}; +use std::{ + io::{Read, Seek}, + path::Path, + time::Duration, +}; + +#[cfg(any( + feature = "symphonia-mp3", + all(feature = "symphonia-ogg", feature = "symphonia-vorbis") +))] +use rodio::source::SeekError; use rodio::{ChannelCount, Decoder, Source}; + use rstest::rstest; use rstest_reuse::{self, *}; -use std::io::{Read, Seek}; -use std::path::Path; -use std::time::Duration; #[cfg(any( feature = "claxon", + feature = "hound", + feature = "lewton", feature = "minimp3", - feature = "symphonia-aac", + all(feature = "symphonia-aac", feature = "symphonia-isomp4"), feature = "symphonia-flac", feature = "symphonia-mp3", - feature = "symphonia-isomp4", - feature = "symphonia-ogg", - feature = "symphonia-wav", - feature = "hound", + all(feature = "symphonia-ogg", feature = "symphonia-vorbis"), + all(feature = "symphonia-pcm", feature = "symphonia-wav"), ))] #[template] #[rstest] +#[cfg_attr(feature = "claxon", case("flac", "claxon"))] +#[cfg_attr(feature = "hound", case("wav", "hound"))] +#[cfg_attr(feature = "lewton", case("ogg", "lewton"))] +#[cfg_attr(feature = "minimp3", case("mp3", "minimp3"))] #[cfg_attr( - all(feature = "symphonia-ogg", feature = "symphonia-vorbis"), - case("ogg", true, "symphonia") -)] -#[cfg_attr( - all(feature = "minimp3", not(feature = "symphonia-mp3")), - case("mp3", false, "minimp3") + all(feature = "symphonia-flac", not(feature = "claxon")), + case("flac", "symphonia") )] -#[cfg_attr( - all(feature = "hound", not(feature = "symphonia-wav")), - case("wav", true, "hound") -)] -#[cfg_attr( - all(feature = "claxon", not(feature = "symphonia-flac")), - case("flac", false, "claxon") -)] -#[cfg_attr(feature = "symphonia-mp3", case("mp3", true, "symphonia"))] #[cfg_attr( all(feature = "symphonia-isomp4", feature = "symphonia-aac"), - case("m4a", true, "symphonia") + case("m4a", "symphonia") )] -#[cfg_attr(feature = "symphonia-wav", case("wav", true, "symphonia"))] -#[cfg_attr(feature = "symphonia-flac", case("flac", true, "symphonia"))] -fn all_decoders( - #[case] format: &'static str, - #[case] supports_seek: bool, - #[case] decoder_name: &'static str, -) { -} - -#[cfg(any( - feature = "symphonia-flac", - feature = "symphonia-mp3", - feature = "symphonia-isomp4", - feature = "symphonia-ogg", - feature = "symphonia-wav", - feature = "hound", -))] -#[template] -#[rstest] #[cfg_attr( - all(feature = "symphonia-ogg", feature = "symphonia-vorbis"), - case("ogg", "symphonia") + all(feature = "symphonia-mp3", not(feature = "minimp3")), + case("mp3", "symphonia") )] #[cfg_attr( - all(feature = "hound", not(feature = "symphonia-wav")), - case("wav", "hound") + all( + feature = "symphonia-ogg", + feature = "symphonia-vorbis", + not(feature = "lewton") + ), + case("ogg", "symphonia") )] -#[cfg_attr(feature = "symphonia-mp3", case("mp3", "symphonia"))] #[cfg_attr( - all(feature = "symphonia-isomp4", feature = "symphonia-aac"), - case("m4a", "symphonia") + all( + feature = "symphonia-pcm", + feature = "symphonia-wav", + not(feature = "hound") + ), + case("wav", "symphonia") )] -#[cfg_attr(feature = "symphonia-wav", case("wav", "symphonia"))] -#[cfg_attr(feature = "symphonia-flac", case("flac", "symphonia"))] -fn supported_decoders(#[case] format: &'static str, #[case] decoder_name: &'static str) {} +fn all_decoders(#[case] format: &'static str, #[case] decoder_name: &'static str) {} #[cfg(any( feature = "claxon", + feature = "hound", + feature = "lewton", feature = "minimp3", + all(feature = "symphonia-aac", feature = "symphonia-isomp4"), feature = "symphonia-flac", feature = "symphonia-mp3", - feature = "symphonia-isomp4", - feature = "symphonia-ogg", - feature = "symphonia-wav", - feature = "hound", + all(feature = "symphonia-ogg", feature = "symphonia-vorbis"), + all(feature = "symphonia-pcm", feature = "symphonia-wav"), ))] #[apply(all_decoders)] #[trace] -fn seek_returns_err_if_unsupported( - #[case] format: &'static str, - #[case] supports_seek: bool, - #[case] decoder_name: &'static str, -) { - let mut decoder = get_music(format); - let res = decoder.try_seek(Duration::from_millis(2500)); - assert_eq!(res.is_ok(), supports_seek, "decoder: {decoder_name}"); -} - -#[cfg(any( - feature = "symphonia-flac", - feature = "symphonia-mp3", - feature = "symphonia-isomp4", - feature = "symphonia-ogg", - feature = "symphonia-wav", - feature = "hound", -))] -#[apply(supported_decoders)] -#[trace] fn seek_beyond_end_saturates(#[case] format: &'static str, #[case] decoder_name: &'static str) { - let mut decoder = get_music(format); println!("seeking beyond end for: {format}\t decoded by: {decoder_name}"); + + let mut decoder = get_music(format); let res = decoder.try_seek(Duration::from_secs(999)); assert!(res.is_ok(), "err: {res:?}"); @@ -122,14 +88,17 @@ fn seek_beyond_end_saturates(#[case] format: &'static str, #[case] decoder_name: } #[cfg(any( + feature = "claxon", + feature = "hound", + feature = "lewton", + feature = "minimp3", + all(feature = "symphonia-aac", feature = "symphonia-isomp4"), feature = "symphonia-flac", feature = "symphonia-mp3", - feature = "symphonia-isomp4", - feature = "symphonia-ogg", - feature = "symphonia-wav", - feature = "hound", + all(feature = "symphonia-ogg", feature = "symphonia-vorbis"), + all(feature = "symphonia-pcm", feature = "symphonia-wav"), ))] -#[apply(supported_decoders)] +#[apply(all_decoders)] #[trace] fn seek_results_in_correct_remaining_playtime( #[case] format: &'static str, @@ -158,19 +127,24 @@ fn seek_results_in_correct_remaining_playtime( } #[cfg(any( + feature = "claxon", + feature = "hound", + feature = "lewton", + feature = "minimp3", + all(feature = "symphonia-aac", feature = "symphonia-isomp4"), feature = "symphonia-flac", feature = "symphonia-mp3", - feature = "symphonia-isomp4", - feature = "symphonia-ogg", - feature = "symphonia-wav", - feature = "hound", + all(feature = "symphonia-ogg", feature = "symphonia-vorbis"), + all(feature = "symphonia-pcm", feature = "symphonia-wav"), ))] -#[apply(supported_decoders)] +#[apply(all_decoders)] #[trace] -fn seek_possible_after_exausting_source( +fn seek_possible_after_exhausting_source( #[case] format: &'static str, - #[case] _decoder_name: &'static str, + #[case] decoder_name: &'static str, ) { + println!("checking seek possibility after exhausting source for: {format}\t decoded by: {decoder_name}"); + let mut source = get_music(format); while source.next().is_some() {} assert!(source.next().is_none()); @@ -180,25 +154,38 @@ fn seek_possible_after_exausting_source( } #[cfg(any( + feature = "claxon", + feature = "hound", + feature = "lewton", + feature = "minimp3", + all(feature = "symphonia-aac", feature = "symphonia-isomp4"), feature = "symphonia-flac", feature = "symphonia-mp3", - feature = "symphonia-isomp4", - feature = "symphonia-ogg", - feature = "symphonia-wav", - feature = "hound", + all(feature = "symphonia-ogg", feature = "symphonia-vorbis"), + all(feature = "symphonia-pcm", feature = "symphonia-wav"), ))] -#[apply(supported_decoders)] +#[apply(all_decoders)] #[trace] fn seek_does_not_break_channel_order( #[case] format: &'static str, - #[case] _decoder_name: &'static str, + #[case] decoder_name: &'static str, ) { - if format == "m4a" { - // skip this test for m4a while the symphonia decoder has issues with aac timing. - // re-investigate when symphonia 0.5.5 or greater is released. - return; + match (format, decoder_name) { + ("m4a", "symphonia") => { + // skip this test for m4a while the symphonia decoder has issues with aac timing. + // TODO: re-investigate when symphonia 0.5.5 or greater is released. + return; + } + ("mp3", "minimp3") => { + // skip this test for mp3 because seeking is coarse and does not work well with + // the stereo beep file which is variable bitrate. + return; + } + _ => {} } + println!("checking channel order after seek for: {format}\t decoded by: {decoder_name}"); + let mut source = get_rl(format); let channels = source.channels(); assert_eq!(channels.get(), 2, "test needs a stereo beep file"); @@ -243,12 +230,25 @@ fn seek_does_not_break_channel_order( } } -#[cfg(feature = "symphonia-mp3")] -#[test] -fn random_access_seeks() { - // Decoder::new:: does *not* set byte_len and is_seekable - let mp3_file = std::fs::File::open("assets/music.mp3").unwrap(); - let mut decoder = Decoder::new(mp3_file).unwrap(); +#[rstest] +#[cfg(any( + feature = "symphonia-mp3", + all(feature = "symphonia-ogg", feature = "symphonia-vorbis",) +))] +#[cfg_attr( + all(feature = "symphonia-mp3", not(feature = "minimp3")), + case("mp3", "symphonia") +)] +#[cfg_attr( + all(feature = "symphonia-ogg", feature = "symphonia-vorbis",), + case("ogg", "symphonia") +)] +fn random_access_seeks(#[case] format: &'static str, #[case] decoder_name: &'static str) { + println!("checking random access seeks for: {format}\t decoded by: {decoder_name}"); + + // Decoder::new:: does *not* set byte_len or is_seekable + let file = std::fs::File::open(Path::new("assets/music").with_extension(format)).unwrap(); + let mut decoder = Decoder::new(file).unwrap(); assert!( decoder.try_seek(Duration::from_secs(2)).is_ok(), "forward seek should work without byte_len" @@ -256,22 +256,20 @@ fn random_access_seeks() { assert!( matches!( decoder.try_seek(Duration::from_secs(1)), - Err(SeekError::SymphoniaDecoder( - symphonia::SeekError::RandomAccessNotSupported, - )) + Err(SeekError::ForwardOnly) ), "backward seek should fail without byte_len" ); // Decoder::try_from:: sets byte_len and is_seekable - let mut decoder = get_music("mp3"); + let mut decoder = get_music(format); assert!( decoder.try_seek(Duration::from_secs(2)).is_ok(), - "forward seek should work with byte_len" + "forward seek should work with byte_len and is_seekable" ); assert!( decoder.try_seek(Duration::from_secs(1)).is_ok(), - "backward seek should work with byte_len" + "backward seek should work with byte_len and is_seekable" ); } diff --git a/tests/source_traits.rs b/tests/source_traits.rs new file mode 100644 index 00000000..5e612e75 --- /dev/null +++ b/tests/source_traits.rs @@ -0,0 +1,341 @@ +#![allow(dead_code)] +#![allow(unused_imports)] + +use std::io::{Read, Seek}; +use std::path::Path; +use std::time::Duration; + +use rodio::{Decoder, Source}; + +use rstest::rstest; +use rstest_reuse::{self, *}; + +#[cfg(any( + feature = "claxon", + feature = "hound", + feature = "lewton", + feature = "minimp3", + all(feature = "symphonia-aac", feature = "symphonia-isomp4"), + feature = "symphonia-flac", + feature = "symphonia-mp3", + all(feature = "symphonia-ogg", feature = "symphonia-vorbis"), + all(feature = "symphonia-pcm", feature = "symphonia-wav"), +))] +#[template] +#[rstest] +#[cfg_attr( + feature = "claxon", + case("flac", Duration::from_secs_f64(10.152380952), "claxon") +)] +#[cfg_attr( + feature = "hound", + case("wav", Duration::from_secs_f64(10.143469387), "hound") +)] +#[cfg_attr( + feature = "lewton", + case("ogg", Duration::from_secs_f64(69.328979591), "lewton") +)] +#[cfg_attr( + feature = "minimp3", + case("mp3", Duration::from_secs_f64(10.187755102), "minimp3") +)] +#[cfg_attr( + all(feature = "symphonia-aac", feature = "symphonia-isomp4"), + case("m4a", Duration::from_secs_f64(10.188662131), "symphonia m4a") +)] +#[cfg_attr( + all(feature = "symphonia-flac", not(feature = "claxon")), + case("flac", Duration::from_secs_f64(10.152380952), "symphonia flac") +)] +#[cfg_attr( + all(feature = "symphonia-mp3", not(feature = "minimp3")), + case("mp3", Duration::from_secs_f64(10.187755102), "symphonia mp3") +)] +#[cfg_attr( + all( + feature = "symphonia-ogg", + feature = "symphonia-vorbis", + not(feature = "lewton") + ), + case("ogg", Duration::from_secs_f64(69.328979591), "symphonia") +)] +#[cfg_attr( + all( + feature = "symphonia-pcm", + feature = "symphonia-wav", + not(feature = "hound") + ), + case("wav", Duration::from_secs_f64(10.143469387), "symphonia wav") +)] +fn all_decoders( + #[case] format: &'static str, + #[case] correct_duration: Duration, + #[case] decoder_name: &'static str, +) { +} + +#[cfg(any( + feature = "lewton", + feature = "minimp3", + all(feature = "symphonia-aac", feature = "symphonia-isomp4"), + feature = "symphonia-mp3", + all(feature = "symphonia-ogg", feature = "symphonia-vorbis"), +))] +#[template] +#[rstest] +#[cfg_attr(feature = "lewton", case("ogg", "lewton"))] +#[cfg_attr(feature = "minimp3", case("mp3", "minimp3"))] +#[cfg_attr( + all(feature = "symphonia-aac", feature = "symphonia-isomp4"), + case("m4a", "symphonia m4a") +)] +#[cfg_attr( + all(feature = "symphonia-mp3", not(feature = "minimp3")), + case("mp3", "symphonia mp3") +)] +#[cfg_attr( + all( + feature = "symphonia-ogg", + feature = "symphonia-vorbis", + not(feature = "lewton") + ), + case("ogg", "symphonia") +)] +fn decoders_with_variable_spans(#[case] format: &'static str, #[case] decoder_name: &'static str) {} + +#[cfg(any( + feature = "lewton", + feature = "minimp3", + all(feature = "symphonia-aac", feature = "symphonia-isomp4"), + feature = "symphonia-mp3", + all(feature = "symphonia-ogg", feature = "symphonia-vorbis"), +))] +#[template] +#[rstest] +#[cfg_attr(all(feature = "lewton",), case("ogg", "lewton"))] +#[cfg_attr(all(feature = "minimp3"), case("mp3", "minimp3"))] +#[cfg_attr( + all(feature = "symphonia-isomp4", feature = "symphonia-aac"), + case("m4a", "symphonia m4a") +)] +#[cfg_attr( + all(feature = "symphonia-mp3", not(feature = "minimp3")), + case("mp3", "symphonia mp3") +)] +#[cfg_attr( + all( + feature = "symphonia-ogg", + feature = "symphonia-vorbis", + not(feature = "lewton") + ), + case("ogg", "symphonia") +)] +fn lossy_decoders(#[case] format: &'static str, #[case] decoder_name: &'static str) {} + +#[cfg(any( + feature = "claxon", + feature = "hound", + feature = "symphonia-flac", + all(feature = "symphonia-pcm", feature = "symphonia-wav"), +))] +#[template] +#[rstest] +#[cfg_attr(feature = "claxon", case("flac", 16, "claxon"))] +#[cfg_attr(feature = "hound", case("wav", 16, "hound"))] +#[cfg_attr( + all(feature = "symphonia-flac", not(feature = "claxon")), + case("flac", 16, "symphonia flac") +)] +#[cfg_attr( + all( + feature = "symphonia-pcm", + feature = "symphonia-wav", + not(feature = "hound") + ), + case("wav", 16, "symphonia wav") +)] +fn lossless_decoders( + #[case] format: &'static str, + #[case] bit_depth: u32, + #[case] decoder_name: &'static str, +) { +} + +fn get_music(format: &str) -> Decoder { + let asset = Path::new("assets/music").with_extension(format); + let file = std::fs::File::open(asset).unwrap(); + let len = file.metadata().unwrap().len(); + rodio::Decoder::builder() + .with_data(file) + .with_byte_len(len) + .with_seekable(true) + .with_scan_duration(true) + .with_gapless(false) + .build() + .unwrap() +} + +#[cfg(any( + feature = "claxon", + feature = "hound", + feature = "lewton", + feature = "minimp3", + all(feature = "symphonia-aac", feature = "symphonia-isomp4"), + feature = "symphonia-flac", + feature = "symphonia-mp3", + all(feature = "symphonia-ogg", feature = "symphonia-vorbis"), + all(feature = "symphonia-pcm", feature = "symphonia-wav"), +))] +#[apply(all_decoders)] +#[trace] +fn decoder_returns_total_duration( + #[case] format: &'static str, + #[case] correct_duration: Duration, + #[case] decoder_name: &'static str, +) { + println!("decoder: {decoder_name}"); + let decoder = get_music(format); + + let res = decoder + .total_duration() + .unwrap_or_else(|| panic!("did not return a total duration, decoder: {decoder_name}")) + .as_secs_f64(); + let correct_duration = correct_duration.as_secs_f64(); + let abs_diff = (res - correct_duration).abs(); + assert!( + abs_diff < 0.0001, + "decoder got {res}, correct is: {correct_duration}" + ); +} + +#[cfg(any( + feature = "lewton", + feature = "minimp3", + all(feature = "symphonia-aac", feature = "symphonia-isomp4"), + feature = "symphonia-mp3", + all(feature = "symphonia-ogg", feature = "symphonia-vorbis"), +))] +#[apply(decoders_with_variable_spans)] +#[trace] +fn decoder_returns_non_zero_span_length( + #[case] format: &'static str, + #[case] decoder_name: &'static str, +) { + println!("decoder: {decoder_name}"); + let decoder = get_music(format); + let span_len = decoder + .current_span_len() + .expect("decoder should return Some(len) for variable parameter formats"); + + assert!( + span_len > 0, + "decoder {decoder_name} returned Some(0) span length, which indicates a buffering problem with test assets" + ); +} + +#[cfg(any( + feature = "claxon", + feature = "hound", + feature = "lewton", + feature = "minimp3", + all(feature = "symphonia-aac", feature = "symphonia-isomp4"), + feature = "symphonia-flac", + feature = "symphonia-mp3", + all(feature = "symphonia-ogg", feature = "symphonia-vorbis"), + all(feature = "symphonia-pcm", feature = "symphonia-wav"), +))] +#[apply(all_decoders)] +#[trace] +fn decoder_returns_correct_channels( + #[case] format: &'static str, + #[case] _correct_duration: Duration, + #[case] decoder_name: &'static str, +) { + use std::num::NonZero; + + println!("decoder: {decoder_name}"); + let decoder = get_music(format); + let channels = decoder.channels(); + + // All our test files should be stereo (2 channels) + assert_eq!( + channels, + NonZero::new(2).unwrap(), + "decoder {decoder_name} returned {channels} channels, expected 2 (stereo)" + ); +} + +#[cfg(any( + feature = "lewton", + feature = "minimp3", + all(feature = "symphonia-aac", feature = "symphonia-isomp4"), + feature = "symphonia-mp3", + all(feature = "symphonia-ogg", feature = "symphonia-vorbis"), +))] +#[apply(lossy_decoders)] +#[trace] +fn decoder_returns_none_bit_depth( + #[case] format: &'static str, + #[case] decoder_name: &'static str, +) { + println!("decoder: {decoder_name}"); + let decoder = get_music(format); + let bit_depth = decoder.bits_per_sample(); + + assert!( + bit_depth.is_none(), + "decoder {decoder_name} returned Some({:?}) bit depth, expected None for lossy formats", + bit_depth.unwrap() + ); +} + +#[cfg(any( + feature = "claxon", + feature = "hound", + feature = "symphonia-flac", + all(feature = "symphonia-pcm", feature = "symphonia-wav"), +))] +#[apply(lossless_decoders)] +#[trace] +fn decoder_returns_some_bit_depth( + #[case] format: &'static str, + #[case] bit_depth: u32, + #[case] decoder_name: &'static str, +) { + println!("decoder: {decoder_name}"); + let decoder = get_music(format); + let returned_bit_depth = decoder.bits_per_sample(); + assert_eq!( + returned_bit_depth, + Some(bit_depth), + "decoder {decoder_name} returned {returned_bit_depth:?} bit depth, expected {bit_depth}" + ); +} +#[cfg(any( + feature = "claxon", + feature = "hound", + feature = "symphonia-flac", + all(feature = "symphonia-pcm", feature = "symphonia-wav") +))] +#[test] +fn decoder_returns_hi_res_bit_depths() { + const CASES: [(&str, u32); 3] = [ + ("audacity24bit_level5.flac", 24), + ("audacity32bit.wav", 32), + ("audacity32bit_int.wav", 32), + ]; + + for (asset, bit_depth) in CASES { + let file = std::fs::File::open(format!("assets/{asset}")).unwrap(); + if let Ok(decoder) = rodio::Decoder::try_from(file) { + // TODO: Symphonia returns None for audacity32bit.wav (float) + if let Some(returned_bit_depth) = decoder.bits_per_sample() { + assert_eq!( + returned_bit_depth, + bit_depth, + "decoder for {asset} returned {returned_bit_depth:?} bit depth, expected {bit_depth}" + ); + } + } + } +} diff --git a/tests/total_duration.rs b/tests/total_duration.rs deleted file mode 100644 index 8d56a8c4..00000000 --- a/tests/total_duration.rs +++ /dev/null @@ -1,107 +0,0 @@ -#![allow(dead_code)] -#![allow(unused_imports)] - -use std::io::{Read, Seek}; -use std::path::Path; -use std::time::Duration; - -use rodio::{Decoder, Source}; - -use rstest::rstest; -use rstest_reuse::{self, *}; - -#[cfg(any( - feature = "claxon", - feature = "minimp3", - feature = "symphonia-aac", - feature = "symphonia-flac", - feature = "symphonia-mp3", - feature = "symphonia-isomp4", - feature = "symphonia-ogg", - feature = "symphonia-wav", - feature = "hound", -))] -#[template] -#[rstest] -#[cfg_attr( - feature = "symphonia-vorbis", - case("ogg", Duration::from_secs_f64(69.328979591), "symphonia") -)] -#[cfg_attr( - all(feature = "minimp3", not(feature = "symphonia-mp3")), - case("mp3", Duration::ZERO, "minimp3") -)] -#[cfg_attr( - all(feature = "hound", not(feature = "symphonia-wav")), - case("wav", Duration::from_secs_f64(10.143469387), "hound") -)] -#[cfg_attr( - all(feature = "claxon", not(feature = "symphonia-flac")), - case("flac", Duration::from_secs_f64(10.152380952), "claxon") -)] -#[cfg_attr( - feature = "symphonia-mp3", - case("mp3", Duration::from_secs_f64(10.187755102), "symphonia mp3") -)] -#[cfg_attr( - feature = "symphonia-isomp4", - case("m4a", Duration::from_secs_f64(10.188662131), "symphonia m4a") -)] -#[cfg_attr( - feature = "symphonia-wav", - case("wav", Duration::from_secs_f64(10.143469387), "symphonia wav") -)] -#[cfg_attr( - feature = "symphonia-flac", - case("flac", Duration::from_secs_f64(10.152380952), "symphonia flac") -)] -fn all_decoders( - #[case] format: &'static str, - #[case] correct_duration: Duration, - #[case] decoder_name: &'static str, -) { -} - -fn get_music(format: &str) -> Decoder { - let asset = Path::new("assets/music").with_extension(format); - let file = std::fs::File::open(asset).unwrap(); - let len = file.metadata().unwrap().len(); - rodio::Decoder::builder() - .with_data(file) - .with_byte_len(len) - .with_seekable(true) - .with_gapless(false) - .build() - .unwrap() -} - -#[cfg(any( - feature = "claxon", - feature = "minimp3", - feature = "symphonia-flac", - feature = "symphonia-mp3", - feature = "symphonia-isomp4", - feature = "symphonia-ogg", - feature = "symphonia-wav", - feature = "hound", -))] -#[apply(all_decoders)] -#[trace] -fn decoder_returns_total_duration( - #[case] format: &'static str, - #[case] correct_duration: Duration, - #[case] decoder_name: &'static str, -) { - eprintln!("decoder: {decoder_name}"); - let decoder = get_music(format); - let res = decoder - .total_duration() - .unwrap_or_else(|| panic!("did not return a total duration, decoder: {decoder_name}")) - .as_secs_f64(); - let correct_duration = correct_duration.as_secs_f64(); - let abs_diff = (res - correct_duration).abs(); - assert!( - abs_diff < 0.0001, - "decoder got {res}, correct is: {correct_duration}" - ); -} From a2fc2724fb603eab536b4d887157b846714603ee Mon Sep 17 00:00:00 2001 From: David Kleingeld Date: Fri, 22 Aug 2025 17:11:16 +0200 Subject: [PATCH 02/17] adds mic docs & example + fixes mic yielding zeros --- src/microphone.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/microphone.rs b/src/microphone.rs index 4c7bfa6b..d191ba3b 100644 --- a/src/microphone.rs +++ b/src/microphone.rs @@ -181,6 +181,20 @@ impl Source for Microphone { self.config.sample_rate } + // TODO: use cpal::SampleFormat::bits_per_sample() when cpal v0.17 is released + fn bits_per_sample(&self) -> Option { + let bits = match self.config.sample_format { + cpal::SampleFormat::I8 | cpal::SampleFormat::U8 => 8, + cpal::SampleFormat::I16 | cpal::SampleFormat::U16 => 16, + cpal::SampleFormat::I24 => 24, + cpal::SampleFormat::I32 | cpal::SampleFormat::U32 | cpal::SampleFormat::F32 => 32, + cpal::SampleFormat::I64 | cpal::SampleFormat::U64 | cpal::SampleFormat::F64 => 64, + _ => return None, + }; + + Some(bits) + } + fn total_duration(&self) -> Option { None } From 5253ff257bb379268c1fcbf0f07209e9914c2750 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 24 Aug 2025 13:09:30 +0200 Subject: [PATCH 03/17] ci: add silent MP3 file and test for decoder silence (#411) --- assets/silence.mp3 | Bin 0 -> 1457 bytes tests/mp3_test.rs | 9 +++++++++ 2 files changed, 9 insertions(+) create mode 100644 assets/silence.mp3 create mode 100644 tests/mp3_test.rs diff --git a/assets/silence.mp3 b/assets/silence.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..4b8b8cbebb329df114e9125dc85940eeb9bc940c GIT binary patch literal 1457 zcmezWd%_V0bP$o5mkt!;2VyP;2G)%Xk}zNl2R?9+1_uprFcS_o!@(Iicm@am;Q(lt zjgO Date: Sun, 24 Aug 2025 15:51:24 +0200 Subject: [PATCH 04/17] fix: MP3 format probe to reject WAV files with minimp3 Without this, minimp3 would incorrectly accept WAV files as MP3 when both `minimp3` and Symphonia's `wav` features are enabled. --- src/decoder/mp3.rs | 6 +++++- tests/seek.rs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/decoder/mp3.rs b/src/decoder/mp3.rs index f8cf60a0..a029c82f 100644 --- a/src/decoder/mp3.rs +++ b/src/decoder/mp3.rs @@ -912,6 +912,10 @@ where { utils::probe_format(data, |reader| { let mut decoder = Decoder::new(reader); - decoder.next_frame().is_ok() + decoder.next_frame().is_ok_and(|frame| { + // Without this check, minimp3 will think it can decode WAV files. This will trigger by + // running the test suite with features `minimp3` and (Symphonia) `wav` enabled. + frame.bitrate != 0 + }) }) } diff --git a/tests/seek.rs b/tests/seek.rs index 60759094..898a2c3c 100644 --- a/tests/seek.rs +++ b/tests/seek.rs @@ -240,7 +240,11 @@ fn seek_does_not_break_channel_order( case("mp3", "symphonia") )] #[cfg_attr( - all(feature = "symphonia-ogg", feature = "symphonia-vorbis",), + all( + feature = "symphonia-ogg", + feature = "symphonia-vorbis", + not(feature = "lewton") + ), case("ogg", "symphonia") )] fn random_access_seeks(#[case] format: &'static str, #[case] decoder_name: &'static str) { From 16c3673800ff3491d861b63f1e67f3adfa62ba13 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 25 Aug 2025 22:43:13 +0200 Subject: [PATCH 05/17] feat: add Decoder support for Bytes, Arc, Box, and Vec types Fixes #141 --- Cargo.lock | 1 + Cargo.toml | 3 + src/decoder/builder.rs | 2 +- src/decoder/mod.rs | 351 ++++++++++++++++++++++++++++++++++++++--- 4 files changed, 331 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d94e3921..33dcaa36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1040,6 +1040,7 @@ version = "0.21.1" dependencies = [ "approx", "atomic_float", + "bytes", "claxon", "cpal", "crossbeam-channel", diff --git a/Cargo.toml b/Cargo.toml index a1c73540..1acf5eba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ wav_output = ["dep:hound"] tracing = ["dep:tracing"] # Experimental features using atomic floating-point operations experimental = ["dep:atomic_float"] +# Enable support for bytes::Bytes type in audio sources +bytes = ["dep:bytes"] # Audio generation features # @@ -92,6 +94,7 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] +bytes = { version = "1", optional = true } cpal = { version = "0.16", optional = true } dasp_sample = "0.11.0" claxon = { version = "0.4.2", optional = true } diff --git a/src/decoder/builder.rs b/src/decoder/builder.rs index ce12144d..596a7ccd 100644 --- a/src/decoder/builder.rs +++ b/src/decoder/builder.rs @@ -385,7 +385,7 @@ impl Default for Settings { mime_type: None, is_seekable: false, total_duration: None, - scan_duration: false, + scan_duration: true, } } } diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 49647c78..1181e917 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -294,27 +294,25 @@ impl DecoderImpl { } } -/// Converts a `File` into a `Decoder` with automatic optimizations. -/// This is the preferred way to decode files as it enables seeking optimizations -/// and accurate duration calculations. +/// Converts a `File` into a `Decoder`. /// -/// This implementation: -/// - Wraps the file in a `BufReader` for better performance -/// - Gets the file length from metadata to improve seeking operations and duration accuracy -/// - Enables seeking by default +/// This is the recommended way to decode audio files from the filesystem. The file is +/// automatically wrapped in a `BufReader` for efficient I/O, and the decoder will know the exact +/// file size for optimal seeking performance. /// /// # Errors /// -/// Returns an error if: -/// - The file metadata cannot be read -/// - The audio format cannot be recognized or is not supported +/// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined or is +/// not supported. +/// +/// Returns `DecoderError::IoError` if the file metadata cannot be read. /// /// # Examples /// ```no_run /// use std::fs::File; /// use rodio::Decoder; /// -/// let file = File::open("audio.mp3").unwrap(); +/// let file = File::open("music.mp3").unwrap(); /// let decoder = Decoder::try_from(file).unwrap(); /// ``` impl TryFrom for Decoder> { @@ -330,19 +328,20 @@ impl TryFrom for Decoder> { .with_data(BufReader::new(file)) .with_byte_len(len) .with_seekable(true) - .with_scan_duration(true) .build() } } -/// Converts a `BufReader` into a `Decoder`. -/// When working with files, prefer `TryFrom` as it will automatically set byte_len -/// for better seeking performance. +/// Converts a `BufReader` into a `Decoder`. +/// +/// This is useful for decoding from any readable and seekable source wrapped in a `BufReader`. +/// When working with files specifically, prefer `TryFrom` as it automatically determines the +/// file size for better seeking performance. /// /// # Errors /// -/// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined -/// or is not supported. +/// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined or is +/// not supported. /// /// # Examples /// ```no_run @@ -361,20 +360,22 @@ where type Error = DecoderError; fn try_from(data: BufReader) -> Result { - Self::new(data) + Self::builder().with_data(data).with_seekable(true).build() } } -/// Converts a `Cursor` into a `Decoder`. -/// When working with files, prefer `TryFrom` as it will automatically set byte_len -/// for better seeking performance. +/// Converts a `Cursor` into a `Decoder`. /// -/// This is useful for decoding audio data that's already in memory. +/// This is useful for decoding audio data that's already wrapped in a `Cursor`. The decoder will +/// know the exact size of the data for efficient seeking and duration calculation. +/// +/// For unwrapped byte containers, prefer the direct `TryFrom` implementations for `Vec`, +/// `Box<[u8]>`, `Arc<[u8]>`, or `bytes::Bytes`. /// /// # Errors /// -/// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined -/// or is not supported. +/// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined or is +/// not supported. /// /// # Examples /// ```no_run @@ -392,7 +393,307 @@ where type Error = DecoderError; fn try_from(data: std::io::Cursor) -> Result { - Self::new(data) + let len = data.get_ref().as_ref().len() as u64; + + Self::builder() + .with_data(data) + .with_byte_len(len) + .with_seekable(true) + .build() + } +} + +/// Helper function to create a decoder from data that can be converted to bytes. +/// +/// This function wraps the data in a `Cursor` and configures the decoder with optimal settings for +/// in-memory audio data: known byte length and seeking enabled for better performance. +fn decoder_from_bytes(data: T) -> Result>, DecoderError> +where + T: AsRef<[u8]> + Send + Sync + 'static, +{ + let len = data.as_ref().len() as u64; + let cursor = std::io::Cursor::new(data); + + Decoder::builder() + .with_data(cursor) + .with_byte_len(len) + .with_seekable(true) + .build() +} + +/// Converts a `Vec` into a `Decoder`. +/// +/// This is useful for decoding audio data that's loaded into memory as a vector. The data is +/// wrapped in a `Cursor` to provide seeking capabilities. The decoder will know the exact size of +/// the audio data, enabling efficient seeking. +/// +/// # Errors +/// +/// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined or is +/// not supported. +/// +/// # Examples +/// ```no_run +/// use rodio::Decoder; +/// +/// // Load audio file into memory +/// let audio_data = std::fs::read("music.mp3").unwrap(); +/// let decoder = Decoder::try_from(audio_data).unwrap(); +/// ``` +impl TryFrom> for Decoder>> { + type Error = DecoderError; + + fn try_from(data: Vec) -> Result { + decoder_from_bytes(data) + } +} + +/// Converts a `Box<[u8]>` into a `Decoder`. +/// +/// This is useful for decoding audio data with exact memory allocation (no extra capacity like +/// `Vec` might have). The boxed slice is memory-efficient and signals that the audio data is +/// immutable and final. +/// +/// # Errors +/// +/// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined or is +/// not supported. +/// +/// # Examples +/// ```no_run +/// use rodio::Decoder; +/// +/// let audio_vec = std::fs::read("audio.flac").unwrap(); +/// let audio_box: Box<[u8]> = audio_vec.into_boxed_slice(); +/// let decoder = Decoder::try_from(audio_box).unwrap(); +/// ``` +impl TryFrom> for Decoder>> { + type Error = DecoderError; + + fn try_from(data: Box<[u8]>) -> Result { + decoder_from_bytes(data) + } +} + +/// Converts an `Arc<[u8]>` into a `Decoder`. +/// +/// This is useful for sharing audio data across multiple decoders or threads without copying the +/// underlying bytes. Perfect for scenarios where you need multiple decoders for the same audio +/// data (e.g., playing overlapping sound effects in games). +/// +/// # Errors +/// +/// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined or is +/// not supported. +/// +/// # Examples +/// ```no_run +/// use std::sync::Arc; +/// use rodio::Decoder; +/// +/// let audio_data: Arc<[u8]> = Arc::from(std::fs::read("sound.wav").unwrap()); +/// +/// // Create multiple decoders sharing the same data (no copying!) +/// let decoder1 = Decoder::try_from(audio_data.clone()).unwrap(); +/// let decoder2 = Decoder::try_from(audio_data).unwrap(); +/// ``` +impl TryFrom> for Decoder>> { + type Error = DecoderError; + + fn try_from(data: std::sync::Arc<[u8]>) -> Result { + decoder_from_bytes(data) + } +} + +/// Converts a `bytes::Bytes` into a `Decoder`. +/// +/// This is particularly useful in async/web applications where audio data is received from HTTP +/// clients, message queues, or other network sources. `Bytes` provides efficient, reference-counted +/// sharing of byte data. +/// +/// This implementation is only available when the `bytes` feature is enabled. +/// +/// # Errors +/// +/// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined or is +/// not supported. +/// +/// # Examples +/// ```ignore +/// use rodio::Decoder; +/// use bytes::Bytes; +/// +/// // Common in web applications +/// let audio_response = reqwest::get("https://example.com/audio.mp3").await.unwrap(); +/// let audio_bytes: Bytes = audio_response.bytes().await.unwrap(); +/// let decoder = Decoder::try_from(audio_bytes).unwrap(); +/// ``` +#[cfg(feature = "bytes")] +#[cfg_attr(docsrs, doc(cfg(feature = "bytes")))] +impl TryFrom for Decoder> { + type Error = DecoderError; + + fn try_from(data: bytes::Bytes) -> Result { + decoder_from_bytes(data) + } +} + +/// Converts a `&'static [u8]` into a `Decoder`. +/// +/// This is useful for decoding audio data that's embedded directly in the binary, such as sound +/// effects in games or applications. The static lifetime ensures the data remains valid for the +/// decoder's lifetime. +/// +/// # Errors +/// +/// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined or is +/// not supported. +/// +/// # Examples +/// ```no_run +/// use rodio::Decoder; +/// +/// // Embedded audio data (e.g., from include_bytes!) +/// static AUDIO_DATA: &[u8] = include_bytes!("music.wav"); +/// let decoder = Decoder::try_from(AUDIO_DATA).unwrap(); +/// ``` +impl TryFrom<&'static [u8]> for Decoder> { + type Error = DecoderError; + + fn try_from(data: &'static [u8]) -> Result { + decoder_from_bytes(data) + } +} + +/// Converts a `Cow<'static, [u8]>` into a `Decoder`. +/// +/// This is useful for APIs that want to accept either borrowed static data or owned data without +/// requiring callers to choose upfront. The cow can contain either embedded audio data or +/// dynamically loaded data. +/// +/// # Errors +/// +/// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined or is +/// not supported. +/// +/// # Examples +/// ```no_run +/// use rodio::Decoder; +/// use rodio::decoder::DecoderError; +/// use std::borrow::Cow; +/// +/// // Can accept both owned and borrowed data +/// fn decode_audio(data: Cow<'static, [u8]>) -> Result>>, DecoderError> { +/// Decoder::try_from(data) +/// } +/// +/// static EMBEDDED: &[u8] = include_bytes!("music.wav"); +/// let decoder1 = decode_audio(Cow::Borrowed(EMBEDDED)).unwrap(); +/// let owned_data = std::fs::read("music.wav").unwrap(); +/// let decoder2 = decode_audio(Cow::Owned(owned_data)).unwrap(); +/// ``` +impl TryFrom> + for Decoder>> +{ + type Error = DecoderError; + + fn try_from(data: std::borrow::Cow<'static, [u8]>) -> Result { + decoder_from_bytes(data) + } +} + +/// Converts a `PathBuf` into a `Decoder`. +/// +/// This is a convenience method for loading audio files from filesystem paths. The file is opened +/// and automatically configured with optimal settings including file size detection and seeking +/// support. +/// +/// # Errors +/// +/// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined or is +/// not supported. +/// +/// Returns `DecoderError::IoError` if the file cannot be opened or its metadata cannot be read. +/// +/// # Examples +/// ```no_run +/// use rodio::Decoder; +/// use std::path::PathBuf; +/// +/// let path = PathBuf::from("music.mp3"); +/// let decoder = Decoder::try_from(path).unwrap(); +/// ``` +impl TryFrom for Decoder> { + type Error = DecoderError; + + fn try_from(path: std::path::PathBuf) -> Result { + let file = std::fs::File::open(path).map_err(|e| Self::Error::IoError(e.to_string()))?; + Self::try_from(file) + } +} + +/// Converts a `&Path` into a `Decoder`. +/// +/// This is a convenience method for loading audio files from filesystem paths. The file is opened +/// and automatically configured with optimal settings including file size detection and seeking +/// support. +/// +/// # Errors +/// +/// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined or is +/// not supported. +/// +/// Returns `DecoderError::IoError` if the file cannot be opened or its metadata cannot be read. +/// +/// # Examples +/// ```no_run +/// use rodio::Decoder; +/// use std::path::Path; +/// +/// let path = Path::new("music.mp3"); +/// let decoder = Decoder::try_from(path).unwrap(); +/// ``` +impl TryFrom<&std::path::Path> for Decoder> { + type Error = DecoderError; + + fn try_from(path: &std::path::Path) -> Result { + let file = std::fs::File::open(path).map_err(|e| Self::Error::IoError(e.to_string()))?; + Self::try_from(file) + } +} + +impl Decoder> { + /// Creates a `Decoder` from any path-like type. + /// + /// This is a convenience method that accepts anything that can be converted to a `Path`, + /// including `&str`, `String`, `&Path`, `PathBuf`, etc. The file is opened and automatically + /// configured with optimal settings including file size detection and seeking support. + /// + /// # Errors + /// + /// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined + /// or is not supported. + /// + /// Returns `DecoderError::IoError` if the file cannot be opened or its metadata cannot be read. + /// + /// # Examples + /// ```no_run + /// use rodio::Decoder; + /// use std::path::Path; + /// + /// // Works with &str + /// let decoder = Decoder::from_path("music.mp3").unwrap(); + /// + /// // Works with String + /// let path = String::from("music.mp3"); + /// let decoder = Decoder::from_path(path).unwrap(); + /// + /// // Works with Path and PathBuf + /// let decoder = Decoder::from_path(Path::new("music.mp3")).unwrap(); + /// ``` + pub fn from_path(path: impl AsRef) -> Result { + let file = std::fs::File::open(path).map_err(|e| DecoderError::IoError(e.to_string()))?; + Self::try_from(file) } } From 5e815adf2eb2888a71fd7158c84886b1eb42122d Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 25 Aug 2025 22:49:32 +0200 Subject: [PATCH 06/17] docs: add doc(cfg(...)) attributes for docsrs feature documentation --- src/decoder/mod.rs | 4 ++++ src/lib.rs | 3 +++ src/source/agc.rs | 1 + 3 files changed, 8 insertions(+) diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 1181e917..72a2acc4 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -1174,6 +1174,7 @@ pub enum DecoderError { /// The stream contained malformed data and could not be decoded or demuxed. #[error("The stream contained malformed data and could not be decoded or demuxed: {0}")] #[cfg(feature = "symphonia")] + #[cfg_attr(docsrs, doc(cfg(feature = "symphonia")))] DecodeError(&'static str), /// A default or user-defined limit was reached while decoding or demuxing @@ -1183,16 +1184,19 @@ pub enum DecoderError { "A default or user-defined limit was reached while decoding or demuxing the stream: {0}" )] #[cfg(feature = "symphonia")] + #[cfg_attr(docsrs, doc(cfg(feature = "symphonia")))] LimitError(&'static str), /// The demuxer or decoder needs to be reset before continuing. #[error("The demuxer or decoder needs to be reset before continuing.")] #[cfg(feature = "symphonia")] + #[cfg_attr(docsrs, doc(cfg(feature = "symphonia")))] ResetRequired, /// No streams were found by the decoder. #[error("No streams were found by the decoder.")] #[cfg(feature = "symphonia")] + #[cfg_attr(docsrs, doc(cfg(feature = "symphonia")))] NoStreams, } assert_error_traits!(DecoderError); diff --git a/src/lib.rs b/src/lib.rs index e4b8ba43..39c456a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -163,6 +163,7 @@ )] #[cfg(feature = "playback")] +#[cfg_attr(docsrs, doc(cfg(feature = "playback")))] pub use cpal::{ self, traits::DeviceTrait, Device, Devices, DevicesError, InputDevices, OutputDevices, SupportedStreamConfig, @@ -172,6 +173,7 @@ mod common; mod sink; mod spatial_sink; #[cfg(feature = "playback")] +#[cfg_attr(docsrs, doc(cfg(feature = "playback")))] pub mod stream; #[cfg(feature = "wav_output")] #[cfg_attr(docsrs, doc(cfg(feature = "wav_output")))] @@ -195,6 +197,7 @@ pub use crate::sink::Sink; pub use crate::source::Source; pub use crate::spatial_sink::SpatialSink; #[cfg(feature = "playback")] +#[cfg_attr(docsrs, doc(cfg(feature = "playback")))] pub use crate::stream::{play, OutputStream, OutputStreamBuilder, PlayError, StreamError}; #[cfg(feature = "wav_output")] #[cfg_attr(docsrs, doc(cfg(feature = "wav_output")))] diff --git a/src/source/agc.rs b/src/source/agc.rs index befffe75..a7711815 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -41,6 +41,7 @@ const fn power_of_two(n: usize) -> usize { const RMS_WINDOW_SIZE: usize = power_of_two(8192); #[cfg(feature = "experimental")] +#[cfg_attr(docsrs, doc(cfg(feature = "experimental")))] /// Automatic Gain Control filter for maintaining consistent output levels. /// /// This struct implements an AGC algorithm that dynamically adjusts audio levels From 5474b22bf7a7ca26ee65042c87233ddd4d5d6027 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 31 Aug 2025 22:22:35 +0200 Subject: [PATCH 07/17] docs: rewrite and clarify decoder documentation and API comments - Improve clarity and conciseness of doc comments for all decoders - Remove redundant and overly verbose explanations - Add missing details to trait and method docs - Update examples to use consistent file paths and types - Move Settings struct visibility to crate-internal - Refactor Path/PathBuf decoder constructors for optimal hinting - Standardize iterator and trait method documentation across formats - Remove misleading or outdated implementation notes - Fix Zero source bits_per_sample to return None - Update seek test attributes for feature consistency --- src/decoder/builder.rs | 280 ++++++++++++++------------------ src/decoder/flac.rs | 71 +++----- src/decoder/mod.rs | 112 ++++++------- src/decoder/mp3.rs | 42 +---- src/decoder/read_seek_source.rs | 9 +- src/decoder/symphonia.rs | 152 ++++------------- src/decoder/vorbis.rs | 109 ++----------- src/decoder/wav.rs | 73 ++------- src/source/mod.rs | 29 +++- src/source/zero.rs | 2 +- tests/seek.rs | 11 +- 11 files changed, 298 insertions(+), 592 deletions(-) diff --git a/src/decoder/builder.rs b/src/decoder/builder.rs index 596a7ccd..0ef8a205 100644 --- a/src/decoder/builder.rs +++ b/src/decoder/builder.rs @@ -42,7 +42,7 @@ //! let decoder = Decoder::builder() //! .with_data(file) //! .with_byte_len(len) // Enable seeking and duration calculation -//! .with_hint("mp3") // Optional format hint for performance +//! .with_hint("mp3") // Reduce format detection overhead //! .with_gapless(true) // Enable gapless playback //! .build()?; //! @@ -217,161 +217,30 @@ pub enum SeekMode { /// - Some settings are universal (e.g., `is_seekable`) /// - Others are format-specific (e.g., `gapless` for supported formats) /// - Unsupported settings are typically ignored gracefully -/// -/// # Performance Impact -/// -/// Several settings significantly affect performance: -/// - `byte_len`: Enables efficient seeking and duration calculation -/// - `hint`/`mime_type`: Reduce format detection overhead -/// - `scan_duration`: Controls expensive file analysis operations -/// - `seek_mode`: Balances seeking speed vs. accuracy #[derive(Clone, Debug)] -pub struct Settings { +pub(super) struct Settings { /// The total length of the stream in bytes. - /// - /// This setting enables several important optimizations: - /// - **Seeking operations**: Required for reliable backward seeking - /// - **Duration calculations**: Essential for formats lacking timing metadata - /// - **Progress indication**: Enables accurate progress tracking - /// - **Buffer optimization**: Helps with memory management decisions - /// - /// # Format Requirements - /// - /// - **MP3**: Required for coarse seeking and duration scanning - /// - **OGG Vorbis**: Used for duration scanning optimization - /// - **FLAC/WAV**: Improves seeking performance but not strictly required - /// - **Symphonia**: May be used for internal optimizations - /// - /// # Obtaining Byte Length - /// - /// Can be obtained from: - /// - File metadata: `file.metadata()?.len()` - /// - Stream seeking: `stream.seek(SeekFrom::End(0))?` - /// - HTTP Content-Length headers for network streams pub byte_len: Option, /// The seeking mode controlling speed vs. accuracy trade-offs. - /// - /// This setting affects seeking behavior across all decoder implementations - /// that support seeking operations. The actual behavior depends on format - /// capabilities and available optimizations. pub seek_mode: SeekMode, /// Whether to enable gapless playback by trimming padding frames. - /// - /// When enabled, removes silence padding added during encoding to achieve - /// seamless transitions between tracks. This is particularly important for - /// albums designed for continuous playback. - /// - /// # Format Support - /// - /// - **MP3**: Removes encoder delay and padding frames - /// - **AAC**: Removes padding specified in container metadata - /// - **FLAC**: Generally gapless by nature, minimal effect - /// - **OGG Vorbis**: Handles sample-accurate boundaries - /// - /// # Duration Impact - /// - /// Disabling gapless may affect duration calculations as padding frames - /// will be included in the total sample count for some formats. pub gapless: bool, /// Format extension hint for accelerated format detection. - /// - /// Providing an accurate hint significantly improves decoder initialization - /// performance by reducing the number of format probes required. Common - /// values include file extensions without the dot. - /// - /// # Common Values - /// - /// - Audio formats: "mp3", "flac", "wav", "ogg", "m4a" - /// - Container hints: "mp4", "mkv", "webm" - /// - Codec hints: "aac", "opus", "vorbis" - /// - /// # Performance Impact - /// - /// Without hints, decoders must probe all supported formats sequentially, - /// which can be slow for large format lists or complex containers. pub hint: Option, /// MIME type hint for container format identification. - /// - /// Provides additional information for format detection, particularly useful - /// for network streams where file extensions may not be available. This - /// complements the extension hint for comprehensive format identification. - /// - /// # Common Values - /// - /// - "audio/mpeg" (MP3) - /// - "audio/flac" (FLAC) - /// - "audio/ogg" (OGG Vorbis/Opus) - /// - "audio/mp4" or "audio/aac" (AAC in MP4) - /// - "audio/wav" or "audio/vnd.wav" (WAV) pub mime_type: Option, /// Whether the decoder should report seeking capabilities. - /// - /// This setting controls whether the decoder will attempt backward seeking - /// operations. When disabled, only forward seeking (sample skipping) is - /// allowed, which is suitable for streaming scenarios. - /// - /// # Requirements - /// - /// For reliable seeking behavior: - /// - The underlying stream must support `Seek` trait - /// - `byte_len` should be set for optimal performance - /// - Some formats may have additional requirements - /// - /// # Automatic Setting - /// - /// This is automatically set to `true` when `byte_len` is provided, - /// as byte length information typically implies seekable streams. pub is_seekable: bool, /// Pre-computed total duration to avoid expensive file scanning. - /// - /// When provided, decoders will use this value instead of performing - /// potentially expensive duration calculation operations. This is - /// particularly useful when duration is known from external metadata. - /// - /// # Use Cases - /// - /// - Database-stored track durations - /// - Previously calculated durations - /// - External metadata sources (ID3, database, etc.) - /// - Avoiding redundant file scanning in batch operations - /// - /// # Priority - /// - /// This setting takes precedence over `scan_duration` when both are set. pub total_duration: Option, /// Enable expensive file scanning for accurate duration computation. - /// - /// When enabled, allows decoders to perform comprehensive file analysis - /// to determine accurate duration information. This can be slow for large - /// files but provides the most accurate duration data. - /// - /// # Prerequisites - /// - /// This setting only takes effect when: - /// - `is_seekable` is `true` - /// - `byte_len` is set - /// - The decoder supports duration scanning - /// - /// # Format Behavior - /// - /// - **MP3**: Scans for XING/VBRI headers, then frame-by-frame if needed - /// - **OGG Vorbis**: Uses binary search to find last granule position - /// - **FLAC**: Duration available in metadata, no scanning needed - /// - **WAV**: Duration available in header, no scanning needed - /// - /// # Performance Impact - /// - /// Scanning time varies significantly: - /// - Small files (< 10MB): Usually very fast - /// - Large files (> 100MB): Can take several seconds - /// - Variable bitrate files: May require more extensive scanning pub scan_duration: bool, } @@ -546,16 +415,28 @@ impl DecoderBuilder { } /// Sets the byte length of the stream. - /// This is required for: - /// - Reliable seeking operations - /// - Duration calculations in formats that lack timing information (e.g. MP3, Vorbis) - /// when `scan_duration` is enabled - /// - Symphonia decoders may also use this for internal scanning operations + /// + /// Depending on the format this can enable several important optimizations: + /// - **Seeking operations**: Required for reliable backward seeking + /// - **Duration calculations**: Essential for formats lacking timing metadata + /// - **Progress indication**: Enables accurate progress tracking + /// - **Buffer optimization**: Helps with memory management decisions /// /// Note that this also sets `is_seekable` to `true`. /// - /// To enable duration scanning for formats that require it, use `with_scan_duration(true)`. - /// File-based decoders (`try_from(File)`) automatically enable scanning. + /// # Format Requirements + /// + /// - **MP3**: Required for coarse seeking and duration scanning + /// - **OGG Vorbis**: Used for duration scanning optimization + /// - **FLAC/WAV**: Improves seeking performance but not strictly required + /// - **Symphonia**: May be used for internal optimizations + /// + /// # Obtaining Byte Length + /// + /// Can be obtained from: + /// - File metadata: `file.metadata()?.len()` + /// - Stream seeking: `stream.seek(SeekFrom::End(0))?` + /// - HTTP Content-Length headers for network streams /// /// `DecoderBuilder::try_from::()` automatically sets this from file metadata. /// Alternatively, you can set it manually from file metadata: @@ -592,6 +473,10 @@ impl DecoderBuilder { } /// Sets the seeking mode for the decoder. + /// + /// This setting affects seeking behavior across all decoder implementations + /// that support seeking operations. The actual behavior depends on format + /// capabilities and available optimizations. pub fn with_seek_mode(mut self, seek_mode: SeekMode) -> Self { self.settings.seek_mode = seek_mode; self @@ -617,7 +502,22 @@ impl DecoderBuilder { /// Enables or disables gapless playback. This is enabled by default. /// - /// When enabled, removes silence between tracks for formats that support it. + /// When enabled, removes silence padding added during encoding to achieve + /// seamless transitions between tracks. This is particularly important for + /// albums designed for continuous playback. + /// + /// # Format Support + /// + /// - **MP3**: Removes encoder delay and padding frames + /// - **AAC**: Removes padding specified in container metadata + /// - **FLAC**: Generally gapless by nature, minimal effect + /// - **OGG Vorbis**: Handles sample-accurate boundaries + /// + /// # Duration Impact + /// + /// Enabling gapless may affect duration calculations as padding frames + /// will be excluded in the total sample count for some decoders. If you + /// need consistent duration reporting across decoders, consider disabling this. pub fn with_gapless(mut self, gapless: bool) -> Self { self.settings.gapless = gapless; self @@ -625,8 +525,21 @@ impl DecoderBuilder { /// Sets a format hint for the decoder. /// - /// When known, this can help the decoder to select the correct codec faster. - /// Common values are "mp3", "wav", "flac", "ogg", etc. + /// Providing an accurate hint significantly improves decoder initialization + /// performance by reducing the number of format probes required. Common + /// values include file extensions without the dot. + /// + /// # Common Values + /// + /// - Codec hints: "aac", "flac", "mp3", "wav" + /// - Container hints: "audio/x-matroska", "audio/mp4", "audio/ogg" + /// + /// For audio within a container, such as MKV, MP4 or Ogg, use the container hint. + /// + /// # Performance Impact + /// + /// Without hints, decoders must probe all supported formats sequentially, + /// which can be slow for large format lists or complex containers. pub fn with_hint(mut self, hint: &str) -> Self { self.settings.hint = Some(hint.to_string()); self @@ -634,8 +547,17 @@ impl DecoderBuilder { /// Sets a mime type hint for the decoder. /// - /// When known, this can help the decoder to select the correct demuxer faster. - /// Common values are "audio/mpeg", "audio/vnd.wav", "audio/flac", "audio/ogg", etc. + /// Provides additional information for format detection, particularly useful + /// for network streams where file extensions may not be available. This + /// complements the extension hint for comprehensive format identification. + /// + /// # Common Values + /// + /// - "audio/mpeg" (MP3) + /// - "audio/flac" (FLAC) + /// - "audio/ogg" (OGG Vorbis/Opus) + /// - "audio/mp4" or "audio/aac" (AAC in MP4) + /// - "audio/wav" or "audio/vnd.wav" (WAV) pub fn with_mime_type(mut self, mime_type: &str) -> Self { self.settings.mime_type = Some(mime_type.to_string()); self @@ -644,10 +566,22 @@ impl DecoderBuilder { /// Configure whether the data supports random access seeking. Without this, only forward /// seeking may work. /// - /// `DecoderBuilder::try_from::()` automatically sets this to `true`. + /// This setting controls whether the decoder will attempt backward seeking + /// operations. When disabled, only forward seeking (sample skipping) is + /// allowed, which is suitable for streaming scenarios. /// - /// For reliable seeking behavior, `byte_len` should also be set. While random access seeking - /// may work without `byte_len` for some decoders, it is not guaranteed. + /// # Requirements + /// + /// For reliable seeking behavior: + /// - The underlying stream must support `Seek` trait + /// - `byte_len` should be set for optimal performance + /// - Some formats may have additional requirements + /// + /// # Automatic Setting + /// + /// This is automatically set to `true` when `byte_len` is provided, + /// as byte length information typically implies seekable streams. + /// `DecoderBuilder::try_from::()` automatically sets this to `true`. /// /// # Examples /// ```no_run @@ -676,7 +610,20 @@ impl DecoderBuilder { /// Provides a pre-computed total duration to avoid file scanning. /// - /// When provided, decoders will use this value when they would otherwise need to scan the file. + /// When provided, decoders will use this value instead of performing + /// potentially expensive duration calculation operations. This is + /// particularly useful when duration is known from external metadata. + /// + /// # Use Cases + /// + /// - Database-stored track durations + /// - Previously calculated durations + /// - External metadata sources (ID3, database, etc.) + /// - Avoiding redundant file scanning in batch operations + /// + /// # Priority + /// + /// This setting takes precedence over `scan_duration` when both are set. /// /// This affects decoder implementations that may scan for duration: /// - **MP3**: May scan if metadata doesn't contain duration @@ -705,8 +652,30 @@ impl DecoderBuilder { /// Enable file scanning for duration computation. /// - /// **Important**: This setting only takes effect when the source is both seekable and has - /// `byte_len` is set. If these prerequisites are not met, this setting is ignored. + /// When enabled, allows decoders to perform comprehensive file analysis + /// to determine accurate duration information. This can be slow for large + /// files but provides the most accurate duration data. + /// + /// # Prerequisites + /// + /// This setting only takes effect when: + /// - `is_seekable` is `true` + /// - `byte_len` is set + /// - The decoder supports duration scanning + /// + /// # Format Behavior + /// + /// - **MP3**: Scans for XING/VBRI headers, then frame-by-frame if needed + /// - **OGG Vorbis**: Uses binary search to find last granule position + /// - **FLAC**: Duration available in metadata, no scanning needed + /// - **WAV**: Duration available in header, no scanning needed + /// + /// # Performance Impact + /// + /// Scanning time varies significantly: + /// - Small files (< 10MB): Usually very fast + /// - Large files (> 100MB): Can take several seconds + /// - Variable bitrate files: May require more extensive scanning /// /// This affects specific decoder implementations: /// - **MP3**: May scan if metadata doesn't contain duration @@ -745,15 +714,6 @@ impl DecoderBuilder { /// It attempts to create decoders in a specific order, passing the data source /// between attempts until a compatible format is found. /// - /// # Format Detection Order - /// - /// Decoders are tried in this order for optimal performance: - /// 1. **WAV**: Fast header-based detection - /// 2. **FLAC**: Distinctive magic bytes for quick identification - /// 3. **OGG Vorbis**: Well-defined container format - /// 4. **MP3**: Frame-based detection (more expensive) - /// 5. **Symphonia**: Multi-format fallback (most comprehensive) - /// /// # Error Handling /// /// Each decoder attempts format detection and returns either: diff --git a/src/decoder/flac.rs b/src/decoder/flac.rs index 8ae3e11f..225feccd 100644 --- a/src/decoder/flac.rs +++ b/src/decoder/flac.rs @@ -6,7 +6,7 @@ //! //! # Features //! -//! - **Bit depths**: Full support for 8, 16, 24, and 32-bit audio (including 12 and 20-bit) +//! - **Bit depths**: Full support for 8, 12, 16, 20, 24, and 32-bit audio //! - **Sample rates**: Supports all FLAC-compatible sample rates (1Hz to 655,350Hz) //! - **Channels**: Supports mono, stereo, and multi-channel audio (up to 8 channels) //! - **Seeking**: Full forward and backward seeking with sample-accurate positioning @@ -80,7 +80,7 @@ use crate::{ /// Reader options for `claxon` FLAC decoder. /// -/// Configured to skip metadata parsing and vorbis comments for faster initialization. +/// Configured to skip metadata parsing and Vorbis comments for faster initialization. /// This improves decoder creation performance by only parsing essential stream information /// needed for audio playback. /// @@ -139,7 +139,7 @@ where /// /// Used for calculating the correct memory layout when accessing interleaved samples. /// FLAC blocks can have variable sizes, so this changes per block. - current_block_channel_len: usize, + current_block_samples_per_channel: usize, /// Current position within the current block. /// @@ -297,7 +297,7 @@ where Ok(Self { reader: Some(reader), current_block: Vec::with_capacity(max_block_size), - current_block_channel_len: 1, + current_block_samples_per_channel: 1, current_block_off: 0, bits_per_sample: spec.bits_per_sample, sample_rate: SampleRate::new(sample_rate) @@ -350,15 +350,10 @@ where { /// Returns the number of samples before parameters change. /// - /// For FLAC, this always returns `None` because audio parameters (sample rate, channels, bit - /// depth) never change during the stream. This allows Rodio to optimize by not frequently - /// checking for parameter changes. - /// - /// # Implementation Note + /// # Returns /// - /// FLAC streams have fixed parameters throughout their duration, unlike some formats - /// that may have parameter changes at specific points. This enables optimizations - /// in the audio pipeline by avoiding frequent parameter validation. + /// For FLAC, this always returns `None` because audio parameters (sample rate, channels, bit + /// depth) never change during the stream. #[inline] fn current_span_len(&self) -> Option { None @@ -374,8 +369,7 @@ where /// /// # Guarantees /// - /// The returned value is constant for the lifetime of the decoder and matches - /// the channel count specified in the FLAC stream metadata. + /// The returned value is constant for the lifetime of the decoder. #[inline] fn channels(&self) -> ChannelCount { self.channels @@ -383,17 +377,9 @@ where /// Returns the sample rate in Hz. /// - /// Common rates that FLAC supports are: - /// - **44.1kHz**: CD quality (most common) - /// - **48kHz**: Professional audio standard - /// - **96kHz**: High-resolution audio - /// - **192kHz**: Ultra high-resolution audio - /// /// # Guarantees /// - /// The returned value is constant for the lifetime of the decoder and matches - /// the sample rate specified in the FLAC stream metadata. This value is - /// available immediately upon decoder creation. + /// The returned value is constant for the lifetime of the decoder. #[inline] fn sample_rate(&self) -> SampleRate { self.sample_rate @@ -404,13 +390,9 @@ where /// FLAC metadata contains the total number of samples, allowing accurate duration calculation. /// This is available immediately upon decoder creation without needing to scan the entire file. /// - /// Returns `None` only for malformed FLAC files missing sample count metadata. - /// - /// # Accuracy + /// # Returns /// - /// The duration is calculated from exact sample counts, providing sample-accurate - /// timing information. This is more precise than duration estimates based on - /// bitrate calculations used by lossy formats. + /// Returns `None` only for malformed FLAC files missing sample count metadata. #[inline] fn total_duration(&self) -> Option { self.total_duration @@ -418,18 +400,14 @@ where /// Returns the bit depth of the audio samples. /// - /// FLAC is a lossless format that preserves the original bit depth: - /// - 16-bit: Standard CD quality - /// - 24-bit: Professional/high-resolution audio - /// - 32-bit: Professional/studio quality - /// - Other depths: 8, 12, and 20-bit are also supported - /// - /// Always returns `Some(depth)` for valid FLAC streams. - /// /// # Implementation Note /// - /// The bit depth information is preserved from the original FLAC stream and + /// Up to 24 bits of information is preserved from the original FLAC stream and /// used for proper sample scaling during conversion to Rodio's sample format. + /// + /// # Returns + /// + /// Always returns `Some(depth)` for valid FLAC streams. #[inline] fn bits_per_sample(&self) -> Option { Some(self.bits_per_sample) @@ -476,12 +454,6 @@ where /// // Seek to 30 seconds into the track /// decoder.try_seek(Duration::from_secs(30)).unwrap(); /// ``` - /// - /// # Implementation Details - /// - /// The seeking implementation handles channel alignment to ensure that seeking - /// to a specific time position results in the correct channel being returned - /// for the first sample after the seek operation. fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { // Seeking should be "saturating", meaning: target positions beyond the end of the stream // are clamped to the end. @@ -545,9 +517,9 @@ impl Iterator for FlacDecoder where R: Read + Seek, { - /// The type of items yielded by the iterator. + /// The type of samples yielded by the iterator. /// - /// Returns `Sample` (typically `f32`) values representing individual audio samples. + /// Returns `Sample` values representing individual audio samples. /// Samples are interleaved across channels in the order: channel 0, channel 1, etc. type Item = Sample; @@ -587,7 +559,7 @@ where if self.current_block_off < self.current_block.len() { // Read from current block. let real_offset = (self.current_block_off % self.channels.get() as usize) - * self.current_block_channel_len + * self.current_block_samples_per_channel + self.current_block_off / self.channels.get() as usize; let raw_val = self.current_block[real_offset]; self.current_block_off += 1; @@ -622,7 +594,8 @@ where .read_next_or_eof(buffer) { Ok(Some(block)) => { - self.current_block_channel_len = (block.len() / block.channels()) as usize; + self.current_block_samples_per_channel = + (block.len() / block.channels()) as usize; self.current_block = block.into_buffer(); } Ok(None) | Err(_) => { @@ -634,7 +607,7 @@ where } } - /// Returns bounds on the remaining length of the iterator. + /// Returns bounds on the remaining amount of samples. /// /// Provides accurate size estimates based on FLAC metadata when available. /// This information can be used by consumers for buffer pre-allocation diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 72a2acc4..d6864d17 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -63,7 +63,8 @@ use crate::{ }; pub mod builder; -pub use builder::{DecoderBuilder, Settings}; +pub use builder::DecoderBuilder; +use builder::Settings; mod utils; @@ -148,6 +149,7 @@ enum DecoderImpl { enum Unreachable {} impl DecoderImpl { + /// Advances the decoder and returns the next sample. #[inline] fn next(&mut self) -> Option { match self { @@ -165,6 +167,7 @@ impl DecoderImpl { } } + /// Returns the bounds on the remaining amount of samples. #[inline] fn size_hint(&self) -> (usize, Option) { match self { @@ -182,6 +185,7 @@ impl DecoderImpl { } } + /// Returns the number of samples before the current span ends. #[inline] fn current_span_len(&self) -> Option { match self { @@ -199,6 +203,7 @@ impl DecoderImpl { } } + /// Returns the number of audio channels. #[inline] fn channels(&self) -> ChannelCount { match self { @@ -216,6 +221,7 @@ impl DecoderImpl { } } + /// Returns the sample rate in Hz. #[inline] fn sample_rate(&self) -> SampleRate { match self { @@ -258,7 +264,10 @@ impl DecoderImpl { /// Returns the bits per sample of this audio source. /// - /// For lossy formats this should always return `None`. + /// # Format Support + /// + /// For lossy formats this should always return `None` as bit depth is not a meaningful + /// concept for compressed audio. #[inline] fn bits_per_sample(&self) -> Option { match self { @@ -276,6 +285,7 @@ impl DecoderImpl { } } + /// Attempts to seek to a given position in the current source. #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { match self { @@ -554,7 +564,7 @@ impl TryFrom for Decoder> { /// use rodio::Decoder; /// /// // Embedded audio data (e.g., from include_bytes!) -/// static AUDIO_DATA: &[u8] = include_bytes!("music.wav"); +/// static AUDIO_DATA: &[u8] = include_bytes!("../../assets/music.wav"); /// let decoder = Decoder::try_from(AUDIO_DATA).unwrap(); /// ``` impl TryFrom<&'static [u8]> for Decoder> { @@ -587,7 +597,7 @@ impl TryFrom<&'static [u8]> for Decoder> { /// Decoder::try_from(data) /// } /// -/// static EMBEDDED: &[u8] = include_bytes!("music.wav"); +/// static EMBEDDED: &[u8] = include_bytes!("../../assets/music.wav"); /// let decoder1 = decode_audio(Cow::Borrowed(EMBEDDED)).unwrap(); /// let owned_data = std::fs::read("music.wav").unwrap(); /// let decoder2 = decode_audio(Cow::Owned(owned_data)).unwrap(); @@ -602,11 +612,11 @@ impl TryFrom> } } -/// Converts a `PathBuf` into a `Decoder`. +/// Converts a `&Path` into a `Decoder`. /// /// This is a convenience method for loading audio files from filesystem paths. The file is opened -/// and automatically configured with optimal settings including file size detection and seeking -/// support. +/// and automatically configured with optimal settings including file size detection, seeking +/// support and format hint. /// /// # Errors /// @@ -618,25 +628,24 @@ impl TryFrom> /// # Examples /// ```no_run /// use rodio::Decoder; -/// use std::path::PathBuf; +/// use std::path::Path; /// -/// let path = PathBuf::from("music.mp3"); +/// let path = Path::new("music.mp3"); /// let decoder = Decoder::try_from(path).unwrap(); /// ``` -impl TryFrom for Decoder> { +impl TryFrom<&std::path::Path> for Decoder> { type Error = DecoderError; - fn try_from(path: std::path::PathBuf) -> Result { - let file = std::fs::File::open(path).map_err(|e| Self::Error::IoError(e.to_string()))?; - Self::try_from(file) + fn try_from(path: &std::path::Path) -> Result { + path.to_path_buf().try_into() } } -/// Converts a `&Path` into a `Decoder`. +/// Converts a `PathBuf` into a `Decoder`. /// /// This is a convenience method for loading audio files from filesystem paths. The file is opened -/// and automatically configured with optimal settings including file size detection and seeking -/// support. +/// and automatically configured with optimal settings including file size detection, seeking +/// support and format hint. /// /// # Errors /// @@ -648,52 +657,43 @@ impl TryFrom for Decoder> { /// # Examples /// ```no_run /// use rodio::Decoder; -/// use std::path::Path; +/// use std::path::PathBuf; /// -/// let path = Path::new("music.mp3"); +/// let path = PathBuf::from("music.mp3"); /// let decoder = Decoder::try_from(path).unwrap(); /// ``` -impl TryFrom<&std::path::Path> for Decoder> { +impl TryFrom for Decoder> { type Error = DecoderError; - fn try_from(path: &std::path::Path) -> Result { - let file = std::fs::File::open(path).map_err(|e| Self::Error::IoError(e.to_string()))?; - Self::try_from(file) - } -} + fn try_from(path: std::path::PathBuf) -> Result { + let ext = path.extension().and_then(|e| e.to_str()); + let file = std::fs::File::open(&path).map_err(|e| DecoderError::IoError(e.to_string()))?; -impl Decoder> { - /// Creates a `Decoder` from any path-like type. - /// - /// This is a convenience method that accepts anything that can be converted to a `Path`, - /// including `&str`, `String`, `&Path`, `PathBuf`, etc. The file is opened and automatically - /// configured with optimal settings including file size detection and seeking support. - /// - /// # Errors - /// - /// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined - /// or is not supported. - /// - /// Returns `DecoderError::IoError` if the file cannot be opened or its metadata cannot be read. - /// - /// # Examples - /// ```no_run - /// use rodio::Decoder; - /// use std::path::Path; - /// - /// // Works with &str - /// let decoder = Decoder::from_path("music.mp3").unwrap(); - /// - /// // Works with String - /// let path = String::from("music.mp3"); - /// let decoder = Decoder::from_path(path).unwrap(); - /// - /// // Works with Path and PathBuf - /// let decoder = Decoder::from_path(Path::new("music.mp3")).unwrap(); - /// ``` - pub fn from_path(path: impl AsRef) -> Result { - let file = std::fs::File::open(path).map_err(|e| DecoderError::IoError(e.to_string()))?; - Self::try_from(file) + let len = file + .metadata() + .map_err(|e| DecoderError::IoError(e.to_string()))? + .len(); + + let mut builder = Self::builder() + .with_data(BufReader::new(file)) + .with_byte_len(len) + .with_seekable(true); + + if let Some(ext) = ext { + let hint = match ext { + "adif" | "adts" => "aac", + "caf" => "audio/x-caf", + "m4a" | "m4b" | "m4p" | "m4r" | "mp4" => "audio/mp4", + "bit" | "mpga" => "mp3", + "mka" | "mkv" => "audio/matroska", + "oga" | "ogm" | "ogv" | "ogx" | "spx" => "audio/ogg", + "wave" => "wav", + _ => ext, + }; + builder = builder.with_hint(hint); + } + + builder.build() } } diff --git a/src/decoder/mp3.rs b/src/decoder/mp3.rs index a029c82f..9f002037 100644 --- a/src/decoder/mp3.rs +++ b/src/decoder/mp3.rs @@ -498,11 +498,6 @@ where /// Returns the sample rate in Hz. /// - /// MP3 supports specific sample rates based on MPEG version: - /// - **MPEG-1**: 32kHz, 44.1kHz, 48kHz - /// - **MPEG-2**: 16kHz, 22.05kHz, 24kHz - /// - **MPEG-2.5**: 8kHz, 11.025kHz, 12kHz - /// /// # Guarantees /// /// The sample rate is fixed for the entire MP3 stream and cannot change @@ -527,6 +522,8 @@ where /// 2. Calculated via duration scanning (when enabled and prerequisites met) /// 3. Estimated from file size and average bitrate (when `byte_len` available) /// + /// # Returns + /// /// Returns `None` when insufficient information is available for estimation. #[inline] fn total_duration(&self) -> Option { @@ -535,17 +532,7 @@ where /// Returns the bit depth of the audio samples. /// - /// MP3 is a lossy compression format that doesn't preserve the original bit depth. - /// The decoded output is provided as floating-point samples regardless of the - /// original source material's bit depth. - /// - /// # Lossy Compression - /// - /// Unlike lossless formats like FLAC, MP3 uses psychoacoustic modeling to - /// remove audio information deemed less perceptible, making bit depth - /// information irrelevant for the decoded output. - /// - /// # Always Returns None + /// # Returns /// /// This method always returns `None` for MP3 streams as bit depth is not /// a meaningful concept for lossy compressed audio formats. @@ -601,12 +588,6 @@ where /// // Quick seek to 30 seconds (may be approximate for VBR) /// decoder.try_seek(Duration::from_secs(30)).unwrap(); /// ``` - /// - /// # Implementation Details - /// - /// The seeking implementation preserves channel alignment to ensure that seeking - /// to a specific time position results in the correct channel being returned - /// for the first sample after the seek operation. fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { // Seeking should be "saturating", meaning: target positions beyond the end of the stream // are clamped to the end. @@ -678,9 +659,9 @@ impl Iterator for Mp3Decoder where R: Read + Seek, { - /// The type of items yielded by the iterator. + /// The type of samples yielded by the iterator. /// - /// Returns `Sample` (typically `f32`) values representing individual audio samples. + /// Returns `Sample` values representing individual audio samples. /// Samples are interleaved across channels in the order: channel 0, channel 1, etc. type Item = Sample; @@ -690,17 +671,6 @@ where /// decoded MP3 frame and returning samples one at a time. It automatically decodes /// new frames as needed and adapts to changing stream characteristics. /// - /// # Sample Format Conversion - /// - /// MP3 frames are decoded to PCM samples which are then converted to Rodio's - /// sample format (typically `f32`). The conversion preserves the dynamic range - /// and quality of the decoded audio. - /// - /// # Performance - /// - /// - **Hot path**: Returning samples from current frame (very fast) - /// - **Cold path**: Decoding new frames when buffer is exhausted (slower) - /// /// # Adaptive Behavior /// /// The decoder adapts to changes in the MP3 stream: @@ -767,7 +737,7 @@ where None } - /// Returns bounds on the remaining length of the iterator. + /// Returns bounds on the remaining amount of samples. /// /// Provides size estimates based on MP3 metadata and current playback position. /// The accuracy depends on the availability and reliability of duration information. diff --git a/src/decoder/read_seek_source.rs b/src/decoder/read_seek_source.rs index 74bbdee8..004fdceb 100644 --- a/src/decoder/read_seek_source.rs +++ b/src/decoder/read_seek_source.rs @@ -30,7 +30,7 @@ use std::io::{Read, Result, Seek, SeekFrom}; use symphonia::core::io::MediaSource; -use super::Settings; +use crate::decoder::builder::Settings; /// A wrapper around a `Read + Seek` type that implements Symphonia's `MediaSource` trait. /// @@ -145,10 +145,9 @@ impl MediaSource for ReadSeekSource { /// /// # Impact on Symphonia /// - /// When `false`, Symphonia will: + /// When `false`, Symphonia may: /// - Avoid backward seeking operations - /// - Use streaming-optimized algorithms - /// - May provide degraded seeking functionality + /// - Provide degraded seeking functionality #[inline] fn is_seekable(&self) -> bool { self.is_seekable @@ -169,9 +168,7 @@ impl MediaSource for ReadSeekSource { /// /// Symphonia may use this information for: /// - **Seeking calculations**: Computing byte offsets for time-based seeks - /// - **Progress tracking**: Determining playback progress percentage /// - **Format detection**: Some formats benefit from knowing stream length - /// - **Buffer optimization**: Memory allocation decisions /// /// # Accuracy Requirements /// diff --git a/src/decoder/symphonia.rs b/src/decoder/symphonia.rs index 064dafbb..52f12750 100644 --- a/src/decoder/symphonia.rs +++ b/src/decoder/symphonia.rs @@ -92,7 +92,8 @@ use symphonia::{ default::get_probe, }; -use super::{DecoderError, Settings}; +use super::DecoderError; +use crate::decoder::builder::Settings; use crate::{ common::{ChannelCount, Sample, SampleRate}, decoder::builder::SeekMode, @@ -517,11 +518,6 @@ impl SymphoniaDecoder { impl Source for SymphoniaDecoder { /// Returns the number of samples before parameters change. /// - /// For Symphonia, this returns the number of samples in the current buffer - /// when available, or `Some(0)` when the stream is exhausted. Unlike formats - /// with fixed parameters, Symphonia streams may have parameter changes during - /// playback due to track switches or codec resets. - /// /// # Parameter Changes /// /// Symphonia may encounter parameter changes in several scenarios: @@ -534,6 +530,11 @@ impl Source for SymphoniaDecoder { /// Buffer sizes are determined by the codec's maximum frame length and /// may vary between packets based on encoding complexity and format /// characteristics. + /// + /// # Returns + /// + /// This returns the number of samples in the current buffer when available, or + /// `Some(0)` when the stream is exhausted. #[inline] fn current_span_len(&self) -> Option { // Audio spec remains stable for the length of the buffer. Return Some(0) when exhausted. @@ -542,10 +543,6 @@ impl Source for SymphoniaDecoder { /// Returns the number of audio channels. /// - /// The channel count comes from Symphonia's signal specification which is - /// established during initialization and may change during playback if - /// codec parameters change or track switching occurs. - /// /// # Dynamic Changes /// /// While most files have consistent channel configuration, Symphonia handles @@ -579,10 +576,6 @@ impl Source for SymphoniaDecoder { /// Returns the sample rate in Hz. /// - /// The sample rate comes from Symphonia's signal specification which is - /// established during initialization and may change during playback if - /// codec parameters change. - /// /// # Dynamic Changes /// /// While most files have consistent sample rates, Symphonia handles cases @@ -591,14 +584,6 @@ impl Source for SymphoniaDecoder { /// - **Codec resets**: Parameter changes requiring decoder recreation /// - **Chained streams**: Concatenated streams with different sample rates /// - /// # Format Support - /// - /// Supported sample rates depend on the underlying format and codec: - /// - **MP3**: 8kHz, 11.025kHz, 12kHz, 16kHz, 22.05kHz, 24kHz, 32kHz, 44.1kHz, 48kHz - /// - **AAC**: 8kHz to 96kHz (format-dependent) - /// - **FLAC**: 1Hz to 655.35kHz (though practical range is smaller) - /// - **Vorbis**: 8kHz to 192kHz (encoder-dependent) - /// /// # Guarantees /// /// The returned value reflects the current signal specification and is @@ -611,10 +596,6 @@ impl Source for SymphoniaDecoder { /// Returns the total duration of the audio stream. /// - /// Duration is calculated from track metadata when available, providing accurate - /// timing information. The calculation uses timebase and frame count information - /// from the container or codec metadata. - /// /// # Availability /// /// Duration is available when: @@ -622,22 +603,17 @@ impl Source for SymphoniaDecoder { /// 2. Container format provides duration information /// 3. Format reader successfully extracts timing metadata /// - /// Returns `None` for: - /// - Live streams without predetermined duration - /// - Malformed files missing duration metadata - /// - Streams where duration cannot be determined - /// - /// # Accuracy - /// - /// Duration accuracy depends on the source: - /// - **Metadata-based**: Exact duration from container or codec information - /// - **Calculated**: Derived from frame count and timebase (very accurate) - /// - **Missing**: No duration information available - /// /// # Multi-track Files /// /// For multi-track files, duration represents the length of the currently /// selected audio track, not the entire file duration. + /// + /// # Returns + /// + /// Returns `None` for: + /// - Live streams without predetermined duration + /// - Malformed files missing duration metadata + /// - Streams where duration cannot be determined #[inline] fn total_duration(&self) -> Option { self.total_duration @@ -645,26 +621,17 @@ impl Source for SymphoniaDecoder { /// Returns the bit depth of the audio samples. /// - /// The bit depth comes from the codec parameters when available. Not all - /// formats preserve or report original bit depth information, particularly - /// lossy formats that use different internal representations. - /// /// # Format Support /// - /// Bit depth availability varies by format: - /// - **FLAC**: Always available (8, 16, 24, 32-bit) - /// - **WAV/PCM**: Always available (8, 16, 24, 32-bit integer/float) - /// - **ALAC**: Available (16, 20, 24, 32-bit) - /// - **MP3**: Not available (lossy compression) - /// - **AAC**: Not available (lossy compression) - /// - **Vorbis**: Not available (floating-point processing) - /// - /// # Implementation Note - /// /// For lossy formats, bit depth is not meaningful as the audio undergoes /// compression that removes the original bit depth information. Lossless /// formats preserve and report the original bit depth. /// + /// # Implementation Note + /// + /// Up to 24 bits of information is preserved from the original stream and + /// used for proper sample scaling during conversion to Rodio's sample format. + /// /// # Returns /// /// - `Some(depth)` for formats that preserve bit depth information @@ -865,16 +832,15 @@ impl Source for SymphoniaDecoder { } impl Iterator for SymphoniaDecoder { - /// The type of items yielded by the iterator. + /// The type of samples yielded by the iterator. /// - /// Returns `Sample` (typically `f32`) values representing individual audio samples. - /// Samples are interleaved across channels in the order determined by the format's - /// channel mapping specification. + /// Returns `Sample` values representing individual audio samples. Samples are interleaved + /// across channels in the order determined by the format's channel mapping specification. type Item = Sample; /// Returns the next audio sample from the multi-format stream. /// - /// This method implements sophisticated packet-based decoding with robust error recovery. + /// This method implements packet-based decoding with robust error recovery. /// It automatically handles format-specific details, codec resets, track changes, /// and various error conditions while maintaining optimal performance. /// @@ -1020,18 +986,12 @@ impl Iterator for SymphoniaDecoder { } } - /// Returns bounds on the remaining length of the iterator. + /// Returns bounds on the remaining amount of samples. /// /// Provides size estimates based on Symphonia's format analysis and current /// playback position. The accuracy depends on the availability and reliability /// of metadata from the underlying format. /// - /// # Returns - /// - /// A tuple `(lower_bound, upper_bound)` where: - /// - `lower_bound`: Minimum number of samples guaranteed to be available - /// - `upper_bound`: Maximum number of samples that might be available (None if unknown) - /// /// # Accuracy Levels /// /// - **High accuracy**: When total samples calculated from frame count metadata @@ -1043,7 +1003,7 @@ impl Iterator for SymphoniaDecoder { /// /// Different formats provide varying levels of size information: /// - **FLAC**: Exact frame count in metadata (highest accuracy) - /// - **WAV**: Sample count in header (perfect accuracy) + /// - **WAV**: Sample count in header (highest accuracy) /// - **MP4**: Duration-based estimation (good accuracy) /// - **MP3**: Variable accuracy depending on encoding type /// - **OGG**: Duration-based when available @@ -1067,6 +1027,12 @@ impl Iterator for SymphoniaDecoder { /// /// For multi-track files, estimates represent the currently selected audio /// track, not the entire file duration or all tracks combined. + /// + /// # Returns + /// + /// A tuple `(lower_bound, upper_bound)` where: + /// - `lower_bound`: Minimum number of samples guaranteed to be available + /// - `upper_bound`: Maximum number of samples that might be available (None if unknown) fn size_hint(&self) -> (usize, Option) { // Samples already decoded and buffered (guaranteed available) let buffered_samples = self @@ -1100,11 +1066,6 @@ impl Iterator for SymphoniaDecoder { /// * `current_track_id` - Optional current track ID for track switching logic /// * `spec` - Optional signal specification to update during recreation /// -/// # Returns -/// -/// - `Ok(track_id)` - ID of the newly selected track -/// * `Err(Error)` - If no suitable track found or decoder creation failed -/// /// # Track Selection Strategy /// /// The function implements different strategies based on context: @@ -1112,13 +1073,6 @@ impl Iterator for SymphoniaDecoder { /// - **During playback**: Attempts to find next supported track after current one /// - **No fallback during playback**: Prevents unexpected track jumping /// -/// # Decoder Recreation Process -/// -/// 1. **Track selection**: Find appropriate audio track with supported codec -/// 2. **Decoder creation**: Create new decoder for selected track -/// 3. **Specification update**: Update signal spec if provided -/// 4. **State consistency**: Ensure new decoder is properly initialized -/// /// # Error Handling /// /// The function handles various error scenarios: @@ -1127,15 +1081,10 @@ impl Iterator for SymphoniaDecoder { /// - **Track not found**: Handles missing track scenarios /// - **Specification updates**: Updates spec when track parameters available /// -/// # Performance Considerations -/// -/// Decoder recreation is an expensive operation that involves: -/// - Track list analysis -/// - Codec instantiation -/// - Parameter validation -/// - State initialization +/// # Returns /// -/// It should be used sparingly and only when required by Symphonia. +/// - `Ok(track_id)` - ID of the newly selected track +/// * `Err(Error)` - If no suitable track found or decoder creation failed fn recreate_decoder( format: &mut Box, decoder: &mut Box, @@ -1202,39 +1151,6 @@ fn recreate_decoder( /// /// - `true` if decoding should continue with the next packet /// - `false` if the error is terminal and decoding should stop -/// -/// # Error Classification -/// -/// - **Recoverable errors**: Can be handled by skipping the problematic packet -/// - `DecodeError`: Corrupted or malformed packet data -/// - `IoError`: Temporary I/O issues during packet reading -/// - `ResetRequired`: Decoder parameter changes (handled with reset) -/// - **Terminal errors**: Indicate unrecoverable conditions -/// - `Unsupported`: Codec or format not supported -/// - `LimitError`: Resource or format limits exceeded -/// - Other unspecified errors -/// -/// # Decoder Reset Handling -/// -/// When `ResetRequired` is encountered, the function automatically resets -/// the decoder state to handle parameter changes. This is essential for -/// maintaining audio quality across parameter transitions. -/// -/// # Performance Impact -/// -/// Error handling is designed to be lightweight: -/// - Quick error classification -/// - Minimal decoder state changes -/// - Efficient recovery strategies -/// - No unnecessary processing for terminal errors -/// -/// # Robustness Strategy -/// -/// The function implements a conservative approach: -/// - Favor continuation when safe -/// - Reset decoder state when needed -/// - Terminate only on truly unrecoverable errors -/// - Preserve audio quality over maximum compatibility fn should_continue_on_decode_error( error: &symphonia::core::errors::Error, decoder: &mut Box, diff --git a/src/decoder/vorbis.rs b/src/decoder/vorbis.rs index 0f2ba663..02189f80 100644 --- a/src/decoder/vorbis.rs +++ b/src/decoder/vorbis.rs @@ -272,13 +272,12 @@ where } // Calculate total duration using the new settings approach (before consuming data) - let last_granule = if settings.total_duration.is_some() { - None // Use provided duration directly - } else if settings.scan_duration && settings.is_seekable && settings.byte_len.is_some() { - find_last_granule(&mut data, settings.byte_len.unwrap()) // Scan for duration - } else { - None // Either scanning disabled or prerequisites not met - }; + let mut last_granule = None; + if settings.scan_duration && settings.is_seekable { + if let Some(byte_len) = settings.byte_len { + last_granule = find_last_granule(&mut data, byte_len); + } + } let mut stream_reader = OggStreamReader::new(data).expect("should still be vorbis"); let current_data = read_next_non_empty_packet(&mut stream_reader); @@ -420,13 +419,6 @@ where /// - **Seeking time**: O(log n) binary search through Ogg pages /// - **Accuracy**: Positions at or before target granule (requires fine-tuning) /// - **Optimal for**: Large files with frequent seeking requirements - /// - /// # Implementation - /// - /// Uses lewton's `seek_absgp_pg` function which performs binary search through - /// Ogg pages to find the page containing the target granule position. The - /// decoder may position slightly before the target, requiring sample skipping - /// for exact positioning. fn granule_seek(&mut self, target_granule_pos: u64) -> Result { let reader = self .stream_reader @@ -458,24 +450,17 @@ where { /// Returns the number of samples before parameters change. /// - /// For Ogg Vorbis, this returns `Some(packet_size)` when a packet is available, - /// representing the number of samples in the current packet. Returns `Some(0)` - /// when the stream is exhausted. - /// /// # Chained Streams /// /// Ogg supports chained streams where multiple Vorbis streams are concatenated. /// When stream parameters change (sample rate, channels), the span length /// reflects the current stream's characteristics. /// - /// # Packet Sizes - /// - /// Vorbis packets have variable sizes depending on: - /// - Audio content complexity - /// - Encoder settings and optimization - /// - Bitrate allocation decisions + /// # Returns /// - /// Typical packet sizes range from hundreds to thousands of samples. + /// This returns `Some(packet_size)` when a packet is available, representing the + /// number of samples in the current packet. Returns `Some(0)` when the stream is + /// exhausted. #[inline] fn current_span_len(&self) -> Option { // Chained Ogg streams are supported by lewton, so parameters can change. @@ -524,14 +509,6 @@ where /// Returns the sample rate in Hz. /// - /// Ogg Vorbis supports a wide range of sample rates from 8kHz to 192kHz: - /// - **8kHz-16kHz**: Speech and low-quality audio - /// - **22.05kHz**: Low-quality music - /// - **44.1kHz**: CD quality (most common) - /// - **48kHz**: Professional audio standard - /// - **96kHz**: High-resolution audio - /// - **192kHz**: Ultra high-resolution audio - /// /// # Chained Streams /// /// Sample rate can change between chained streams, though this is rare in @@ -555,27 +532,12 @@ where /// Returns the total duration of the audio stream. /// - /// Duration accuracy depends on how it was calculated: - /// - **Provided explicitly**: Most accurate (when available from metadata) - /// - **Granule scanning**: Very accurate, calculated from final granule position - /// - **Not available**: Returns `None` when duration cannot be determined - /// - /// # Granule Position Method - /// - /// The most accurate method scans to the final granule position in the stream, - /// which represents the exact number of decoded samples. This provides - /// sample-accurate duration information. - /// /// # Chained Streams /// /// For chained streams, duration represents the total across all chains. /// Individual chain durations are not separately tracked. /// - /// # Availability - /// - /// Duration is available when: - /// 1. Explicitly provided via `total_duration` setting - /// 2. Calculated via granule position scanning (when enabled and prerequisites met) + /// # Returns /// /// Returns `None` when scanning is disabled or prerequisites are not met. #[inline] @@ -585,22 +547,10 @@ where /// Returns the bit depth of the audio samples. /// - /// Ogg Vorbis is a lossy compression format that doesn't preserve the original - /// bit depth. The decoded output uses floating-point processing internally and - /// is provided as floating-point samples regardless of the original source - /// material's bit depth. - /// - /// # Lossy Compression - /// - /// Unlike lossless formats like FLAC, Vorbis uses advanced psychoacoustic - /// modeling to remove audio information deemed less perceptible, making - /// bit depth information irrelevant for the decoded output. - /// - /// # Always Returns None + /// # Returns /// - /// This method always returns `None` for Ogg Vorbis streams as bit depth is - /// not a meaningful concept for lossy compressed audio formats with - /// floating-point processing. + /// This method always returns `None` for Vorbis streams as bit depth is not + /// a meaningful concept for lossy compressed audio formats. #[inline] fn bits_per_sample(&self) -> Option { None @@ -655,12 +605,6 @@ where /// // Fast granule-based seek to 30 seconds /// decoder.try_seek(Duration::from_secs(30)).unwrap(); /// ``` - /// - /// # Implementation Details - /// - /// The seeking implementation preserves channel alignment to ensure that seeking - /// to a specific time position results in the correct channel being returned - /// for the first sample after the seek operation. fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { // Seeking should be "saturating", meaning: target positions beyond the end of the stream // are clamped to the end. @@ -706,9 +650,9 @@ impl Iterator for VorbisDecoder where R: Read + Seek, { - /// The type of items yielded by the iterator. + /// The type of samples yielded by the iterator. /// - /// Returns `Sample` (typically `f32`) values representing individual audio samples. + /// Returns `Sample` values representing individual audio samples. /// Samples are interleaved across channels in the order: channel 0, channel 1, etc. type Item = Sample; @@ -718,12 +662,6 @@ where /// decoded Vorbis packet and returning samples one at a time. It automatically decodes /// new packets as needed and handles various Ogg/Vorbis stream conditions. /// - /// # Sample Format - /// - /// Vorbis packets are decoded to interleaved PCM samples using lewton's floating-point - /// processing. The samples are provided in Rodio's sample format (typically `f32`) - /// preserving the quality and dynamic range of the decoded audio. - /// /// # Performance /// /// - **Hot path**: Returning samples from current packet (very fast) @@ -784,7 +722,7 @@ where None } - /// Returns bounds on the remaining length of the iterator. + /// Returns bounds on the remaining amount of samples. /// /// Provides size estimates based on Ogg Vorbis stream characteristics and current /// playback position. The accuracy depends on the availability of duration information @@ -853,19 +791,6 @@ where /// /// - `Some(samples)` - Vector of interleaved audio samples from the next valid packet /// - `None` - Stream exhausted or unrecoverable error occurred -/// -/// # Error Handling -/// -/// The function handles several error conditions gracefully: -/// - **Header packets**: Skipped automatically (not audio data) -/// - **Capture pattern errors**: Ignored for robustness during seeking -/// - **Empty packets**: Skipped to find packets with actual audio -/// - **Terminal errors**: Result in stream termination -/// -/// # Performance -/// -/// This function optimizes for the common case of valid audio packets while -/// providing robust error recovery for edge cases and stream boundary conditions. fn read_next_non_empty_packet( stream_reader: &mut OggStreamReader, ) -> Option> { diff --git a/src/decoder/wav.rs b/src/decoder/wav.rs index fbbdb9e7..45eeed44 100644 --- a/src/decoder/wav.rs +++ b/src/decoder/wav.rs @@ -355,9 +355,9 @@ impl Iterator for SamplesIterator where R: Read + Seek, { - /// The type of items yielded by the iterator. + /// The type of samples yielded by the iterator. /// - /// Returns `Sample` (typically `f32`) values representing individual audio samples. + /// Returns `Sample` values representing individual audio samples. /// Samples are interleaved across channels in the order: channel 0, channel 1, etc. type Item = Sample; @@ -449,7 +449,7 @@ where next_sample } - /// Returns bounds on the remaining length of the iterator. + /// Returns bounds on the remaining amount of samples. /// /// For WAV files, this provides exact remaining sample count based on /// header information and current position. This enables accurate @@ -518,18 +518,9 @@ where /// Returns the sample rate in Hz. /// - /// WAV supports a wide range of sample rates: - /// - **8kHz-16kHz**: Speech and telephone quality - /// - **22.05kHz**: Lower quality music - /// - **44.1kHz**: CD quality (most common) - /// - **48kHz**: Professional audio standard - /// - **96kHz/192kHz**: High-resolution audio - /// /// # Guarantees /// - /// The returned value is constant for the lifetime of the decoder and - /// matches the sample rate specified in the WAV file's fmt chunk. - /// This value is available immediately upon decoder creation. + /// The returned value is constant for the lifetime of the decoder. #[inline] fn sample_rate(&self) -> SampleRate { self.sample_rate @@ -537,26 +528,9 @@ where /// Returns the total duration of the audio stream. /// - /// For WAV files, this is calculated directly from the header information - /// and is always available immediately upon decoder creation. The calculation - /// uses exact sample counts for perfect accuracy. - /// - /// # Accuracy - /// - /// WAV duration is sample-accurate because: - /// - Sample count is stored in the header - /// - Sample rate is fixed throughout the file - /// - No compression artifacts or estimation required - /// - /// # Always Available - /// - /// Unlike compressed formats that may require scanning, WAV duration is - /// instantly available from header information with no I/O overhead. - /// - /// # Guarantees + /// # Returns /// - /// Always returns `Some(duration)` for valid WAV files. The duration - /// represents the exact playback time based on sample count and rate. + /// Always returns `Some(duration)` for valid WAV files. #[inline] fn total_duration(&self) -> Option { Some(self.total_duration) @@ -564,22 +538,15 @@ where /// Returns the bit depth of the audio samples. /// - /// WAV files preserve the original bit depth from the source material: - /// - **8-bit**: Basic quality, often used for legacy applications - /// - **16-bit**: CD quality, most common for music - /// - **24-bit**: High-resolution audio, professional recordings - /// - **32-bit integer**: Maximum precision integer samples - /// - **32-bit float**: Floating-point samples with extended dynamic range + /// # Implementation Note /// - /// # Guarantees + /// Up to 24 bits of information is preserved from the original WAV file and + /// used for proper sample scaling during conversion to Rodio's sample format. + /// + /// # Returns /// /// Always returns `Some(depth)` for valid WAV files. The bit depth is /// constant throughout the file and matches the fmt chunk specification. - /// - /// # Implementation Note - /// - /// The bit depth information is preserved from the original WAV file and - /// used for proper sample scaling during conversion to Rodio's sample format. #[inline] fn bits_per_sample(&self) -> Option { Some(self.reader.reader.spec().bits_per_sample as u32) @@ -632,12 +599,6 @@ where /// // Instant seek to 30 seconds /// decoder.try_seek(Duration::from_secs(30)).unwrap(); /// ``` - /// - /// # Implementation Details - /// - /// The seeking implementation preserves channel alignment to ensure that seeking - /// to a specific time position results in the correct channel being returned - /// for the first sample after the seek operation. #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { let file_len = self.reader.reader.duration(); @@ -676,9 +637,9 @@ impl Iterator for WavDecoder where R: Read + Seek, { - /// The type of items yielded by the iterator. + /// The type of samples yielded by the iterator. /// - /// Returns `Sample` (typically `f32`) values representing individual audio samples. + /// Returns `Sample` values representing individual audio samples. /// Samples are interleaved across channels in the order: channel 0, channel 1, etc. type Item = Sample; @@ -705,7 +666,7 @@ where self.reader.next() } - /// Returns bounds on the remaining length of the iterator. + /// Returns bounds on the remaining amount of samples. /// /// Delegates to the internal `SamplesIterator` which provides exact /// remaining sample count based on WAV header information. @@ -715,12 +676,6 @@ where /// A tuple `(lower_bound, upper_bound)` where: /// - `lower_bound`: Always 0 (no samples guaranteed without I/O) /// - `upper_bound`: Exact remaining sample count from WAV header - /// - /// # Accuracy - /// - /// WAV files provide perfect size hint accuracy due to exact sample - /// counts in file headers, enabling optimal buffer allocation and - /// progress indication. #[inline] fn size_hint(&self) -> (usize, Option) { self.reader.size_hint() diff --git a/src/source/mod.rs b/src/source/mod.rs index d33641a5..11ab2f8c 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -161,27 +161,44 @@ pub use self::noise::{Pink, WhiteUniform}; /// channels can potentially change. /// pub trait Source: Iterator { - /// Returns the number of samples before the current span ends. `None` means "infinite" or - /// "until the sound ends". Sources that return `Some(x)` should return `Some(0)` if and - /// if when there's no more data. + /// Returns the number of samples before the current span ends. /// /// After the engine has finished reading the specified number of samples, it will check - /// whether the value of `channels()` and/or `sample_rate()` have changed. + /// whether the value of `channels()`, `sample_rate()` and/or `bits_per_sample()` have changed. + /// + /// # Returns + /// + /// `None` means "infinite" or "until the sound ends". Sources that return `Some(x)` should + /// return `Some(0)` if and only if when there's no more data. fn current_span_len(&self) -> Option; /// Returns the number of channels. Channels are always interleaved. - /// Should never be Zero + /// + /// Common configurations: + /// - 1 channel: Mono + /// - 2 channels: Stereo + /// - 6 channels: 5.1 surround + /// - 8 channels: 7.1 surround fn channels(&self) -> ChannelCount; - /// Returns the rate at which the source should be played. In number of samples per second. + /// Returns the sample rate in Hz at which the source should be played. fn sample_rate(&self) -> SampleRate; /// Returns the total duration of this source, if known. /// + /// # Returns + /// /// `None` indicates at the same time "infinite" or "unknown". fn total_duration(&self) -> Option; /// Returns the number of bits per sample, if known. + /// + /// # Implementation Note + /// + /// This function returns the bit depth of the original audio stream when available, + /// not the bit depth of Rodio's internal sample representation. Up to 24 bits of + /// information is preserved from the original stream and used for proper sample + /// scaling during conversion to Rodio's sample format. fn bits_per_sample(&self) -> Option; /// Stores the source in a buffer in addition to returning it. This iterator can be cloned. diff --git a/src/source/zero.rs b/src/source/zero.rs index 26e50e63..4d405d4f 100644 --- a/src/source/zero.rs +++ b/src/source/zero.rs @@ -80,7 +80,7 @@ impl Source for Zero { #[inline] fn bits_per_sample(&self) -> Option { - Some(f32::MANTISSA_DIGITS) + None } #[inline] diff --git a/tests/seek.rs b/tests/seek.rs index 898a2c3c..3663c5fa 100644 --- a/tests/seek.rs +++ b/tests/seek.rs @@ -235,16 +235,9 @@ fn seek_does_not_break_channel_order( feature = "symphonia-mp3", all(feature = "symphonia-ogg", feature = "symphonia-vorbis",) ))] +#[cfg_attr(all(feature = "symphonia-mp3"), case("mp3", "symphonia"))] #[cfg_attr( - all(feature = "symphonia-mp3", not(feature = "minimp3")), - case("mp3", "symphonia") -)] -#[cfg_attr( - all( - feature = "symphonia-ogg", - feature = "symphonia-vorbis", - not(feature = "lewton") - ), + all(feature = "symphonia-ogg", feature = "symphonia-vorbis",), case("ogg", "symphonia") )] fn random_access_seeks(#[case] format: &'static str, #[case] decoder_name: &'static str) { From 81e23f65f8caa88b7eac78a142bff3b5c761e3a2 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 31 Aug 2025 22:38:21 +0200 Subject: [PATCH 08/17] feat: add bits_per_sample method to Microphone source --- src/microphone.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/microphone.rs b/src/microphone.rs index d191ba3b..9130a767 100644 --- a/src/microphone.rs +++ b/src/microphone.rs @@ -101,6 +101,7 @@ use core::fmt; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::{f32, f64}; use std::{thread, time::Duration}; use crate::common::assert_error_traits; @@ -198,6 +199,21 @@ impl Source for Microphone { fn total_duration(&self) -> Option { None } + + fn bits_per_sample(&self) -> Option { + let bits = match self.config.sample_format { + cpal::SampleFormat::I8 | cpal::SampleFormat::U8 => 8, + cpal::SampleFormat::I16 | cpal::SampleFormat::U16 => 16, + cpal::SampleFormat::I24 => 24, + cpal::SampleFormat::I32 | cpal::SampleFormat::U32 => 32, + cpal::SampleFormat::I64 | cpal::SampleFormat::U64 => 64, + cpal::SampleFormat::F32 => f32::MANTISSA_DIGITS, + cpal::SampleFormat::F64 => f64::MANTISSA_DIGITS, + _ => return None, + }; + + Some(bits) + } } impl Iterator for Microphone { From 1b665197c1096cda2cad3bb8698f4e89b6e234b8 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 5 Sep 2025 23:07:30 +0200 Subject: [PATCH 09/17] refactor: use BitDepth for bits_per_sample - Introduce BitDepth as a newtype for non-zero u32 - Update Source trait and all implementations to use Option - Update decoders, sources, and tests to use BitDepth instead of u32 for bit depth - Replace NonZero usage for sample rate and channel count with SampleRate and ChannelCount types where appropriate - Update documentation and examples to reflect new types fix: change bits_per_sample to return Option --- benches/pipeline.rs | 5 +- benches/shared.rs | 6 +- examples/custom_config.rs | 5 +- examples/mix_multiple_sources.rs | 8 ++- examples/signal_generator.rs | 5 +- src/buffer.rs | 9 ++- src/common.rs | 5 +- src/decoder/flac.rs | 15 ++--- src/decoder/mod.rs | 8 +-- src/decoder/mp3.rs | 14 ++--- src/decoder/symphonia.rs | 103 ++++++++++++++++++------------- src/decoder/vorbis.rs | 9 +-- src/decoder/wav.rs | 18 +++--- src/lib.rs | 2 +- src/microphone.rs | 27 ++------ src/microphone/builder.rs | 10 +-- src/microphone/config.rs | 6 +- src/mixer.rs | 4 +- src/queue.rs | 4 +- src/source/agc.rs | 4 +- src/source/amplify.rs | 4 +- src/source/blt.rs | 4 +- src/source/buffered.rs | 5 +- src/source/channel_volume.rs | 4 +- src/source/chirp.rs | 6 +- src/source/delay.rs | 4 +- src/source/distortion.rs | 4 +- src/source/done.rs | 4 +- src/source/empty.rs | 4 +- src/source/empty_callback.rs | 4 +- src/source/fadein.rs | 4 +- src/source/fadeout.rs | 4 +- src/source/from_iter.rs | 4 +- src/source/limit.rs | 6 +- src/source/linear_ramp.rs | 4 +- src/source/mix.rs | 4 +- src/source/mod.rs | 18 +++--- src/source/noise.rs | 16 ++--- src/source/pausable.rs | 4 +- src/source/periodic.rs | 4 +- src/source/position.rs | 4 +- src/source/repeat.rs | 4 +- src/source/sawtooth.rs | 7 +-- src/source/signal_generator.rs | 10 +-- src/source/sine.rs | 6 +- src/source/skip.rs | 4 +- src/source/skippable.rs | 5 +- src/source/spatial.rs | 4 +- src/source/speed.rs | 4 +- src/source/square.rs | 6 +- src/source/stoppable.rs | 4 +- src/source/take.rs | 4 +- src/source/triangle.rs | 6 +- src/source/uniform.rs | 6 +- src/source/zero.rs | 4 +- src/static_buffer.rs | 9 +-- src/stream.rs | 9 ++- tests/limit.rs | 6 +- tests/source_traits.rs | 15 +++-- 59 files changed, 250 insertions(+), 245 deletions(-) diff --git a/benches/pipeline.rs b/benches/pipeline.rs index 3164ca40..b6b2d808 100644 --- a/benches/pipeline.rs +++ b/benches/pipeline.rs @@ -1,9 +1,8 @@ -use std::num::NonZero; use std::time::Duration; use divan::Bencher; -use rodio::ChannelCount; use rodio::{source::UniformSourceIterator, Source}; +use rodio::{ChannelCount, SampleRate}; mod shared; use shared::music_wav; @@ -36,7 +35,7 @@ fn long(bencher: Bencher) { let resampled = UniformSourceIterator::new( effects_applied, ChannelCount::new(2).unwrap(), - NonZero::new(40_000).unwrap(), + SampleRate::new(40_000).unwrap(), ); resampled.for_each(divan::black_box_drop) }) diff --git a/benches/shared.rs b/benches/shared.rs index bb524afe..f442d395 100644 --- a/benches/shared.rs +++ b/benches/shared.rs @@ -2,13 +2,13 @@ use std::io::Cursor; use std::time::Duration; use std::vec; -use rodio::{ChannelCount, Sample, SampleRate, Source}; +use rodio::{BitDepth, ChannelCount, Sample, SampleRate, Source}; pub struct TestSource { samples: vec::IntoIter, channels: ChannelCount, sample_rate: SampleRate, - bits_per_sample: Option, + bits_per_sample: Option, total_duration: Duration, } @@ -50,7 +50,7 @@ impl Source for TestSource { } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.bits_per_sample } } diff --git a/examples/custom_config.rs b/examples/custom_config.rs index aa83a7ee..db29f494 100644 --- a/examples/custom_config.rs +++ b/examples/custom_config.rs @@ -1,9 +1,8 @@ use cpal::traits::HostTrait; use cpal::{BufferSize, SampleFormat}; use rodio::source::SineWave; -use rodio::Source; +use rodio::{SampleRate, Source}; use std::error::Error; -use std::num::NonZero; use std::thread; use std::time::Duration; @@ -16,7 +15,7 @@ fn main() -> Result<(), Box> { // No need to set all parameters explicitly here, // the defaults were set from the device's description. .with_buffer_size(BufferSize::Fixed(256)) - .with_sample_rate(NonZero::new(48_000).unwrap()) + .with_sample_rate(SampleRate::new(48_000).unwrap()) .with_sample_format(SampleFormat::F32) // Note that the function below still tries alternative configs if the specified one fails. // If you need to only use the exact specified configuration, diff --git a/examples/mix_multiple_sources.rs b/examples/mix_multiple_sources.rs index 8c6be920..ca2f67a0 100644 --- a/examples/mix_multiple_sources.rs +++ b/examples/mix_multiple_sources.rs @@ -1,12 +1,14 @@ -use rodio::mixer; use rodio::source::{SineWave, Source}; +use rodio::{mixer, ChannelCount, SampleRate}; use std::error::Error; -use std::num::NonZero; use std::time::Duration; fn main() -> Result<(), Box> { // Construct a dynamic controller and mixer, stream_handle, and sink. - let (controller, mixer) = mixer::mixer(NonZero::new(2).unwrap(), NonZero::new(44_100).unwrap()); + let (controller, mixer) = mixer::mixer( + ChannelCount::new(2).unwrap(), + SampleRate::new(44_100).unwrap(), + ); let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(stream_handle.mixer()); diff --git a/examples/signal_generator.rs b/examples/signal_generator.rs index 2257f30d..ea1f1b30 100644 --- a/examples/signal_generator.rs +++ b/examples/signal_generator.rs @@ -1,7 +1,8 @@ //! Test signal generator example. use std::error::Error; -use std::num::NonZero; + +use rodio::SampleRate; fn main() -> Result<(), Box> { use rodio::source::{chirp, Function, SignalGenerator, Source}; @@ -12,7 +13,7 @@ fn main() -> Result<(), Box> { let test_signal_duration = Duration::from_millis(1000); let interval_duration = Duration::from_millis(1500); - let sample_rate = NonZero::new(48000).unwrap(); + let sample_rate = SampleRate::new(48000).unwrap(); println!("Playing 1000 Hz tone"); stream_handle.mixer().add( diff --git a/src/buffer.rs b/src/buffer.rs index dd124724..a22e7f8f 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -5,15 +5,14 @@ //! # Example //! //! ``` -//! use rodio::buffer::SamplesBuffer; -//! use core::num::NonZero; -//! let _ = SamplesBuffer::new(NonZero::new(1).unwrap(), NonZero::new(44100).unwrap(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); +//! use rodio::{ChannelCount, SampleRate, buffer::SamplesBuffer}; +//! let _ = SamplesBuffer::new(ChannelCount::new(1).unwrap(), SampleRate::new(44100).unwrap(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); //! ``` //! use crate::common::{ChannelCount, SampleRate}; use crate::source::{SeekError, UniformSourceIterator}; -use crate::{Sample, Source}; +use crate::{BitDepth, Sample, Source}; use std::sync::Arc; use std::time::Duration; @@ -92,7 +91,7 @@ impl Source for SamplesBuffer { } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { None } diff --git a/src/common.rs b/src/common.rs index 6c713a07..ceea50bb 100644 --- a/src/common.rs +++ b/src/common.rs @@ -4,9 +4,12 @@ use std::num::NonZero; /// Stream sample rate (a frame rate or samples per second per channel). pub type SampleRate = NonZero; -/// Number of channels in a stream. Can never be Zero +/// Number of channels in a stream. Can never be zero. pub type ChannelCount = NonZero; +/// Number of bits per sample. Can never be zero. +pub type BitDepth = NonZero; + /// Represents value of a single sample. /// Silence corresponds to the value `0.0`. The expected amplitude range is -1.0...1.0. /// Values below and above this range are clipped in conversion to other sample types. diff --git a/src/decoder/flac.rs b/src/decoder/flac.rs index 225feccd..f049d55a 100644 --- a/src/decoder/flac.rs +++ b/src/decoder/flac.rs @@ -72,11 +72,7 @@ use dasp_sample::Sample as _; use dasp_sample::I24; use super::{utils, Settings}; -use crate::{ - common::{ChannelCount, Sample, SampleRate}, - source::SeekError, - Source, -}; +use crate::{source::SeekError, BitDepth, ChannelCount, Sample, SampleRate, Source}; /// Reader options for `claxon` FLAC decoder. /// @@ -152,7 +148,7 @@ where /// Preserved from the original FLAC stream metadata and used for proper /// sample conversion during iteration. FLAC supports various bit depths /// including non-standard ones like 12 and 20-bit. - bits_per_sample: u32, + bits_per_sample: BitDepth, /// Sample rate in Hz. /// @@ -299,7 +295,8 @@ where current_block: Vec::with_capacity(max_block_size), current_block_samples_per_channel: 1, current_block_off: 0, - bits_per_sample: spec.bits_per_sample, + bits_per_sample: BitDepth::new(spec.bits_per_sample) + .expect("flac should never have zero bits per sample"), sample_rate: SampleRate::new(sample_rate) .expect("flac data should never have a zero sample rate"), channels: ChannelCount::new( @@ -409,7 +406,7 @@ where /// /// Always returns `Some(depth)` for valid FLAC streams. #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { Some(self.bits_per_sample) } @@ -564,7 +561,7 @@ where let raw_val = self.current_block[real_offset]; self.current_block_off += 1; self.samples_read += 1; - let bits = self.bits_per_sample; + let bits = self.bits_per_sample.get(); let real_val = match bits { 8 => (raw_val as i8).to_sample(), 16 => (raw_val as i16).to_sample(), diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index d6864d17..948b3ec0 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -59,7 +59,7 @@ use crate::{ common::{assert_error_traits, ChannelCount, SampleRate}, math::nz, source::{SeekError, Source}, - Sample, + BitDepth, Sample, }; pub mod builder; @@ -269,7 +269,7 @@ impl DecoderImpl { /// For lossy formats this should always return `None` as bit depth is not a meaningful /// concept for compressed audio. #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { match self { #[cfg(feature = "hound")] DecoderImpl::Wav(source) => source.bits_per_sample(), @@ -951,7 +951,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.0.bits_per_sample() } @@ -1131,7 +1131,7 @@ where /// Returns the bits per sample of the underlying decoder, if available. #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.inner.as_ref()?.bits_per_sample() } diff --git a/src/decoder/mp3.rs b/src/decoder/mp3.rs index 9f002037..6aa0350e 100644 --- a/src/decoder/mp3.rs +++ b/src/decoder/mp3.rs @@ -54,7 +54,6 @@ use std::{ io::{Read, Seek, SeekFrom}, - num::NonZero, sync::Arc, time::Duration, }; @@ -66,9 +65,7 @@ use minimp3_fixed as minimp3; use super::{utils, Settings}; use crate::{ - common::{ChannelCount, Sample, SampleRate}, - decoder::builder::SeekMode, - source::SeekError, + decoder::builder::SeekMode, source::SeekError, BitDepth, ChannelCount, Sample, SampleRate, Source, }; @@ -354,8 +351,9 @@ where start_byte, current_span: Some(current_span), current_span_offset: 0, - channels: NonZero::new(channels as _).expect("mp3's have at least one channel"), - sample_rate: NonZero::new(sample_rate as _).expect("mp3's have a non zero sample rate"), + channels: ChannelCount::new(channels as _).expect("mp3's have at least one channel"), + sample_rate: SampleRate::new(sample_rate as _) + .expect("mp3's have a non zero sample rate"), samples_read: 0, total_samples, total_duration, @@ -537,7 +535,7 @@ where /// This method always returns `None` for MP3 streams as bit depth is not /// a meaningful concept for lossy compressed audio formats. #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { None } @@ -716,7 +714,7 @@ where // Update channels if they changed (can vary between MP3 frames) self.channels = - NonZero::new(span.channels as _).expect("mp3's have at least one channel"); + ChannelCount::new(span.channels as _).expect("mp3's have at least one channel"); // Sample rate is fixed per MP3 stream, so no need to update self.current_span = Some(span); self.current_span_offset = 0; diff --git a/src/decoder/symphonia.rs b/src/decoder/symphonia.rs index 52f12750..e111fbd8 100644 --- a/src/decoder/symphonia.rs +++ b/src/decoder/symphonia.rs @@ -81,7 +81,7 @@ use std::{sync::Arc, time::Duration}; use symphonia::{ core::{ - audio::{SampleBuffer, SignalSpec}, + audio::SampleBuffer, codecs::{Decoder, DecoderOptions, CODEC_TYPE_NULL, CODEC_TYPE_VORBIS}, errors::Error, formats::{FormatOptions, FormatReader, SeekMode as SymphoniaSeekMode, SeekTo}, @@ -93,12 +93,12 @@ use symphonia::{ }; use super::DecoderError; -use crate::decoder::builder::Settings; use crate::{ common::{ChannelCount, Sample, SampleRate}, decoder::builder::SeekMode, source, Source, }; +use crate::{decoder::builder::Settings, BitDepth}; /// Multi-format audio decoder using the Symphonia library. /// @@ -165,18 +165,21 @@ pub struct SymphoniaDecoder { /// May be `None` for streams without duration metadata or live streams. total_duration: Option, + /// Sample rate of the audio stream. + sample_rate: SampleRate, + + /// Number of audio channels. + channels: ChannelCount, + + /// Bit depth of the audio samples. + bits_per_sample: Option, + /// Current decoded audio buffer. /// /// Contains interleaved PCM samples from the most recently decoded packet. /// `None` indicates that a new packet needs to be decoded. buffer: Option>, - /// Audio signal specification (channels, sample rate, etc.). - /// - /// May change during playback if codec parameters change or track switching occurs. - /// Updated automatically when such changes are detected. - spec: SignalSpec, - /// Seeking precision mode. /// /// Controls the trade-off between seeking speed and accuracy: @@ -440,7 +443,7 @@ impl SymphoniaDecoder { // If ResetRequired is returned, then the track list must be re-examined and all // Decoders re-created. Err(Error::ResetRequired) => { - track_id = recreate_decoder(&mut probed.format, &mut decoder, None, None)?; + track_id = recreate_decoder(&mut probed.format, &mut decoder, None)?; continue; } @@ -479,6 +482,20 @@ impl SymphoniaDecoder { } }; + // Cache initial spec values + let sample_rate = SampleRate::new(spec.rate).expect("Invalid sample rate"); + let channels = spec + .channels + .count() + .try_into() + .ok() + .and_then(ChannelCount::new) + .expect("Invalid channel count"); + let bits_per_sample = decoder + .codec_params() + .bits_per_sample + .and_then(BitDepth::new); + // Calculate total samples let total_samples = { // Try frame-based calculation first (most accurate) @@ -490,9 +507,7 @@ impl SymphoniaDecoder { } else if let Some(duration) = total_duration { // Fallback to duration-based calculation let total_secs = duration.as_secs_f64(); - let sample_rate = spec.rate as f64; - let channels = spec.channels.count() as f64; - Some((total_secs * sample_rate * channels).ceil() as u64) + Some((total_secs * sample_rate.get() as f64 * channels.get() as f64).ceil() as u64) } else { None } @@ -503,8 +518,10 @@ impl SymphoniaDecoder { current_span_offset: 0, demuxer: probed.format, total_duration, + sample_rate, + channels, + bits_per_sample, buffer, - spec, seek_mode: settings.seek_mode, total_samples, samples_read: 0, @@ -513,6 +530,26 @@ impl SymphoniaDecoder { byte_len, })) } + + /// Parses the signal specification from the decoder and returns sample rate, channel count, + /// and bit depth. + fn cache_spec(&mut self) { + if let Some(rate) = self.decoder.codec_params().sample_rate { + if let Some(rate) = SampleRate::new(rate) { + self.sample_rate = rate; + } + } + + if let Some(channels) = self.decoder.codec_params().channels { + if let Some(count) = channels.count().try_into().ok().and_then(ChannelCount::new) { + self.channels = count; + } + } + + if let Some(bits_per_sample) = self.decoder.codec_params().bits_per_sample { + self.bits_per_sample = BitDepth::new(bits_per_sample); + } + } } impl Source for SymphoniaDecoder { @@ -564,14 +601,7 @@ impl Source for SymphoniaDecoder { /// parameters change. #[inline] fn channels(&self) -> ChannelCount { - ChannelCount::new( - self.spec - .channels - .count() - .try_into() - .expect("rodio only support up to u16::MAX channels (65_535)"), - ) - .expect("audio should always have at least one channel") + self.channels } /// Returns the sample rate in Hz. @@ -591,7 +621,7 @@ impl Source for SymphoniaDecoder { /// parameters change. #[inline] fn sample_rate(&self) -> SampleRate { - SampleRate::new(self.spec.rate).expect("audio should always have a non zero SampleRate") + self.sample_rate } /// Returns the total duration of the audio stream. @@ -637,8 +667,8 @@ impl Source for SymphoniaDecoder { /// - `Some(depth)` for formats that preserve bit depth information /// - `None` for lossy formats or when bit depth is not determinable #[inline] - fn bits_per_sample(&self) -> Option { - self.decoder.codec_params().bits_per_sample + fn bits_per_sample(&self) -> Option { + self.bits_per_sample } /// Attempts to seek to the specified position in the audio stream. @@ -913,13 +943,10 @@ impl Iterator for SymphoniaDecoder { // If ResetRequired is returned, then the track list must be re-examined and all // Decoders re-created. Err(Error::ResetRequired) => { - self.track_id = recreate_decoder( - &mut self.demuxer, - &mut self.decoder, - Some(self.track_id), - Some(&mut self.spec), - ) - .ok()?; + self.track_id = + recreate_decoder(&mut self.demuxer, &mut self.decoder, Some(self.track_id)) + .ok()?; + self.cache_spec(); // Clear buffer after decoder reset - spec may have been updated self.buffer = None; @@ -1089,7 +1116,6 @@ fn recreate_decoder( format: &mut Box, decoder: &mut Box, current_track_id: Option, - spec: Option<&mut SignalSpec>, ) -> Result { let track = if let Some(current_id) = current_track_id { // During playback: find the next supported track after the current one @@ -1118,22 +1144,11 @@ fn recreate_decoder( "No supported track found after current track", ))?; - let new_track_id = track.id; - // Create new decoder *decoder = symphonia::default::get_codecs().make(&track.codec_params, &DecoderOptions::default())?; - // Update spec if provided - this will be refined on next successful decode - if let Some(spec) = spec { - if let Some(sample_rate) = track.codec_params.sample_rate { - if let Some(channels) = track.codec_params.channels { - *spec = SignalSpec::new(sample_rate, channels); - } - } - } - - Ok(new_track_id) + Ok(track.id) } /// Determines whether to continue decoding after a decode error. diff --git a/src/decoder/vorbis.rs b/src/decoder/vorbis.rs index 02189f80..2ab4b2d0 100644 --- a/src/decoder/vorbis.rs +++ b/src/decoder/vorbis.rs @@ -58,7 +58,6 @@ use std::{ io::{Read, Seek, SeekFrom}, - num::NonZero, sync::Arc, time::Duration, }; @@ -73,9 +72,7 @@ use lewton::{ use super::{utils, Settings}; use crate::{ - common::{ChannelCount, Sample, SampleRate}, - decoder::builder::SeekMode, - source::SeekError, + decoder::builder::SeekMode, source::SeekError, BitDepth, ChannelCount, Sample, SampleRate, Source, }; @@ -282,7 +279,7 @@ where let mut stream_reader = OggStreamReader::new(data).expect("should still be vorbis"); let current_data = read_next_non_empty_packet(&mut stream_reader); - let sample_rate = NonZero::new(stream_reader.ident_hdr.audio_sample_rate) + let sample_rate = SampleRate::new(stream_reader.ident_hdr.audio_sample_rate) .expect("vorbis has non-zero sample rate"); let channels = stream_reader.ident_hdr.audio_channels; @@ -552,7 +549,7 @@ where /// This method always returns `None` for Vorbis streams as bit depth is not /// a meaningful concept for lossy compressed audio formats. #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { None } diff --git a/src/decoder/wav.rs b/src/decoder/wav.rs index 45eeed44..95e433b9 100644 --- a/src/decoder/wav.rs +++ b/src/decoder/wav.rs @@ -74,10 +74,7 @@ use hound::{SampleFormat, WavReader}; use super::utils; use crate::{ - common::{ChannelCount, SampleRate}, - decoder::Settings, - source::SeekError, - Sample, Source, + decoder::Settings, source::SeekError, BitDepth, ChannelCount, Sample, SampleRate, Source, }; /// Decoder for the WAV format using the `hound` library. @@ -155,6 +152,11 @@ where /// mono (1), stereo (2), and various surround sound formats. channels: ChannelCount, + /// Bit depth of the audio samples (cached from reader). + /// + /// Cached from the WAV header without repeatedly constructing it. + bits_per_sample: BitDepth, + /// Whether random access seeking is enabled. /// /// When `true`, enables backward seeking using hound's direct seek functionality. @@ -272,13 +274,15 @@ where let samples_per_channel = len / (channels as u64); let total_duration = utils::samples_to_duration(samples_per_channel, sample_rate as u64); - Ok(WavDecoder { + Ok(Self { reader, total_duration, sample_rate: SampleRate::new(sample_rate) .expect("wav should have a sample rate higher then zero"), channels: ChannelCount::new(channels).expect("wav should have a least one channel"), is_seekable: settings.is_seekable, + bits_per_sample: BitDepth::new(spec.bits_per_sample.into()) + .expect("wav should have a bit depth higher then zero"), }) } @@ -548,8 +552,8 @@ where /// Always returns `Some(depth)` for valid WAV files. The bit depth is /// constant throughout the file and matches the fmt chunk specification. #[inline] - fn bits_per_sample(&self) -> Option { - Some(self.reader.reader.spec().bits_per_sample as u32) + fn bits_per_sample(&self) -> Option { + Some(self.bits_per_sample) } /// Attempts to seek to the specified position in the audio stream. diff --git a/src/lib.rs b/src/lib.rs index 39c456a9..a9a9bcd9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -191,7 +191,7 @@ pub mod queue; pub mod source; pub mod static_buffer; -pub use crate::common::{ChannelCount, Sample, SampleRate}; +pub use crate::common::{BitDepth, ChannelCount, Sample, SampleRate}; pub use crate::decoder::Decoder; pub use crate::sink::Sink; pub use crate::source::Source; diff --git a/src/microphone.rs b/src/microphone.rs index 9130a767..8f655c89 100644 --- a/src/microphone.rs +++ b/src/microphone.rs @@ -106,7 +106,7 @@ use std::{thread, time::Duration}; use crate::common::assert_error_traits; use crate::conversions::SampleTypeConverter; -use crate::{Sample, Source}; +use crate::{BitDepth, Sample, Source}; mod builder; mod config; @@ -182,37 +182,22 @@ impl Source for Microphone { self.config.sample_rate } - // TODO: use cpal::SampleFormat::bits_per_sample() when cpal v0.17 is released - fn bits_per_sample(&self) -> Option { - let bits = match self.config.sample_format { - cpal::SampleFormat::I8 | cpal::SampleFormat::U8 => 8, - cpal::SampleFormat::I16 | cpal::SampleFormat::U16 => 16, - cpal::SampleFormat::I24 => 24, - cpal::SampleFormat::I32 | cpal::SampleFormat::U32 | cpal::SampleFormat::F32 => 32, - cpal::SampleFormat::I64 | cpal::SampleFormat::U64 | cpal::SampleFormat::F64 => 64, - _ => return None, - }; - - Some(bits) - } - fn total_duration(&self) -> Option { None } - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { + // TODO: use cpal::SampleFormat::bits_per_sample() when cpal v0.17 is released let bits = match self.config.sample_format { cpal::SampleFormat::I8 | cpal::SampleFormat::U8 => 8, cpal::SampleFormat::I16 | cpal::SampleFormat::U16 => 16, cpal::SampleFormat::I24 => 24, - cpal::SampleFormat::I32 | cpal::SampleFormat::U32 => 32, - cpal::SampleFormat::I64 | cpal::SampleFormat::U64 => 64, - cpal::SampleFormat::F32 => f32::MANTISSA_DIGITS, - cpal::SampleFormat::F64 => f64::MANTISSA_DIGITS, + cpal::SampleFormat::I32 | cpal::SampleFormat::U32 | cpal::SampleFormat::F32 => 32, + cpal::SampleFormat::I64 | cpal::SampleFormat::U64 | cpal::SampleFormat::F64 => 64, _ => return None, }; - Some(bits) + BitDepth::new(bits) } } diff --git a/src/microphone/builder.rs b/src/microphone/builder.rs index 387a198f..8f23bccc 100644 --- a/src/microphone/builder.rs +++ b/src/microphone/builder.rs @@ -230,12 +230,14 @@ where /// Sets a custom input configuration. /// /// # Example + /// /// ```no_run - /// # use rodio::microphone::{MicrophoneBuilder, InputConfig}; - /// # use std::num::NonZero; + /// use rodio::{ChannelCount, SampleRate}; + /// use rodio::microphone::{MicrophoneBuilder, InputConfig}; + /// /// let config = InputConfig { - /// sample_rate: NonZero::new(44_100).expect("44100 is not zero"), - /// channel_count: NonZero::new(2).expect("2 is not zero"), + /// sample_rate: SampleRate::new(44_100).unwrap(), + /// channel_count: ChannelCount::new(2).unwrap(), /// buffer_size: cpal::BufferSize::Fixed(42_000), /// sample_format: cpal::SampleFormat::U16, /// }; diff --git a/src/microphone/config.rs b/src/microphone/config.rs index 21c6d5c5..d3e0459e 100644 --- a/src/microphone/config.rs +++ b/src/microphone/config.rs @@ -1,5 +1,3 @@ -use std::num::NonZero; - use crate::{math::nz, ChannelCount, SampleRate}; /// Describes the input stream's configuration @@ -60,9 +58,9 @@ impl From for InputConfig { cpal::SupportedBufferSize::Unknown => cpal::BufferSize::Default, }; Self { - channel_count: NonZero::new(value.channels()) + channel_count: ChannelCount::new(value.channels()) .expect("A supported config never has 0 channels"), - sample_rate: NonZero::new(value.sample_rate().0) + sample_rate: SampleRate::new(value.sample_rate().0) .expect("A supported config produces samples"), buffer_size, sample_format: value.sample_format(), diff --git a/src/mixer.rs b/src/mixer.rs index 435cab7c..50b70bfb 100644 --- a/src/mixer.rs +++ b/src/mixer.rs @@ -2,7 +2,7 @@ use crate::common::{ChannelCount, SampleRate}; use crate::source::{SeekError, Source, UniformSourceIterator}; -use crate::Sample; +use crate::{BitDepth, Sample}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -106,7 +106,7 @@ impl Source for MixerSource { } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.current_sources .iter() .flat_map(|s| s.bits_per_sample()) diff --git a/src/queue.rs b/src/queue.rs index 27b191c3..7538cb08 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -6,7 +6,7 @@ use std::time::Duration; use crate::math::nz; use crate::source::{Empty, SeekError, Source, Zero}; -use crate::Sample; +use crate::{BitDepth, Sample}; use crate::common::{ChannelCount, SampleRate}; #[cfg(feature = "crossbeam-channel")] @@ -168,7 +168,7 @@ impl Source for SourcesQueueOutput { } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.current.bits_per_sample() } diff --git a/src/source/agc.rs b/src/source/agc.rs index a7711815..0c5530c5 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -14,7 +14,7 @@ // use super::SeekError; -use crate::Source; +use crate::{BitDepth, Source}; #[cfg(feature = "experimental")] use atomic_float::AtomicF32; #[cfg(feature = "experimental")] @@ -491,7 +491,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.input.bits_per_sample() } diff --git a/src/source/amplify.rs b/src/source/amplify.rs index 3e6a0716..8528cc35 100644 --- a/src/source/amplify.rs +++ b/src/source/amplify.rs @@ -3,7 +3,7 @@ use std::time::Duration; use super::SeekError; use crate::{ common::{ChannelCount, SampleRate}, - math, Source, + math, BitDepth, Source, }; /// Internal function that builds a `Amplify` object. @@ -97,7 +97,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.input.bits_per_sample() } diff --git a/src/source/blt.rs b/src/source/blt.rs index 174339d2..4e3c652e 100644 --- a/src/source/blt.rs +++ b/src/source/blt.rs @@ -1,5 +1,5 @@ use crate::common::{ChannelCount, SampleRate}; -use crate::Source; +use crate::{BitDepth, Source}; use std::f32::consts::PI; use std::time::Duration; @@ -174,7 +174,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.input.bits_per_sample() } diff --git a/src/source/buffered.rs b/src/source/buffered.rs index 8b269fc2..193fb45c 100644 --- a/src/source/buffered.rs +++ b/src/source/buffered.rs @@ -6,6 +6,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; use crate::math::nz; +use crate::BitDepth; use crate::Source; /// Internal function that builds a `Buffered` object. @@ -62,7 +63,7 @@ where data: Vec, channels: ChannelCount, rate: SampleRate, - bits_per_sample: Option, + bits_per_sample: Option, next: Mutex>>, } @@ -238,7 +239,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { match *self.current_span { Span::Data(SpanData { bits_per_sample, .. diff --git a/src/source/channel_volume.rs b/src/source/channel_volume.rs index 60fb10ef..fce13ed9 100644 --- a/src/source/channel_volume.rs +++ b/src/source/channel_volume.rs @@ -2,7 +2,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::{Sample, Source}; +use crate::{BitDepth, Sample, Source}; /// Combines channels in input into a single mono source, then plays that mono sound /// to each channel at the volume given for that channel. @@ -120,7 +120,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.input.bits_per_sample() } diff --git a/src/source/chirp.rs b/src/source/chirp.rs index 9216b036..4e9f783a 100644 --- a/src/source/chirp.rs +++ b/src/source/chirp.rs @@ -6,7 +6,7 @@ use crate::{ common::{ChannelCount, SampleRate}, math::nz, source::SeekError, - Source, + BitDepth, Source, }; /// Convenience function to create a new `Chirp` source. @@ -103,7 +103,7 @@ impl Source for Chirp { } #[inline] - fn bits_per_sample(&self) -> Option { - Some(f32::MANTISSA_DIGITS) + fn bits_per_sample(&self) -> Option { + BitDepth::new(32) } } diff --git a/src/source/delay.rs b/src/source/delay.rs index 90fde8f6..4f871804 100644 --- a/src/source/delay.rs +++ b/src/source/delay.rs @@ -2,7 +2,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::Source; +use crate::{BitDepth, Source}; fn remaining_samples( until_playback: Duration, @@ -112,7 +112,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.input.bits_per_sample() } diff --git a/src/source/distortion.rs b/src/source/distortion.rs index a13e88e8..0ca0b5c1 100644 --- a/src/source/distortion.rs +++ b/src/source/distortion.rs @@ -2,7 +2,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::Source; +use crate::{BitDepth, Source}; /// Internal function that builds a `Distortion` object. pub(crate) fn distortion(input: I, gain: f32, threshold: f32) -> Distortion @@ -104,7 +104,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.input.bits_per_sample() } diff --git a/src/source/done.rs b/src/source/done.rs index 8738eca9..317c25c7 100644 --- a/src/source/done.rs +++ b/src/source/done.rs @@ -4,7 +4,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::Source; +use crate::{BitDepth, Source}; /// When the inner source is empty this decrements a `AtomicUsize`. #[derive(Debug, Clone)] @@ -91,7 +91,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.input.bits_per_sample() } diff --git a/src/source/empty.rs b/src/source/empty.rs index 36040e47..7337a4c6 100644 --- a/src/source/empty.rs +++ b/src/source/empty.rs @@ -3,7 +3,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; use crate::math::nz; -use crate::{Sample, Source}; +use crate::{BitDepth, Sample, Source}; /// An empty source. #[derive(Debug, Copy, Clone)] @@ -56,7 +56,7 @@ impl Source for Empty { } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { None } diff --git a/src/source/empty_callback.rs b/src/source/empty_callback.rs index c05b51ce..c820bdb0 100644 --- a/src/source/empty_callback.rs +++ b/src/source/empty_callback.rs @@ -3,7 +3,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; use crate::math::nz; -use crate::{Sample, Source}; +use crate::{BitDepth, Sample, Source}; /// An empty source that executes a callback function pub struct EmptyCallback { @@ -53,7 +53,7 @@ impl Source for EmptyCallback { } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { None } diff --git a/src/source/fadein.rs b/src/source/fadein.rs index 27d30226..f8fd483b 100644 --- a/src/source/fadein.rs +++ b/src/source/fadein.rs @@ -2,7 +2,7 @@ use std::time::Duration; use super::{linear_ramp::linear_gain_ramp, LinearGainRamp, SeekError}; use crate::common::{ChannelCount, SampleRate}; -use crate::Source; +use crate::{BitDepth, Source}; /// Internal function that builds a `FadeIn` object. pub fn fadein(input: I, duration: Duration) -> FadeIn @@ -87,7 +87,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.inner().bits_per_sample() } diff --git a/src/source/fadeout.rs b/src/source/fadeout.rs index c83c4e75..64c01c99 100644 --- a/src/source/fadeout.rs +++ b/src/source/fadeout.rs @@ -2,7 +2,7 @@ use std::time::Duration; use super::{linear_ramp::linear_gain_ramp, LinearGainRamp, SeekError}; use crate::common::{ChannelCount, SampleRate}; -use crate::Source; +use crate::{BitDepth, Source}; /// Internal function that builds a `FadeOut` object. pub fn fadeout(input: I, duration: Duration) -> FadeOut @@ -87,7 +87,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.inner().bits_per_sample() } diff --git a/src/source/from_iter.rs b/src/source/from_iter.rs index a979e887..7085357b 100644 --- a/src/source/from_iter.rs +++ b/src/source/from_iter.rs @@ -3,7 +3,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; use crate::math::nz; -use crate::Source; +use crate::{BitDepth, Source}; /// Builds a source that chains sources provided by an iterator. /// @@ -138,7 +138,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { if let Some(src) = &self.current_source { src.bits_per_sample() } else { diff --git a/src/source/limit.rs b/src/source/limit.rs index 55c32fad..2e98a367 100644 --- a/src/source/limit.rs +++ b/src/source/limit.rs @@ -63,7 +63,7 @@ use std::time::Duration; use super::SeekError; use crate::{ common::{ChannelCount, Sample, SampleRate}, - math, Source, + math, BitDepth, Source, }; /// Configuration settings for audio limiting. @@ -596,7 +596,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.0.bits_per_sample() } @@ -1086,7 +1086,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.inner().bits_per_sample() } diff --git a/src/source/linear_ramp.rs b/src/source/linear_ramp.rs index 22ddfecf..56ae1476 100644 --- a/src/source/linear_ramp.rs +++ b/src/source/linear_ramp.rs @@ -2,7 +2,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::Source; +use crate::{BitDepth, Source}; /// Internal function that builds a `LinearRamp` object. pub fn linear_gain_ramp( @@ -128,7 +128,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.input.bits_per_sample() } diff --git a/src/source/mix.rs b/src/source/mix.rs index 66ab808c..c43d8fd2 100644 --- a/src/source/mix.rs +++ b/src/source/mix.rs @@ -4,7 +4,7 @@ use std::time::Duration; use crate::common::{ChannelCount, SampleRate}; use crate::source::uniform::UniformSourceIterator; use crate::source::SeekError; -use crate::Source; +use crate::{BitDepth, Source}; /// Internal function that builds a `Mix` object. pub fn mix(input1: I1, input2: I2) -> Mix @@ -114,7 +114,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { let f1 = self.input1.bits_per_sample(); let f2 = self.input2.bits_per_sample(); diff --git a/src/source/mod.rs b/src/source/mod.rs index 11ab2f8c..3434bbae 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use crate::{ buffer::SamplesBuffer, common::{assert_error_traits, ChannelCount, SampleRate}, - math, Sample, + math, BitDepth, Sample, }; use dasp_sample::FromSample; @@ -193,13 +193,17 @@ pub trait Source: Iterator { /// Returns the number of bits per sample, if known. /// + /// # Format Support + /// + /// For lossy formats this should always return `None` as bit depth is not a meaningful + /// concept for compressed audio. + /// /// # Implementation Note /// - /// This function returns the bit depth of the original audio stream when available, - /// not the bit depth of Rodio's internal sample representation. Up to 24 bits of - /// information is preserved from the original stream and used for proper sample - /// scaling during conversion to Rodio's sample format. - fn bits_per_sample(&self) -> Option; + /// Rodio processes audio samples internally as 32-bit floating point values. When audio data + /// is converted to integer output, up to 24 bits of information is preserved from the + /// original audio stream. + fn bits_per_sample(&self) -> Option; /// Stores the source in a buffer in addition to returning it. This iterator can be cloned. #[inline] @@ -806,7 +810,7 @@ macro_rules! source_pointer_impl { } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { (**self).bits_per_sample() } diff --git a/src/source/noise.rs b/src/source/noise.rs index b35fb160..e6aa318b 100644 --- a/src/source/noise.rs +++ b/src/source/noise.rs @@ -20,7 +20,7 @@ //! use std::num::NonZero; //! use rodio::source::noise::{WhiteUniform, Pink, WhiteTriangular, Blue, Red}; //! -//! let sample_rate = NonZero::new(44100).unwrap(); +//! let sample_rate = SampleRate::new(44100).unwrap(); //! //! // Simple usage - creates generators with `SmallRng` //! @@ -50,7 +50,7 @@ use rand::{ use rand_distr::{Normal, Triangular}; use crate::math::nz; -use crate::{ChannelCount, Sample, SampleRate, Source}; +use crate::{BitDepth, ChannelCount, Sample, SampleRate, Source}; /// Convenience function to create a new `WhiteUniform` noise source. #[deprecated(since = "0.21.0", note = "use WhiteUniform::new() instead")] @@ -85,8 +85,8 @@ macro_rules! impl_noise_source { None } - fn bits_per_sample(&self) -> Option { - Some(f32::MANTISSA_DIGITS) + fn bits_per_sample(&self) -> Option { + BitDepth::new(32) } fn try_seek(&mut self, _pos: Duration) -> Result<(), crate::source::SeekError> { @@ -751,8 +751,8 @@ impl Source for Brownian { None } - fn bits_per_sample(&self) -> Option { - Some(f32::MANTISSA_DIGITS) + fn bits_per_sample(&self) -> Option { + BitDepth::new(32) } fn try_seek(&mut self, _pos: Duration) -> Result<(), crate::source::SeekError> { @@ -832,8 +832,8 @@ impl Source for Red { None } - fn bits_per_sample(&self) -> Option { - Some(f32::MANTISSA_DIGITS) + fn bits_per_sample(&self) -> Option { + BitDepth::new(32) } fn try_seek(&mut self, _pos: Duration) -> Result<(), crate::source::SeekError> { diff --git a/src/source/pausable.rs b/src/source/pausable.rs index 6e885254..9e04e6c6 100644 --- a/src/source/pausable.rs +++ b/src/source/pausable.rs @@ -2,7 +2,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::Source; +use crate::{BitDepth, Source}; /// Builds a `Pausable` object. pub fn pausable(source: I, paused: bool) -> Pausable @@ -127,7 +127,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.input.bits_per_sample() } diff --git a/src/source/periodic.rs b/src/source/periodic.rs index 3c8a33cc..886919fb 100644 --- a/src/source/periodic.rs +++ b/src/source/periodic.rs @@ -3,7 +3,7 @@ use std::time::Duration; use super::SeekError; use crate::{ common::{ChannelCount, SampleRate}, - Source, + BitDepth, Source, }; /// Internal function that builds a `PeriodicAccess` object. @@ -118,7 +118,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.input.bits_per_sample() } diff --git a/src/source/position.rs b/src/source/position.rs index fa9b0b82..57a73de0 100644 --- a/src/source/position.rs +++ b/src/source/position.rs @@ -3,7 +3,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; use crate::math::nz; -use crate::Source; +use crate::{BitDepth, Source}; /// Internal function that builds a `TrackPosition` object. See trait docs for /// details @@ -143,7 +143,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.input.bits_per_sample() } diff --git a/src/source/repeat.rs b/src/source/repeat.rs index 73e9300e..217d139c 100644 --- a/src/source/repeat.rs +++ b/src/source/repeat.rs @@ -4,7 +4,7 @@ use crate::source::buffered::Buffered; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::Source; +use crate::{BitDepth, Source}; /// Internal function that builds a `Repeat` object. pub fn repeat(input: I) -> Repeat @@ -84,7 +84,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { match self.inner.current_span_len() { Some(0) => self.next.bits_per_sample(), _ => self.inner.bits_per_sample(), diff --git a/src/source/sawtooth.rs b/src/source/sawtooth.rs index f96a61ca..ba8ff807 100644 --- a/src/source/sawtooth.rs +++ b/src/source/sawtooth.rs @@ -1,7 +1,6 @@ use crate::common::{ChannelCount, SampleRate}; -use crate::math::nz; use crate::source::{Function, SignalGenerator}; -use crate::Source; +use crate::{nz, BitDepth, Source}; use std::time::Duration; use super::SeekError; @@ -60,8 +59,8 @@ impl Source for SawtoothWave { } #[inline] - fn bits_per_sample(&self) -> Option { - Some(f32::MANTISSA_DIGITS) + fn bits_per_sample(&self) -> Option { + BitDepth::new(32) } #[inline] diff --git a/src/source/signal_generator.rs b/src/source/signal_generator.rs index c07d51f2..1da139e9 100644 --- a/src/source/signal_generator.rs +++ b/src/source/signal_generator.rs @@ -7,15 +7,15 @@ //! # Example //! //! ``` +//! use rodio::SampleRate;; //! use rodio::source::{SignalGenerator,Function}; -//! use core::num::NonZero; //! -//! let tone = SignalGenerator::new(NonZero::new(48000).unwrap(), 440.0, Function::Sine); +//! let tone = SignalGenerator::new(SampleRate::new(48_000).unwrap(), 440.0, Function::Sine); //! ``` use super::SeekError; use crate::common::{ChannelCount, SampleRate}; use crate::math::nz; -use crate::Source; +use crate::{BitDepth, Source}; use std::f32::consts::TAU; use std::time::Duration; @@ -159,8 +159,8 @@ impl Source for SignalGenerator { } #[inline] - fn bits_per_sample(&self) -> Option { - Some(f32::MANTISSA_DIGITS) + fn bits_per_sample(&self) -> Option { + BitDepth::new(32) } #[inline] diff --git a/src/source/sine.rs b/src/source/sine.rs index 67f5c1df..0d0f0275 100644 --- a/src/source/sine.rs +++ b/src/source/sine.rs @@ -1,7 +1,7 @@ use crate::common::{ChannelCount, SampleRate}; use crate::math::nz; use crate::source::{Function, SignalGenerator}; -use crate::Source; +use crate::{BitDepth, Source}; use std::time::Duration; use super::SeekError; @@ -60,8 +60,8 @@ impl Source for SineWave { } #[inline] - fn bits_per_sample(&self) -> Option { - Some(f32::MANTISSA_DIGITS) + fn bits_per_sample(&self) -> Option { + BitDepth::new(32) } #[inline] diff --git a/src/source/skip.rs b/src/source/skip.rs index 164acc4a..dbcab6ed 100644 --- a/src/source/skip.rs +++ b/src/source/skip.rs @@ -2,7 +2,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::Source; +use crate::{BitDepth, Source}; const NS_PER_SECOND: u128 = 1_000_000_000; @@ -154,7 +154,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.input.bits_per_sample() } diff --git a/src/source/skippable.rs b/src/source/skippable.rs index eacd2b52..a5f4f3b4 100644 --- a/src/source/skippable.rs +++ b/src/source/skippable.rs @@ -1,5 +1,4 @@ -use crate::common::{ChannelCount, SampleRate}; -use crate::Source; +use crate::{BitDepth, ChannelCount, SampleRate, Source}; use std::time::Duration; use super::SeekError; @@ -95,7 +94,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.input.bits_per_sample() } diff --git a/src/source/spatial.rs b/src/source/spatial.rs index fbd84241..d376a09f 100644 --- a/src/source/spatial.rs +++ b/src/source/spatial.rs @@ -3,7 +3,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; use crate::source::ChannelVolume; -use crate::Source; +use crate::{BitDepth, Source}; /// A simple spatial audio source. The underlying source is transformed to Mono /// and then played in stereo. The left and right channel's volume are amplified @@ -113,7 +113,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.input.bits_per_sample() } diff --git a/src/source/speed.rs b/src/source/speed.rs index 9dc384b2..43c3a601 100644 --- a/src/source/speed.rs +++ b/src/source/speed.rs @@ -50,7 +50,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::Source; +use crate::{BitDepth, Source}; /// Internal function that builds a `Speed` object. pub fn speed(input: I, factor: f32) -> Speed { @@ -138,7 +138,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.input.bits_per_sample() } diff --git a/src/source/square.rs b/src/source/square.rs index 298a9e33..01ffcdcc 100644 --- a/src/source/square.rs +++ b/src/source/square.rs @@ -1,7 +1,7 @@ use crate::common::{ChannelCount, SampleRate}; use crate::math::nz; use crate::source::{Function, SignalGenerator}; -use crate::Source; +use crate::{BitDepth, Source}; use std::time::Duration; use super::SeekError; @@ -60,8 +60,8 @@ impl Source for SquareWave { } #[inline] - fn bits_per_sample(&self) -> Option { - Some(f32::MANTISSA_DIGITS) + fn bits_per_sample(&self) -> Option { + BitDepth::new(32) } #[inline] diff --git a/src/source/stoppable.rs b/src/source/stoppable.rs index 96b4765a..092533ac 100644 --- a/src/source/stoppable.rs +++ b/src/source/stoppable.rs @@ -2,7 +2,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::Source; +use crate::{BitDepth, Source}; /// This is the same as [`skippable`](crate::source::skippable) see its docs pub fn stoppable(source: I) -> Stoppable { @@ -91,7 +91,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.input.bits_per_sample() } diff --git a/src/source/take.rs b/src/source/take.rs index 98c34eb3..e956312f 100644 --- a/src/source/take.rs +++ b/src/source/take.rs @@ -2,7 +2,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::{Sample, Source}; +use crate::{BitDepth, Sample, Source}; /// Internal function that builds a `TakeDuration` object. pub fn take_duration(input: I, duration: Duration) -> TakeDuration @@ -171,7 +171,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.input.bits_per_sample() } diff --git a/src/source/triangle.rs b/src/source/triangle.rs index 5309de1a..55f60407 100644 --- a/src/source/triangle.rs +++ b/src/source/triangle.rs @@ -1,7 +1,7 @@ use crate::common::{ChannelCount, SampleRate}; use crate::math::nz; use crate::source::{Function, SignalGenerator}; -use crate::Source; +use crate::{BitDepth, Source}; use std::time::Duration; use super::SeekError; @@ -60,8 +60,8 @@ impl Source for TriangleWave { } #[inline] - fn bits_per_sample(&self) -> Option { - Some(f32::MANTISSA_DIGITS) + fn bits_per_sample(&self) -> Option { + BitDepth::new(32) } #[inline] diff --git a/src/source/uniform.rs b/src/source/uniform.rs index 0e06b6fc..45224833 100644 --- a/src/source/uniform.rs +++ b/src/source/uniform.rs @@ -4,7 +4,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; use crate::conversions::{ChannelCountConverter, SampleRateConverter}; -use crate::Source; +use crate::{BitDepth, Source}; /// An iterator that reads from a `Source` and converts the samples to a /// specific type, sample-rate and channels count. @@ -20,7 +20,7 @@ where target_channels: ChannelCount, target_sample_rate: SampleRate, total_duration: Option, - bits_per_sample: Option, + bits_per_sample: Option, } impl UniformSourceIterator @@ -123,7 +123,7 @@ where } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { self.bits_per_sample } diff --git a/src/source/zero.rs b/src/source/zero.rs index 4d405d4f..c9e40d11 100644 --- a/src/source/zero.rs +++ b/src/source/zero.rs @@ -2,7 +2,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::{Sample, Source}; +use crate::{BitDepth, Sample, Source}; /// An source that produces samples with value zero (silence). Depending on if /// it where created with [`Zero::new`] or [`Zero::new_samples`] it can be never @@ -79,7 +79,7 @@ impl Source for Zero { } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { None } diff --git a/src/static_buffer.rs b/src/static_buffer.rs index 05dc189e..515fa584 100644 --- a/src/static_buffer.rs +++ b/src/static_buffer.rs @@ -5,9 +5,10 @@ //! # Example //! //! ``` +//! use rodio::{ChannelCount, SampleRate}; //! use rodio::static_buffer::StaticSamplesBuffer; -//! use core::num::NonZero; -//! let _ = StaticSamplesBuffer::new(NonZero::new(1).unwrap(), NonZero::new(44100).unwrap(), &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); +//! +//! let _ = StaticSamplesBuffer::new(ChannelCount::new(1).unwrap(), SampleRate::new(44_100).unwrap(), &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); //! ``` //! @@ -17,7 +18,7 @@ use std::time::Duration; use crate::common::{ChannelCount, SampleRate}; use crate::source::SeekError; -use crate::{Sample, Source}; +use crate::{BitDepth, Sample, Source}; /// A buffer of samples treated as a source. #[derive(Clone)] @@ -92,7 +93,7 @@ impl Source for StaticSamplesBuffer { } #[inline] - fn bits_per_sample(&self) -> Option { + fn bits_per_sample(&self) -> Option { None } diff --git a/src/stream.rs b/src/stream.rs index 3707dc17..45f39db7 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -15,7 +15,6 @@ use cpal::{BufferSize, Sample, SampleFormat, StreamConfig}; use std::fmt; use std::io::{Read, Seek}; use std::marker::Sync; -use std::num::NonZero; const HZ_44100: SampleRate = nz!(44_100); @@ -297,9 +296,9 @@ where config: &cpal::SupportedStreamConfig, ) -> OutputStreamBuilder { self.config = OutputStreamConfig { - channel_count: NonZero::new(config.channels()) + channel_count: ChannelCount::new(config.channels()) .expect("no valid cpal config has zero channels"), - sample_rate: NonZero::new(config.sample_rate().0) + sample_rate: SampleRate::new(config.sample_rate().0) .expect("no valid cpal config has zero sample rate"), sample_format: config.sample_format(), ..Default::default() @@ -310,9 +309,9 @@ where /// Set all output stream parameters at once from CPAL stream config. pub fn with_config(mut self, config: &cpal::StreamConfig) -> OutputStreamBuilder { self.config = OutputStreamConfig { - channel_count: NonZero::new(config.channels) + channel_count: ChannelCount::new(config.channels) .expect("no valid cpal config has zero channels"), - sample_rate: NonZero::new(config.sample_rate.0) + sample_rate: SampleRate::new(config.sample_rate.0) .expect("no valid cpal config has zero sample rate"), buffer_size: config.buffer_size, ..self.config diff --git a/tests/limit.rs b/tests/limit.rs index 62434f97..c8032aab 100644 --- a/tests/limit.rs +++ b/tests/limit.rs @@ -1,5 +1,5 @@ use rodio::source::Source; -use std::num::NonZero; +use rodio::{ChannelCount, SampleRate}; use std::time::Duration; #[test] @@ -131,8 +131,8 @@ fn test_limiter_stereo_processing() { } let buffer = SamplesBuffer::new( - NonZero::new(2).unwrap(), - NonZero::new(44100).unwrap(), + ChannelCount::new(2).unwrap(), + SampleRate::new(44100).unwrap(), stereo_samples, ); let settings = rodio::source::LimitSettings::default().with_threshold(-3.0); diff --git a/tests/source_traits.rs b/tests/source_traits.rs index 5e612e75..478fe6cb 100644 --- a/tests/source_traits.rs +++ b/tests/source_traits.rs @@ -2,6 +2,7 @@ #![allow(unused_imports)] use std::io::{Read, Seek}; +use std::num::NonZeroU32; use std::path::Path; use std::time::Duration; @@ -253,6 +254,8 @@ fn decoder_returns_correct_channels( ) { use std::num::NonZero; + use rodio::ChannelCount; + println!("decoder: {decoder_name}"); let decoder = get_music(format); let channels = decoder.channels(); @@ -260,7 +263,7 @@ fn decoder_returns_correct_channels( // All our test files should be stereo (2 channels) assert_eq!( channels, - NonZero::new(2).unwrap(), + ChannelCount::new(2).unwrap(), "decoder {decoder_name} returned {channels} channels, expected 2 (stereo)" ); } @@ -307,7 +310,7 @@ fn decoder_returns_some_bit_depth( let returned_bit_depth = decoder.bits_per_sample(); assert_eq!( returned_bit_depth, - Some(bit_depth), + NonZeroU32::new(bit_depth), "decoder {decoder_name} returned {returned_bit_depth:?} bit depth, expected {bit_depth}" ); } @@ -331,10 +334,10 @@ fn decoder_returns_hi_res_bit_depths() { // TODO: Symphonia returns None for audacity32bit.wav (float) if let Some(returned_bit_depth) = decoder.bits_per_sample() { assert_eq!( - returned_bit_depth, - bit_depth, - "decoder for {asset} returned {returned_bit_depth:?} bit depth, expected {bit_depth}" - ); + returned_bit_depth.get(), + bit_depth, + "decoder for {asset} returned {returned_bit_depth:?} bit depth, expected {bit_depth}" + ); } } } From 4187282602213a7e103762717de3ec706f142834 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 6 Sep 2025 14:53:56 +0200 Subject: [PATCH 10/17] refactor: use Decoder::try_from for file loading in examples and tests --- CHANGELOG.md | 10 ++++++++++ examples/automatic_gain_control.rs | 6 +++--- examples/callback_on_end.rs | 4 ++-- examples/distortion_mp3.rs | 4 ++-- examples/distortion_wav.rs | 4 ++-- examples/distortion_wav_alternate.rs | 4 ++-- examples/into_file.rs | 4 ++-- examples/limit_wav.rs | 4 ++-- examples/low_pass.rs | 5 ++--- examples/music_flac.rs | 4 ++-- examples/music_m4a.rs | 4 ++-- examples/music_mp3.rs | 4 ++-- examples/music_wav.rs | 4 ++-- examples/reverb.rs | 4 ++-- examples/seek_mp3.rs | 4 ++-- examples/spatial.rs | 4 ++-- examples/stereo.rs | 4 ++-- src/decoder/mod.rs | 12 ++++++------ src/lib.rs | 4 ++-- src/source/speed.rs | 4 ++-- src/wav_output.rs | 16 +++++++--------- tests/flac_test.rs | 9 +++++---- tests/mp3_test.rs | 4 ++-- tests/seek.rs | 6 ++---- tests/source_traits.rs | 5 +++-- tests/wav_test.rs | 24 ++++++++++++------------ 26 files changed, 84 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81c1d4ca..39f48ebf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `with_scan_duration()` - Enable file scanning for duration computation. - All alternative decoders now support `Settings` via `new_with_settings()`. - Symphonia decoder handles multi-track containers and chained Ogg streams. +- Added `Decoder::TryFrom` implementations for memory-based types: + - `Vec` - Decoding from owned byte vectors + - `Box<[u8]>` - Decoding from boxed byte slices + - `Arc<[u8]>` - Decoding from shared byte slices + - `&'static [u8]` - Decoding from embedded static data + - `Cow<'static, [u8]>` - Decoding from borrowed or owned byte data + - `bytes::Bytes` - Decoding from bytes crate (requires `bytes` feature) + - `&Path` and `PathBuf` - Convenient file path decoding with format hints +- `TryFrom>` and `TryFrom>` now use `DecoderBuilder` with optimized settings + instead of basic `Decoder::new()` - enabling seeking and byte length detection where possible. ### Changed - `output_to_wav` renamed to `wav_to_file` and now takes ownership of the `Source`. diff --git a/examples/automatic_gain_control.rs b/examples/automatic_gain_control.rs index 884d046a..71146c9e 100644 --- a/examples/automatic_gain_control.rs +++ b/examples/automatic_gain_control.rs @@ -1,7 +1,7 @@ use rodio::source::Source; use rodio::Decoder; use std::error::Error; -use std::fs::File; +use std::path::Path; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::thread; @@ -12,8 +12,8 @@ fn main() -> Result<(), Box> { let sink = rodio::Sink::connect_new(stream_handle.mixer()); // Decode the sound file into a source - let file = File::open("assets/music.flac")?; - let source = Decoder::try_from(file)?; + let path = Path::new("assets/music.flac"); + let source = Decoder::try_from(path)?; // Apply automatic gain control to the source let agc_source = source.automatic_gain_control(1.0, 4.0, 0.005, 5.0); diff --git a/examples/callback_on_end.rs b/examples/callback_on_end.rs index f367f7f4..33341026 100644 --- a/examples/callback_on_end.rs +++ b/examples/callback_on_end.rs @@ -6,8 +6,8 @@ fn main() -> Result<(), Box> { let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(stream_handle.mixer()); - let file = std::fs::File::open("assets/music.wav")?; - sink.append(rodio::Decoder::try_from(file)?); + let path = std::path::Path::new("assets/music.wav"); + sink.append(rodio::Decoder::try_from(path)?); // lets increment a number after `music.wav` has played. We are going to use atomics // however you could also use a `Mutex` or send a message through a `std::sync::mpsc`. diff --git a/examples/distortion_mp3.rs b/examples/distortion_mp3.rs index 895c7647..f9edb7a7 100644 --- a/examples/distortion_mp3.rs +++ b/examples/distortion_mp3.rs @@ -6,9 +6,9 @@ fn main() -> Result<(), Box> { let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(stream_handle.mixer()); - let file = std::fs::File::open("assets/music.mp3")?; + let path = std::path::Path::new("assets/music.mp3"); // Apply distortion effect before appending to the sink - let source = rodio::Decoder::try_from(file)?.distortion(4.0, 0.3); + let source = rodio::Decoder::try_from(path)?.distortion(4.0, 0.3); sink.append(source); sink.sleep_until_end(); diff --git a/examples/distortion_wav.rs b/examples/distortion_wav.rs index 0955fd51..eb80f772 100644 --- a/examples/distortion_wav.rs +++ b/examples/distortion_wav.rs @@ -6,9 +6,9 @@ fn main() -> Result<(), Box> { let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(stream_handle.mixer()); - let file = std::fs::File::open("assets/music.wav")?; + let path = std::path::Path::new("assets/music.wav"); // Apply distortion effect before appending to the sink - let source = rodio::Decoder::try_from(file)?.distortion(4.0, 0.3); + let source = rodio::Decoder::try_from(path)?.distortion(4.0, 0.3); sink.append(source); sink.sleep_until_end(); diff --git a/examples/distortion_wav_alternate.rs b/examples/distortion_wav_alternate.rs index 8d3f36b4..b6b460b0 100644 --- a/examples/distortion_wav_alternate.rs +++ b/examples/distortion_wav_alternate.rs @@ -12,8 +12,8 @@ fn main() -> Result<(), Box> { let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(stream_handle.mixer()); - let file = std::fs::File::open("assets/music.wav")?; - let source = rodio::Decoder::try_from(file)?; + let path = std::path::Path::new("assets/music.wav"); + let source = rodio::Decoder::try_from(path)?; // Shared flag to enable/disable distortion let distortion_enabled = Arc::new(AtomicBool::new(true)); diff --git a/examples/into_file.rs b/examples/into_file.rs index cc87a8e3..9136159a 100644 --- a/examples/into_file.rs +++ b/examples/into_file.rs @@ -5,8 +5,8 @@ use std::error::Error; /// This example does not use any audio devices /// and can be used in build configurations without `cpal` feature enabled. fn main() -> Result<(), Box> { - let file = std::fs::File::open("assets/music.mp3")?; - let mut audio = rodio::Decoder::try_from(file)? + let path = std::path::Path::new("assets/music.mp3"); + let mut audio = rodio::Decoder::try_from(path)? .automatic_gain_control(1.0, 4.0, 0.005, 3.0) .speed(0.8); diff --git a/examples/limit_wav.rs b/examples/limit_wav.rs index b104d3ff..a30a4ce1 100644 --- a/examples/limit_wav.rs +++ b/examples/limit_wav.rs @@ -5,8 +5,8 @@ fn main() -> Result<(), Box> { let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(stream_handle.mixer()); - let file = std::fs::File::open("assets/music.wav")?; - let source = rodio::Decoder::try_from(file)? + let path = std::path::Path::new("assets/music.wav"); + let source = rodio::Decoder::try_from(path)? .amplify(3.0) .limit(LimitSettings::default()); diff --git a/examples/low_pass.rs b/examples/low_pass.rs index ec343888..76f01be6 100644 --- a/examples/low_pass.rs +++ b/examples/low_pass.rs @@ -1,5 +1,4 @@ use std::error::Error; -use std::io::BufReader; use rodio::Source; @@ -7,8 +6,8 @@ fn main() -> Result<(), Box> { let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(stream_handle.mixer()); - let file = std::fs::File::open("assets/music.wav")?; - let decoder = rodio::Decoder::new(BufReader::new(file))?; + let path = std::path::Path::new("assets/music.wav"); + let decoder = rodio::Decoder::try_from(path)?; let source = decoder.low_pass(200); sink.append(source); diff --git a/examples/music_flac.rs b/examples/music_flac.rs index f93028be..eee2aed8 100644 --- a/examples/music_flac.rs +++ b/examples/music_flac.rs @@ -4,8 +4,8 @@ fn main() -> Result<(), Box> { let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(stream_handle.mixer()); - let file = std::fs::File::open("assets/music.flac")?; - sink.append(rodio::Decoder::try_from(file)?); + let path = std::path::Path::new("assets/music.flac"); + sink.append(rodio::Decoder::try_from(path)?); sink.sleep_until_end(); diff --git a/examples/music_m4a.rs b/examples/music_m4a.rs index b8a20921..a1800ad3 100644 --- a/examples/music_m4a.rs +++ b/examples/music_m4a.rs @@ -4,8 +4,8 @@ fn main() -> Result<(), Box> { let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(stream_handle.mixer()); - let file = std::fs::File::open("assets/music.m4a")?; - sink.append(rodio::Decoder::try_from(file)?); + let path = std::path::Path::new("assets/music.m4a"); + sink.append(rodio::Decoder::try_from(path)?); sink.sleep_until_end(); diff --git a/examples/music_mp3.rs b/examples/music_mp3.rs index bacc7309..609cecd0 100644 --- a/examples/music_mp3.rs +++ b/examples/music_mp3.rs @@ -4,8 +4,8 @@ fn main() -> Result<(), Box> { let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(stream_handle.mixer()); - let file = std::fs::File::open("assets/music.mp3")?; - sink.append(rodio::Decoder::try_from(file)?); + let path = std::path::Path::new("assets/music.mp3"); + sink.append(rodio::Decoder::try_from(path)?); sink.sleep_until_end(); diff --git a/examples/music_wav.rs b/examples/music_wav.rs index ea50de38..f8fba1f3 100644 --- a/examples/music_wav.rs +++ b/examples/music_wav.rs @@ -4,8 +4,8 @@ fn main() -> Result<(), Box> { let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(stream_handle.mixer()); - let file = std::fs::File::open("assets/music.wav")?; - sink.append(rodio::Decoder::try_from(file)?); + let path = std::path::Path::new("assets/music.wav"); + sink.append(rodio::Decoder::try_from(path)?); sink.sleep_until_end(); diff --git a/examples/reverb.rs b/examples/reverb.rs index b8c04b52..c5b546cc 100644 --- a/examples/reverb.rs +++ b/examples/reverb.rs @@ -6,8 +6,8 @@ fn main() -> Result<(), Box> { let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(stream_handle.mixer()); - let file = std::fs::File::open("assets/music.ogg")?; - let source = rodio::Decoder::try_from(file)?; + let path = std::path::Path::new("assets/music.ogg"); + let source = rodio::Decoder::try_from(path)?; let with_reverb = source.buffered().reverb(Duration::from_millis(40), 0.7); sink.append(with_reverb); diff --git a/examples/seek_mp3.rs b/examples/seek_mp3.rs index c27f5346..657e1b9f 100644 --- a/examples/seek_mp3.rs +++ b/examples/seek_mp3.rs @@ -5,8 +5,8 @@ fn main() -> Result<(), Box> { let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(stream_handle.mixer()); - let file = std::fs::File::open("assets/music.mp3")?; - sink.append(rodio::Decoder::try_from(file)?); + let path = std::path::Path::new("assets/music.mp3"); + sink.append(rodio::Decoder::try_from(path)?); std::thread::sleep(std::time::Duration::from_secs(2)); sink.try_seek(Duration::from_secs(0))?; diff --git a/examples/spatial.rs b/examples/spatial.rs index f1b97e44..d3609e48 100644 --- a/examples/spatial.rs +++ b/examples/spatial.rs @@ -28,8 +28,8 @@ fn main() -> Result<(), Box> { positions.2, ); - let file = std::fs::File::open("assets/music.ogg")?; - let source = rodio::Decoder::try_from(file)? + let path = std::path::Path::new("assets/music.ogg"); + let source = rodio::Decoder::try_from(path)? .repeat_infinite() .take_duration(total_duration); sink.append(source); diff --git a/examples/stereo.rs b/examples/stereo.rs index c86eef73..3c8b2c95 100644 --- a/examples/stereo.rs +++ b/examples/stereo.rs @@ -7,8 +7,8 @@ fn main() -> Result<(), Box> { let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(stream_handle.mixer()); - let file = std::fs::File::open("assets/RL.ogg")?; - sink.append(rodio::Decoder::try_from(file)?.amplify(0.2)); + let path = std::path::Path::new("assets/RL.ogg"); + sink.append(rodio::Decoder::try_from(path)?.amplify(0.2)); sink.sleep_until_end(); diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 948b3ec0..8e3c3743 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -5,13 +5,13 @@ //! //! # Usage //! -//! The simplest way to decode files (automatically sets up seeking and duration): +//! The simplest way to decode files (automatically sets up seeking, duration and format hint): //! ```no_run -//! use std::fs::File; +//! use std::path::Path; //! use rodio::Decoder; //! -//! let file = File::open("audio.mp3").unwrap(); -//! let decoder = Decoder::try_from(file).unwrap(); // Automatically sets byte_len from metadata +//! let path = Path::new("audio.mp3"); +//! let decoder = Decoder::try_from(path).unwrap(); // Automatically sets byte_len from metadata //! ``` //! //! For more control over decoder settings, use the builder pattern: @@ -322,8 +322,8 @@ impl DecoderImpl { /// use std::fs::File; /// use rodio::Decoder; /// -/// let file = File::open("music.mp3").unwrap(); -/// let decoder = Decoder::try_from(file).unwrap(); +/// let path = std::path::Path::new("music.mp3"); +/// let decoder = Decoder::try_from(path).unwrap(); /// ``` impl TryFrom for Decoder> { type Error = DecoderError; diff --git a/src/lib.rs b/src/lib.rs index a9a9bcd9..f13bd04c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,9 +23,9 @@ //! .expect("open default audio stream"); //! let sink = rodio::Sink::connect_new(&stream_handle.mixer()); //! // Load a sound from a file, using a path relative to Cargo.toml -//! let file = File::open("examples/music.ogg").unwrap(); +//! let path = std::path::Path::new("examples/music.ogg"); //! // Decode that sound file into a source -//! let source = Decoder::try_from(file).unwrap(); +//! let source = Decoder::try_from(path).unwrap(); //! // Play the sound directly on the device //! stream_handle.mixer().add(source); //! diff --git a/src/source/speed.rs b/src/source/speed.rs index 43c3a601..7dd704b4 100644 --- a/src/source/speed.rs +++ b/src/source/speed.rs @@ -21,9 +21,9 @@ //! let stream_handle = rodio::OutputStreamBuilder::open_default_stream() //! .expect("open default audio stream"); //! // Load a sound from a file, using a path relative to `Cargo.toml` -//! let file = File::open("examples/music.ogg").unwrap(); +//! let path = std::path::Path::new("examples/music.ogg"); //! // Decode that sound file into a source -//! let source = Decoder::try_from(file).unwrap(); +//! let source = Decoder::try_from(path).unwrap(); //! // Play the sound directly on the device 2x faster //! stream_handle.mixer().add(source.speed(2.0)); //! std::thread::sleep(std::time::Duration::from_secs(5)); diff --git a/src/wav_output.rs b/src/wav_output.rs index ddf40f7b..c351b340 100644 --- a/src/wav_output.rs +++ b/src/wav_output.rs @@ -46,7 +46,7 @@ pub fn wav_to_file( /// # Example /// ```rust /// # use rodio::static_buffer::StaticSamplesBuffer; -/// # use rodio::collect_to_wav; +/// # use rodio::wav_to_writer; /// # const SAMPLES: [rodio::Sample; 5] = [0.0, 1.0, 2.0, 3.0, 4.0]; /// # let source = StaticSamplesBuffer::new( /// # 1.try_into().unwrap(), @@ -130,7 +130,7 @@ impl> Iterator for WholeFrames { #[cfg(test)] mod test { use super::wav_to_file; - use crate::Source; + use crate::{Decoder, Source}; use std::io::BufReader; use std::time::Duration; @@ -144,15 +144,13 @@ mod test { let wav_file_path = "target/tmp/save-to-wav-test.wav"; wav_to_file(&mut make_source(), wav_file_path).expect("output file can be written"); - let file = std::fs::File::open(wav_file_path).expect("output file can be opened"); - // Not using crate::Decoder bcause it is limited to i16 samples. - let mut reader = - hound::WavReader::new(BufReader::new(file)).expect("wav file can be read back"); + let path = std::path::Path::new(wav_file_path); + let decoder = Decoder::try_from(path).expect("wav file can be read back"); let reference = make_source(); - assert_eq!(reference.sample_rate().get(), reader.spec().sample_rate); - assert_eq!(reference.channels().get(), reader.spec().channels); + assert_eq!(reference.sample_rate(), decoder.sample_rate()); + assert_eq!(reference.channels(), decoder.channels()); - let actual_samples: Vec = reader.samples::().map(|x| x.unwrap()).collect(); + let actual_samples: Vec = decoder.collect(); let expected_samples: Vec = reference.collect(); assert!( expected_samples == actual_samples, diff --git a/tests/flac_test.rs b/tests/flac_test.rs index e17602a6..00ff9eb0 100644 --- a/tests/flac_test.rs +++ b/tests/flac_test.rs @@ -7,8 +7,8 @@ use std::time::Duration; #[test] fn test_flac_encodings() { // 16 bit FLAC file exported from Audacity (2 channels, compression level 5) - let file = std::fs::File::open("assets/audacity16bit_level5.flac").unwrap(); - let mut decoder = rodio::Decoder::try_from(file).unwrap(); + let path = std::path::Path::new("assets/audacity16bit_level5.flac"); + let mut decoder = rodio::Decoder::try_from(path).unwrap(); // File is not just silence assert!(decoder.any(|x| x != 0.0)); @@ -16,8 +16,9 @@ fn test_flac_encodings() { // 24 bit FLAC file exported from Audacity (2 channels, various compression levels) for level in &[0, 5, 8] { - let file = std::fs::File::open(format!("assets/audacity24bit_level{level}.flac")).unwrap(); - let mut decoder = rodio::Decoder::try_from(file).unwrap(); + let path_str = format!("assets/audacity24bit_level{level}.flac"); + let path = std::path::Path::new(&path_str); + let mut decoder = rodio::Decoder::try_from(path).unwrap(); assert!(!decoder.all(|x| x != 0.0)); assert_eq!(decoder.total_duration(), Some(Duration::from_secs(3))); } diff --git a/tests/mp3_test.rs b/tests/mp3_test.rs index 2c9fba94..2a1f413f 100644 --- a/tests/mp3_test.rs +++ b/tests/mp3_test.rs @@ -1,8 +1,8 @@ #[cfg(any(feature = "minimp3", feature = "symphonia-mp3"))] #[test] fn test_silent_mp3() { - let file = std::fs::File::open("assets/silence.mp3").unwrap(); - let mut decoder = rodio::Decoder::try_from(file).unwrap(); + let path = std::path::Path::new("assets/silence.mp3"); + let mut decoder = rodio::Decoder::try_from(path).unwrap(); // File is just silence assert!(decoder.all(|x| x < 0.0001)); diff --git a/tests/seek.rs b/tests/seek.rs index 3663c5fa..00d52908 100644 --- a/tests/seek.rs +++ b/tests/seek.rs @@ -342,12 +342,10 @@ fn time_remaining(decoder: Decoder) -> Duration { fn get_music(format: &str) -> Decoder { let asset = Path::new("assets/music").with_extension(format); - let file = std::fs::File::open(asset).unwrap(); - Decoder::try_from(file).unwrap() + Decoder::try_from(asset).unwrap() } fn get_rl(format: &str) -> Decoder { let asset = Path::new("assets/RL").with_extension(format); - let file = std::fs::File::open(asset).unwrap(); - Decoder::try_from(file).unwrap() + Decoder::try_from(asset).unwrap() } diff --git a/tests/source_traits.rs b/tests/source_traits.rs index 478fe6cb..a38c9509 100644 --- a/tests/source_traits.rs +++ b/tests/source_traits.rs @@ -329,8 +329,9 @@ fn decoder_returns_hi_res_bit_depths() { ]; for (asset, bit_depth) in CASES { - let file = std::fs::File::open(format!("assets/{asset}")).unwrap(); - if let Ok(decoder) = rodio::Decoder::try_from(file) { + let path_str = format!("assets/{asset}"); + let path = std::path::Path::new(&path_str); + if let Ok(decoder) = rodio::Decoder::try_from(path) { // TODO: Symphonia returns None for audacity32bit.wav (float) if let Some(returned_bit_depth) = decoder.bits_per_sample() { assert_eq!( diff --git a/tests/wav_test.rs b/tests/wav_test.rs index 585eadda..a23cde4d 100644 --- a/tests/wav_test.rs +++ b/tests/wav_test.rs @@ -2,32 +2,32 @@ #[test] fn test_wav_encodings() { // 16 bit wav file exported from Audacity (1 channel) - let file = std::fs::File::open("assets/audacity16bit.wav").unwrap(); - let mut decoder = rodio::Decoder::try_from(file).unwrap(); + let path = std::path::Path::new("assets/audacity16bit.wav"); + let mut decoder = rodio::Decoder::try_from(path).unwrap(); assert!(decoder.any(|x| x != 0.0)); // 16 bit wav file exported from LMMS (2 channels) - let file = std::fs::File::open("assets/lmms16bit.wav").unwrap(); - let mut decoder = rodio::Decoder::try_from(file).unwrap(); + let path = std::path::Path::new("assets/lmms16bit.wav"); + let mut decoder = rodio::Decoder::try_from(path).unwrap(); assert!(decoder.any(|x| x != 0.0)); // 24 bit wav file exported from LMMS (2 channels) - let file = std::fs::File::open("assets/lmms24bit.wav").unwrap(); - let mut decoder = rodio::Decoder::try_from(file).unwrap(); + let path = std::path::Path::new("assets/lmms24bit.wav"); + let mut decoder = rodio::Decoder::try_from(path).unwrap(); assert!(decoder.any(|x| x != 0.0)); // 32 bit wav file exported from Audacity (1 channel) - let file = std::fs::File::open("assets/audacity32bit.wav").unwrap(); - let mut decoder = rodio::Decoder::try_from(file).unwrap(); + let path = std::path::Path::new("assets/audacity32bit.wav"); + let mut decoder = rodio::Decoder::try_from(path).unwrap(); assert!(decoder.any(|x| x != 0.0)); // 32 bit wav file exported from LMMS (2 channels) - let file = std::fs::File::open("assets/lmms32bit.wav").unwrap(); - let mut decoder = rodio::Decoder::try_from(file).unwrap(); + let path = std::path::Path::new("assets/lmms32bit.wav"); + let mut decoder = rodio::Decoder::try_from(path).unwrap(); assert!(decoder.any(|x| x != 0.0)); // 32 bit signed integer wav file exported from Audacity (1 channel). - let file = std::fs::File::open("assets/audacity32bit_int.wav").unwrap(); - let mut decoder = rodio::Decoder::try_from(file).unwrap(); + let path = std::path::Path::new("assets/audacity32bit_int.wav"); + let mut decoder = rodio::Decoder::try_from(path).unwrap(); assert!(decoder.any(|x| x != 0.0)); } From 4f5dd2791e2d51e9592c9bf1c26a756c2a93ea7c Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 13 Sep 2025 13:48:04 +0200 Subject: [PATCH 11/17] refactor: address review comments --- src/decoder/builder.rs | 4 +- src/decoder/flac.rs | 66 -------------------------------- src/decoder/mod.rs | 5 --- src/decoder/mp3.rs | 53 ++------------------------ src/decoder/read_seek_source.rs | 6 --- src/decoder/symphonia.rs | 67 +++++++++++++-------------------- src/decoder/vorbis.rs | 44 ---------------------- src/decoder/wav.rs | 52 ------------------------- 8 files changed, 32 insertions(+), 265 deletions(-) diff --git a/src/decoder/builder.rs b/src/decoder/builder.rs index 0ef8a205..679ecb7a 100644 --- a/src/decoder/builder.rs +++ b/src/decoder/builder.rs @@ -62,6 +62,7 @@ //! let file = File::open("audio.flac")?; //! let len = file.metadata()?.len(); //! +//! // High-quality decoder with precise seeking //! let decoder = Decoder::builder() //! .with_data(file) //! .with_byte_len(len) @@ -73,8 +74,7 @@ //! .with_gapless(false) //! .build()?; //! -//! // High-quality decoder with precise seeking -//! Ok(()) +//! Ok(()) //! } //! ``` //! diff --git a/src/decoder/flac.rs b/src/decoder/flac.rs index f049d55a..4f0052b7 100644 --- a/src/decoder/flac.rs +++ b/src/decoder/flac.rs @@ -107,11 +107,6 @@ const READER_OPTIONS: FlacReaderOptions = FlacReaderOptions { /// to minimize allocations during playback. Buffers are reused across blocks for /// optimal performance. /// -/// # Thread Safety -/// -/// This decoder is not thread-safe. Create separate instances for concurrent access -/// or use appropriate synchronization primitives. -/// /// # Generic Parameters /// /// * `R` - The underlying data source type, must implement `Read + Seek` @@ -526,20 +521,6 @@ where /// buffer of the current FLAC block. It returns samples one at a time while /// automatically decoding new blocks as needed. /// - /// # Sample Format Conversion - /// - /// Raw FLAC samples are converted to Rodio's sample format based on bit depth: - /// - 8-bit: Direct conversion from `i8` - /// - 16-bit: Direct conversion from `i16` - /// - 24-bit: Conversion using `I24` type - /// - 32-bit: Direct conversion from `i32` - /// - Other (12, 20-bit): Bit-shifted to 32-bit then converted - /// - /// # Performance - /// - /// - **Hot path**: Returning samples from current block (very fast) - /// - **Cold path**: Decoding new blocks when buffer is exhausted (slower) - /// /// # Returns /// /// - `Some(sample)` - Next audio sample @@ -610,12 +591,6 @@ where /// This information can be used by consumers for buffer pre-allocation /// and progress indication. /// - /// # Returns - /// - /// A tuple `(lower_bound, upper_bound)` where: - /// - `lower_bound`: Minimum number of samples guaranteed to be available - /// - `upper_bound`: Maximum number of samples that might be available (None if unknown) - /// /// # Accuracy /// /// - **With metadata**: Exact remaining sample count (lower == upper) @@ -646,32 +621,6 @@ where } /// Probes input data to detect FLAC format. -/// -/// This function attempts to parse the FLAC magic bytes and stream info header to determine if the -/// data contains a valid FLAC stream. The stream position is restored regardless of the result. -/// -/// # Arguments -/// -/// * `data` - Mutable reference to the input stream to probe -/// -/// # Returns -/// -/// - `true` if the data appears to contain a valid FLAC stream -/// - `false` if the data is not FLAC or is corrupted -/// -/// # Implementation -/// -/// Uses the common `utils::probe_format` helper which: -/// 1. Saves the current stream position -/// 2. Attempts FLAC detection using `claxon::FlacReader` -/// 3. Restores the original stream position -/// 4. Returns the detection result -/// -/// # Performance -/// -/// This function only reads the minimum amount of data needed to identify -/// the FLAC format (magic bytes and basic header), making it efficient for -/// format detection in multi-format scenarios. fn is_flac(data: &mut R) -> bool where R: Read + Seek, @@ -679,21 +628,6 @@ where utils::probe_format(data, |reader| FlacReader::new(reader).is_ok()) } -/// Converts claxon decoder errors to rodio seek errors. -/// -/// This implementation provides error context preservation when FLAC decoding operations fail -/// during seeking. The original `claxon` error is wrapped in an `Arc` for thread safety and -/// converted to the appropriate Rodio error type. -/// -/// # Error Mapping -/// -/// All `claxon::Error` variants are mapped to `SeekError::ClaxonDecoder` with the -/// original error preserved for debugging and error analysis. -/// -/// # Thread Safety -/// -/// The error is wrapped in `Arc` to allow sharing across thread boundaries if needed, -/// following Rodio's error handling patterns. impl From for SeekError { /// Converts a claxon error into a Rodio seek error. /// diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 8e3c3743..18c6db8b 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -111,14 +111,10 @@ pub struct LoopedDecoder { inner: Option>, /// Configuration settings for the decoder. settings: Settings, - /// Cached metadata from the first successful decoder creation. /// Used to avoid expensive file scanning on subsequent loops. cached_duration: Option, } -// Cannot really reduce the size of the VorbisDecoder. There are not any -/// Internal enum representing different decoder implementations. -/// /// This enum dispatches to the appropriate decoder based on detected format /// and available features. Large enum variant size is acceptable here since /// these are infrequently created and moved. @@ -145,7 +141,6 @@ enum DecoderImpl { None(Unreachable, PhantomData), } -/// Placeholder type for the None variant that can never be instantiated. enum Unreachable {} impl DecoderImpl { diff --git a/src/decoder/mp3.rs b/src/decoder/mp3.rs index 6aa0350e..c26bce08 100644 --- a/src/decoder/mp3.rs +++ b/src/decoder/mp3.rs @@ -98,11 +98,6 @@ use crate::{ /// MP3 frames can theoretically change channel configuration, though this is /// rare in practice. The decoder handles such changes dynamically. /// -/// # Thread Safety -/// -/// This decoder is not thread-safe. Create separate instances for concurrent access -/// or use appropriate synchronization primitives. -/// /// # Generic Parameters /// /// * `R` - The underlying data source type, must implement `Read + Seek` @@ -110,10 +105,8 @@ pub struct Mp3Decoder where R: Read + Seek, { - /// The underlying minimp3 decoder, wrapped in Option for seeking operations. - /// - /// Temporarily set to `None` during stream reset operations for seeking. - /// Always `Some` during normal operation and iteration. + /// The underlying minimp3 decoder, wrapped in Option to allow owning the + /// Decoder instance during seeking. decoder: Option>, /// Byte position where audio data begins (after headers/metadata). @@ -142,8 +135,7 @@ where /// Sample rate in Hz. /// - /// Fixed for the entire MP3 stream. Common rates include 44.1kHz (CD quality), - /// 48kHz (professional), and various rates for different MPEG versions. + /// Does not change after decoder initialization. sample_rate: SampleRate, /// Number of samples read so far (for seeking calculations). @@ -740,12 +732,6 @@ where /// Provides size estimates based on MP3 metadata and current playback position. /// The accuracy depends on the availability and reliability of duration information. /// - /// # Returns - /// - /// A tuple `(lower_bound, upper_bound)` where: - /// - `lower_bound`: Minimum number of samples guaranteed to be available - /// - `upper_bound`: Maximum number of samples that might be available (None if unknown) - /// /// # Accuracy Levels /// /// - **High accuracy**: When total samples calculated from scanned duration @@ -841,39 +827,6 @@ fn get_mp3_duration(data: &mut R) -> Option { } /// Probes input data to detect MP3 format. -/// -/// This function attempts to decode the first MP3 frame to determine if the -/// data contains a valid MP3 stream. The stream position is restored regardless -/// of the result, making it safe to use for format detection. -/// -/// # Arguments -/// -/// * `data` - Mutable reference to the input stream to probe -/// -/// # Returns -/// -/// - `true` if the data appears to contain a valid MP3 stream -/// - `false` if the data is not MP3 or is corrupted -/// -/// # Implementation -/// -/// Uses the common `utils::probe_format` helper which: -/// 1. Saves the current stream position -/// 2. Attempts MP3 detection using `minimp3::Decoder` -/// 3. Restores the original stream position -/// 4. Returns the detection result -/// -/// # Performance -/// -/// This function only reads the minimum amount of data needed to identify -/// and decode the first MP3 frame, making it efficient for format detection -/// in multi-format scenarios. -/// -/// # Robustness -/// -/// The detection uses actual frame decoding rather than just header checking, -/// providing more reliable format identification at the cost of slightly -/// higher computational overhead. fn is_mp3(data: &mut R) -> bool where R: Read + Seek, diff --git a/src/decoder/read_seek_source.rs b/src/decoder/read_seek_source.rs index 004fdceb..ccb68178 100644 --- a/src/decoder/read_seek_source.rs +++ b/src/decoder/read_seek_source.rs @@ -52,12 +52,6 @@ use crate::decoder::builder::Settings; /// - **Byte length**: Total stream size for seeking and progress calculations /// - **Configuration**: Stream properties from decoder builder settings /// -/// # Thread Safety -/// -/// This wrapper requires `Send + Sync` bounds on the wrapped type to ensure -/// thread safety for Symphonia's internal operations. Most standard I/O types -/// satisfy these requirements. -/// /// # Generic Parameters /// /// * `T` - The wrapped I/O type, must implement `Read + Seek + Send + Sync` diff --git a/src/decoder/symphonia.rs b/src/decoder/symphonia.rs index e111fbd8..91306d99 100644 --- a/src/decoder/symphonia.rs +++ b/src/decoder/symphonia.rs @@ -135,11 +135,6 @@ use crate::{decoder::builder::Settings, BitDepth}; /// - **Reset required**: Recreate decoder and continue with next track /// - **I/O errors**: Attempt to continue when possible /// - **Terminal errors**: Clean shutdown when recovery is impossible -/// -/// # Thread Safety -/// -/// This decoder is not thread-safe. Create separate instances for concurrent access -/// or use appropriate synchronization primitives. pub struct SymphoniaDecoder { /// The underlying Symphonia audio decoder. /// @@ -1054,12 +1049,6 @@ impl Iterator for SymphoniaDecoder { /// /// For multi-track files, estimates represent the currently selected audio /// track, not the entire file duration or all tracks combined. - /// - /// # Returns - /// - /// A tuple `(lower_bound, upper_bound)` where: - /// - `lower_bound`: Minimum number of samples guaranteed to be available - /// - `upper_bound`: Maximum number of samples that might be available (None if unknown) fn size_hint(&self) -> (usize, Option) { // Samples already decoded and buffered (guaranteed available) let buffered_samples = self @@ -1189,36 +1178,34 @@ fn should_continue_on_decode_error( } } +/// Converts Rodio's SeekMode to Symphonia's SeekMode. +/// +/// This conversion maps Rodio's seeking preferences to Symphonia's +/// internal seeking modes, enabling consistent seeking behavior +/// across different audio processing layers. +/// +/// # Mapping +/// +/// - `SeekMode::Fastest` → `SymphoniaSeekMode::Coarse` +/// - Prioritizes speed over precision +/// - Uses keyframe-based seeking when available +/// - Suitable for user scrubbing and fast navigation +/// - `SeekMode::Nearest` → `SymphoniaSeekMode::Accurate` +/// - Prioritizes precision over speed +/// - Attempts sample-accurate positioning +/// - Suitable for gapless playback and precise positioning +/// +/// # Performance Implications +/// +/// The choice between modes affects performance significantly: +/// - **Coarse**: Fast seeks but may require fine-tuning +/// - **Accurate**: Slower seeks but precise positioning +/// +/// # Format Compatibility +/// +/// Not all formats support both modes equally. Automatic +/// fallbacks may occur when preferred mode unavailable. impl From for SymphoniaSeekMode { - /// Converts Rodio's SeekMode to Symphonia's SeekMode. - /// - /// This conversion maps Rodio's seeking preferences to Symphonia's - /// internal seeking modes, enabling consistent seeking behavior - /// across different audio processing layers. - /// - /// # Mapping - /// - /// - `SeekMode::Fastest` → `SymphoniaSeekMode::Coarse` - /// - Prioritizes speed over precision - /// - Uses keyframe-based seeking when available - /// - Suitable for user scrubbing and fast navigation - /// - `SeekMode::Nearest` → `SymphoniaSeekMode::Accurate` - /// - Prioritizes precision over speed - /// - Attempts sample-accurate positioning - /// - Suitable for gapless playback and precise positioning - /// - /// # Performance Implications - /// - /// The choice between modes affects performance significantly: - /// - **Coarse**: Fast seeks but may require fine-tuning - /// - **Accurate**: Slower seeks but precise positioning - /// - /// # Format Compatibility - /// - /// Not all formats support both modes equally: - /// - Some formats only implement one mode effectively - /// - Automatic fallbacks may occur when preferred mode unavailable - /// - Mode availability may depend on stream characteristics fn from(mode: SeekMode) -> Self { match mode { SeekMode::Fastest => SymphoniaSeekMode::Coarse, diff --git a/src/decoder/vorbis.rs b/src/decoder/vorbis.rs index 2ab4b2d0..5cee2708 100644 --- a/src/decoder/vorbis.rs +++ b/src/decoder/vorbis.rs @@ -105,11 +105,6 @@ use crate::{ /// Ogg supports chained streams where multiple Vorbis streams are concatenated. /// The decoder adapts to parameter changes (sample rate, channels) between streams. /// -/// # Thread Safety -/// -/// This decoder is not thread-safe. Create separate instances for concurrent access -/// or use appropriate synchronization primitives. -/// /// # Generic Parameters /// /// * `R` - The underlying data source type, must implement `Read + Seek` @@ -725,12 +720,6 @@ where /// playback position. The accuracy depends on the availability of duration information /// from granule position scanning or explicit duration settings. /// - /// # Returns - /// - /// A tuple `(lower_bound, upper_bound)` where: - /// - `lower_bound`: Minimum number of samples guaranteed to be available - /// - `upper_bound`: Maximum number of samples that might be available (None if unknown) - /// /// # Accuracy Levels /// /// - **High accuracy**: When total samples calculated from granule scanning @@ -995,39 +984,6 @@ fn granules_to_duration(granules: u64, sample_rate: SampleRate) -> Duration { } /// Probes input data to detect Ogg Vorbis format. -/// -/// This function attempts to initialize a lewton OggStreamReader to determine if the -/// data contains a valid Ogg Vorbis stream. The stream position is restored regardless -/// of the result, making it safe to use for format detection. -/// -/// # Arguments -/// -/// * `data` - Mutable reference to the input stream to probe -/// -/// # Returns -/// -/// - `true` if the data appears to contain a valid Ogg Vorbis stream -/// - `false` if the data is not Ogg Vorbis or is corrupted -/// -/// # Implementation -/// -/// Uses the common `utils::probe_format` helper which: -/// 1. Saves the current stream position -/// 2. Attempts Ogg Vorbis detection using `lewton::OggStreamReader` -/// 3. Restores the original stream position -/// 4. Returns the detection result -/// -/// # Performance -/// -/// This function reads the minimum amount of data needed to identify the Ogg -/// container format and Vorbis codec headers, making it efficient for format -/// detection in multi-format scenarios. -/// -/// # Robustness -/// -/// The detection uses actual stream reader initialization rather than just header -/// checking, providing reliable format identification with proper Ogg/Vorbis -/// validation at the cost of slightly higher computational overhead. fn is_vorbis(data: &mut R) -> bool where R: Read + Seek, diff --git a/src/decoder/wav.rs b/src/decoder/wav.rs index 95e433b9..52a45dc3 100644 --- a/src/decoder/wav.rs +++ b/src/decoder/wav.rs @@ -116,11 +116,6 @@ use crate::{ /// - **Channel preservation**: Maintains correct channel order across seeks /// - **Sample accuracy**: Precise positioning without approximation /// -/// # Thread Safety -/// -/// This decoder is not thread-safe. Create separate instances for concurrent access -/// or use appropriate synchronization primitives. -/// /// # Generic Parameters /// /// * `R` - The underlying data source type, must implement `Read + Seek` @@ -459,12 +454,6 @@ where /// header information and current position. This enables accurate /// buffer pre-allocation and progress indication. /// - /// # Returns - /// - /// A tuple `(lower_bound, upper_bound)` where: - /// - `lower_bound`: Always 0 (no samples guaranteed without I/O) - /// - `upper_bound`: Exact remaining sample count from WAV header - /// /// # Accuracy /// /// WAV files provide exact sample counts in their headers, making the @@ -674,12 +663,6 @@ where /// /// Delegates to the internal `SamplesIterator` which provides exact /// remaining sample count based on WAV header information. - /// - /// # Returns - /// - /// A tuple `(lower_bound, upper_bound)` where: - /// - `lower_bound`: Always 0 (no samples guaranteed without I/O) - /// - `upper_bound`: Exact remaining sample count from WAV header #[inline] fn size_hint(&self) -> (usize, Option) { self.reader.size_hint() @@ -687,41 +670,6 @@ where } /// Probes input data to detect WAV format. -/// -/// This function attempts to parse the RIFF/WAVE headers to determine if the -/// data contains a valid WAV file. The stream position is restored regardless -/// of the result, making it safe to use for format detection. -/// -/// # Arguments -/// -/// * `data` - Mutable reference to the input stream to probe -/// -/// # Returns -/// -/// - `true` if the data appears to contain a valid WAV file -/// - `false` if the data is not WAV or has invalid headers -/// -/// # Implementation -/// -/// Uses the common `utils::probe_format` helper which: -/// 1. Saves the current stream position -/// 2. Attempts WAV detection using `hound::WavReader` -/// 3. Restores the original stream position -/// 4. Returns the detection result -/// -/// # Performance -/// -/// This function only reads the minimal amount of data needed to identify -/// the RIFF/WAVE headers, making it very efficient for format detection -/// in multi-format scenarios. -/// -/// # Robustness -/// -/// The detection uses hound's robust RIFF/WAVE parsing which validates: -/// - RIFF container format compliance -/// - WAVE format identification -/// - fmt chunk presence and validity -/// - Basic structural integrity fn is_wave(data: &mut R) -> bool where R: Read + Seek, From e01f1a24686cac1c78c1df42686ee05cd9f1e4c5 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 13 Sep 2025 14:15:33 +0200 Subject: [PATCH 12/17] refactor: move LoopedDecoder to its own module and refactor construction --- src/decoder/builder.rs | 6 +- src/decoder/looped.rs | 264 +++++++++++++++++++++++++++++++++++++++++ src/decoder/mod.rs | 230 +---------------------------------- 3 files changed, 267 insertions(+), 233 deletions(-) create mode 100644 src/decoder/looped.rs diff --git a/src/decoder/builder.rs b/src/decoder/builder.rs index 679ecb7a..a6eee804 100644 --- a/src/decoder/builder.rs +++ b/src/decoder/builder.rs @@ -845,10 +845,6 @@ impl DecoderBuilder { /// on each loop iteration, improving performance for repeated playback. pub fn build_looped(self) -> Result, DecoderError> { let (decoder, settings) = self.build_impl()?; - Ok(LoopedDecoder { - inner: Some(decoder), - settings, - cached_duration: None, - }) + Ok(LoopedDecoder::new(decoder, settings)) } } diff --git a/src/decoder/looped.rs b/src/decoder/looped.rs new file mode 100644 index 00000000..e463e585 --- /dev/null +++ b/src/decoder/looped.rs @@ -0,0 +1,264 @@ +use std::{ + io::{Read, Seek}, + marker::PhantomData, + sync::Arc, + time::Duration, +}; + +use crate::{ + common::{ChannelCount, SampleRate}, + math::nz, + source::{SeekError, Source}, + BitDepth, Sample, +}; + +use super::{builder::Settings, DecoderError, DecoderImpl}; + +#[cfg(feature = "claxon")] +use super::flac; +#[cfg(feature = "minimp3")] +use super::mp3; +#[cfg(feature = "symphonia")] +use super::symphonia; +#[cfg(feature = "lewton")] +use super::vorbis; +#[cfg(feature = "hound")] +use super::wav; + +/// Source of audio samples from decoding a file that never ends. +/// When the end of the file is reached, the decoder starts again from the beginning. +/// +/// A `LoopedDecoder` will attempt to seek back to the start of the stream when it reaches +/// the end. If seeking fails for any reason (like IO errors), iteration will stop. +/// +/// For seekable sources with gapless playback enabled, this uses `try_seek(Duration::ZERO)` +/// which is fast. For non-seekable sources or when gapless is disabled, it recreates the +/// decoder but caches metadata from the first iteration to avoid expensive file scanning +/// on subsequent loops. +/// +/// # Examples +/// +/// ```no_run +/// use std::fs::File; +/// use rodio::Decoder; +/// +/// let file = File::open("audio.mp3").unwrap(); +/// let looped_decoder = Decoder::new_looped(file).unwrap(); +/// ``` +#[allow(dead_code)] +pub struct LoopedDecoder { + /// The underlying decoder implementation. + pub(super) inner: Option>, + /// Configuration settings for the decoder. + pub(super) settings: Settings, + /// Used to avoid expensive file scanning on subsequent loops. + cached_duration: Option, +} + +impl LoopedDecoder +where + R: Read + Seek, +{ + /// Create a new `LoopedDecoder` with the given decoder and settings. + pub(super) fn new(decoder: DecoderImpl, settings: Settings) -> Self { + Self { + inner: Some(decoder), + settings, + cached_duration: None, + } + } + + /// Recreate decoder with cached metadata to avoid expensive file scanning. + fn recreate_decoder_with_cache( + &mut self, + decoder: DecoderImpl, + ) -> Option<(DecoderImpl, Option)> { + // Create settings with cached metadata for fast recreation. + // Note: total_duration is important even though LoopedDecoder::total_duration() returns + // None, because the individual decoder's total_duration() is used for seek saturation + // (clamping seeks beyond the end to the end position). + let mut fast_settings = self.settings.clone(); + fast_settings.total_duration = self.cached_duration; + + let (new_decoder, sample) = match decoder { + #[cfg(feature = "hound")] + DecoderImpl::Wav(source) => { + let mut reader = source.into_inner(); + reader.rewind().ok()?; + let mut source = wav::WavDecoder::new_with_settings(reader, &fast_settings).ok()?; + let sample = source.next(); + (DecoderImpl::Wav(source), sample) + } + #[cfg(feature = "lewton")] + DecoderImpl::Vorbis(source) => { + let mut reader = source.into_inner().into_inner().into_inner(); + reader.rewind().ok()?; + let mut source = + vorbis::VorbisDecoder::new_with_settings(reader, &fast_settings).ok()?; + let sample = source.next(); + (DecoderImpl::Vorbis(source), sample) + } + #[cfg(feature = "claxon")] + DecoderImpl::Flac(source) => { + let mut reader = source.into_inner(); + reader.rewind().ok()?; + let mut source = + flac::FlacDecoder::new_with_settings(reader, &fast_settings).ok()?; + let sample = source.next(); + (DecoderImpl::Flac(source), sample) + } + #[cfg(feature = "minimp3")] + DecoderImpl::Mp3(source) => { + let mut reader = source.into_inner(); + reader.rewind().ok()?; + let mut source = mp3::Mp3Decoder::new_with_settings(reader, &fast_settings).ok()?; + let sample = source.next(); + (DecoderImpl::Mp3(source), sample) + } + #[cfg(feature = "symphonia")] + DecoderImpl::Symphonia(source, PhantomData) => { + let mut reader = source.into_inner(); + reader.rewind().ok()?; + let mut source = + symphonia::SymphoniaDecoder::new_with_settings(reader, &fast_settings).ok()?; + let sample = source.next(); + (DecoderImpl::Symphonia(source, PhantomData), sample) + } + DecoderImpl::None(_, _) => return None, + }; + Some((new_decoder, sample)) + } +} + +impl Iterator for LoopedDecoder +where + R: Read + Seek, +{ + type Item = Sample; + + /// Returns the next sample in the audio stream. + /// + /// When the end of the stream is reached, attempts to seek back to the start and continue + /// playing. For seekable sources with gapless playback, this uses fast seeking. For + /// non-seekable sources or when gapless is disabled, recreates the decoder using cached + /// metadata to avoid expensive file scanning. + fn next(&mut self) -> Option { + if let Some(inner) = &mut self.inner { + if let Some(sample) = inner.next() { + return Some(sample); + } + + // Cache duration from current decoder before resetting (first time only) + if self.cached_duration.is_none() { + self.cached_duration = inner.total_duration(); + } + + // Try seeking first for seekable sources - this is fast and gapless + // Only use fast seeking when gapless=true, otherwise recreate normally + if self.settings.gapless + && self.settings.is_seekable + && inner.try_seek(Duration::ZERO).is_ok() + { + return inner.next(); + } + + // Fall back to recreation with cached metadata to avoid expensive scanning + let decoder = self.inner.take()?; + let (new_decoder, sample) = self.recreate_decoder_with_cache(decoder)?; + self.inner = Some(new_decoder); + sample + } else { + None + } + } + + /// Returns the size hint for this iterator. + /// + /// The lower bound is: + /// - The minimum number of samples remaining in the current iteration if there is an active + /// decoder + /// - 0 if there is no active decoder (inner is None) + /// + /// The upper bound is always `None` since the decoder loops indefinitely. + /// + /// Note that even with an active decoder, reaching the end of the stream may result in the + /// decoder becoming inactive if seeking back to the start fails. + #[inline] + fn size_hint(&self) -> (usize, Option) { + ( + self.inner.as_ref().map_or(0, |inner| inner.size_hint().0), + None, + ) + } +} + +impl Source for LoopedDecoder +where + R: Read + Seek, +{ + /// Returns the current span length of the underlying decoder. + /// + /// Returns `None` if there is no active decoder. + #[inline] + fn current_span_len(&self) -> Option { + self.inner.as_ref()?.current_span_len() + } + + /// Returns the number of channels in the audio stream. + /// + /// Returns the default channel count if there is no active decoder. + #[inline] + fn channels(&self) -> ChannelCount { + self.inner.as_ref().map_or(nz!(1), |inner| inner.channels()) + } + + /// Returns the sample rate of the audio stream. + /// + /// Returns the default sample rate if there is no active decoder. + #[inline] + fn sample_rate(&self) -> SampleRate { + self.inner + .as_ref() + .map_or(nz!(44100), |inner| inner.sample_rate()) + } + + /// Returns the total duration of this audio source. + /// + /// Always returns `None` for looped decoders since they have no fixed end point - + /// they will continue playing indefinitely by seeking back to the start when reaching + /// the end of the audio data. + #[inline] + fn total_duration(&self) -> Option { + None + } + + /// Returns the bits per sample of the underlying decoder, if available. + #[inline] + fn bits_per_sample(&self) -> Option { + self.inner.as_ref()?.bits_per_sample() + } + + /// Attempts to seek to a specific position in the audio stream. + /// + /// # Errors + /// + /// Returns `SeekError::NotSupported` if: + /// - There is no active decoder + /// - The underlying decoder does not support seeking + /// + /// May also return other `SeekError` variants if the underlying decoder's seek operation fails. + /// + /// # Note + /// + /// Even for looped playback, seeking past the end of the stream will not automatically + /// wrap around to the beginning - it will return an error just like a normal decoder. + /// Looping only occurs when reaching the end through normal playback. + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + match &mut self.inner { + Some(inner) => inner.try_seek(pos), + None => Err(SeekError::Other(Arc::new(DecoderError::IoError( + "Looped source ended when it failed to loop back".to_string(), + )))), + } + } +} \ No newline at end of file diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 18c6db8b..dccc60c7 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -48,7 +48,6 @@ use std::{ io::{BufReader, Read, Seek}, marker::PhantomData, - sync::Arc, time::Duration, }; @@ -57,16 +56,16 @@ use std::io::SeekFrom; use crate::{ common::{assert_error_traits, ChannelCount, SampleRate}, - math::nz, source::{SeekError, Source}, BitDepth, Sample, }; pub mod builder; pub use builder::DecoderBuilder; -use builder::Settings; mod utils; +mod looped; +pub use looped::LoopedDecoder; #[cfg(feature = "claxon")] mod flac; @@ -85,35 +84,6 @@ mod wav; /// See the [module-level documentation](self) for examples and usage. pub struct Decoder(DecoderImpl); -/// Source of audio samples from decoding a file that never ends. -/// When the end of the file is reached, the decoder starts again from the beginning. -/// -/// A `LoopedDecoder` will attempt to seek back to the start of the stream when it reaches -/// the end. If seeking fails for any reason (like IO errors), iteration will stop. -/// -/// For seekable sources with gapless playback enabled, this uses `try_seek(Duration::ZERO)` -/// which is fast. For non-seekable sources or when gapless is disabled, it recreates the -/// decoder but caches metadata from the first iteration to avoid expensive file scanning -/// on subsequent loops. -/// -/// # Examples -/// -/// ```no_run -/// use std::fs::File; -/// use rodio::Decoder; -/// -/// let file = File::open("audio.mp3").unwrap(); -/// let looped_decoder = Decoder::new_looped(file).unwrap(); -/// ``` -#[allow(dead_code)] -pub struct LoopedDecoder { - /// The underlying decoder implementation. - inner: Option>, - /// Configuration settings for the decoder. - settings: Settings, - /// Used to avoid expensive file scanning on subsequent loops. - cached_duration: Option, -} /// This enum dispatches to the appropriate decoder based on detected format /// and available features. Large enum variant size is acceptable here since @@ -956,204 +926,8 @@ where } } -impl LoopedDecoder -where - R: Read + Seek, -{ - /// Recreate decoder with cached metadata to avoid expensive file scanning. - fn recreate_decoder_with_cache( - &mut self, - decoder: DecoderImpl, - ) -> Option<(DecoderImpl, Option)> { - // Create settings with cached metadata for fast recreation. - // Note: total_duration is important even though LoopedDecoder::total_duration() returns - // None, because the individual decoder's total_duration() is used for seek saturation - // (clamping seeks beyond the end to the end position). - let mut fast_settings = self.settings.clone(); - fast_settings.total_duration = self.cached_duration; - - let (new_decoder, sample) = match decoder { - #[cfg(feature = "hound")] - DecoderImpl::Wav(source) => { - let mut reader = source.into_inner(); - reader.rewind().ok()?; - let mut source = wav::WavDecoder::new_with_settings(reader, &fast_settings).ok()?; - let sample = source.next(); - (DecoderImpl::Wav(source), sample) - } - #[cfg(feature = "lewton")] - DecoderImpl::Vorbis(source) => { - let mut reader = source.into_inner().into_inner().into_inner(); - reader.rewind().ok()?; - let mut source = - vorbis::VorbisDecoder::new_with_settings(reader, &fast_settings).ok()?; - let sample = source.next(); - (DecoderImpl::Vorbis(source), sample) - } - #[cfg(feature = "claxon")] - DecoderImpl::Flac(source) => { - let mut reader = source.into_inner(); - reader.rewind().ok()?; - let mut source = - flac::FlacDecoder::new_with_settings(reader, &fast_settings).ok()?; - let sample = source.next(); - (DecoderImpl::Flac(source), sample) - } - #[cfg(feature = "minimp3")] - DecoderImpl::Mp3(source) => { - let mut reader = source.into_inner(); - reader.rewind().ok()?; - let mut source = mp3::Mp3Decoder::new_with_settings(reader, &fast_settings).ok()?; - let sample = source.next(); - (DecoderImpl::Mp3(source), sample) - } - #[cfg(feature = "symphonia")] - DecoderImpl::Symphonia(source, PhantomData) => { - let mut reader = source.into_inner(); - reader.rewind().ok()?; - let mut source = - symphonia::SymphoniaDecoder::new_with_settings(reader, &fast_settings).ok()?; - let sample = source.next(); - (DecoderImpl::Symphonia(source, PhantomData), sample) - } - DecoderImpl::None(_, _) => return None, - }; - Some((new_decoder, sample)) - } -} - -impl Iterator for LoopedDecoder -where - R: Read + Seek, -{ - type Item = Sample; - /// Returns the next sample in the audio stream. - /// - /// When the end of the stream is reached, attempts to seek back to the start and continue - /// playing. For seekable sources with gapless playback, this uses fast seeking. For - /// non-seekable sources or when gapless is disabled, recreates the decoder using cached - /// metadata to avoid expensive file scanning. - fn next(&mut self) -> Option { - if let Some(inner) = &mut self.inner { - if let Some(sample) = inner.next() { - return Some(sample); - } - - // Cache duration from current decoder before resetting (first time only) - if self.cached_duration.is_none() { - self.cached_duration = inner.total_duration(); - } - - // Try seeking first for seekable sources - this is fast and gapless - // Only use fast seeking when gapless=true, otherwise recreate normally - if self.settings.gapless - && self.settings.is_seekable - && inner.try_seek(Duration::ZERO).is_ok() - { - return inner.next(); - } - - // Fall back to recreation with cached metadata to avoid expensive scanning - let decoder = self.inner.take()?; - let (new_decoder, sample) = self.recreate_decoder_with_cache(decoder)?; - self.inner = Some(new_decoder); - sample - } else { - None - } - } - - /// Returns the size hint for this iterator. - /// - /// The lower bound is: - /// - The minimum number of samples remaining in the current iteration if there is an active - /// decoder - /// - 0 if there is no active decoder (inner is None) - /// - /// The upper bound is always `None` since the decoder loops indefinitely. - /// - /// Note that even with an active decoder, reaching the end of the stream may result in the - /// decoder becoming inactive if seeking back to the start fails. - #[inline] - fn size_hint(&self) -> (usize, Option) { - ( - self.inner.as_ref().map_or(0, |inner| inner.size_hint().0), - None, - ) - } -} -impl Source for LoopedDecoder -where - R: Read + Seek, -{ - /// Returns the current span length of the underlying decoder. - /// - /// Returns `None` if there is no active decoder. - #[inline] - fn current_span_len(&self) -> Option { - self.inner.as_ref()?.current_span_len() - } - - /// Returns the number of channels in the audio stream. - /// - /// Returns the default channel count if there is no active decoder. - #[inline] - fn channels(&self) -> ChannelCount { - self.inner.as_ref().map_or(nz!(1), |inner| inner.channels()) - } - - /// Returns the sample rate of the audio stream. - /// - /// Returns the default sample rate if there is no active decoder. - #[inline] - fn sample_rate(&self) -> SampleRate { - self.inner - .as_ref() - .map_or(nz!(44100), |inner| inner.sample_rate()) - } - - /// Returns the total duration of this audio source. - /// - /// Always returns `None` for looped decoders since they have no fixed end point - - /// they will continue playing indefinitely by seeking back to the start when reaching - /// the end of the audio data. - #[inline] - fn total_duration(&self) -> Option { - None - } - - /// Returns the bits per sample of the underlying decoder, if available. - #[inline] - fn bits_per_sample(&self) -> Option { - self.inner.as_ref()?.bits_per_sample() - } - - /// Attempts to seek to a specific position in the audio stream. - /// - /// # Errors - /// - /// Returns `SeekError::NotSupported` if: - /// - There is no active decoder - /// - The underlying decoder does not support seeking - /// - /// May also return other `SeekError` variants if the underlying decoder's seek operation fails. - /// - /// # Note - /// - /// Even for looped playback, seeking past the end of the stream will not automatically - /// wrap around to the beginning - it will return an error just like a normal decoder. - /// Looping only occurs when reaching the end through normal playback. - fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - match &mut self.inner { - Some(inner) => inner.try_seek(pos), - None => Err(SeekError::Other(Arc::new(DecoderError::IoError( - "Looped source ended when it failed to loop back".to_string(), - )))), - } - } -} /// Errors that can occur when creating a decoder. #[derive(Debug, thiserror::Error, Clone)] From 924fa290fa3ddfaab38b23fe13d1fc731737d3c7 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 13 Sep 2025 14:23:09 +0200 Subject: [PATCH 13/17] refactor: move ReadSeekSource into symphonia module --- src/decoder/builder.rs | 2 +- src/decoder/mod.rs | 2 - src/decoder/read_seek_source.rs | 250 -------------------------------- src/decoder/symphonia.rs | 205 +++++++++++++++++++++++++- 4 files changed, 205 insertions(+), 254 deletions(-) delete mode 100644 src/decoder/read_seek_source.rs diff --git a/src/decoder/builder.rs b/src/decoder/builder.rs index a6eee804..cd445cc2 100644 --- a/src/decoder/builder.rs +++ b/src/decoder/builder.rs @@ -106,7 +106,7 @@ use std::{ }; #[cfg(feature = "symphonia")] -use self::read_seek_source::ReadSeekSource; +use super::symphonia::ReadSeekSource; #[cfg(feature = "symphonia")] use ::symphonia::core::io::{MediaSource, MediaSourceStream}; diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index dccc60c7..c8802a57 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -72,8 +72,6 @@ mod flac; #[cfg(feature = "minimp3")] mod mp3; #[cfg(feature = "symphonia")] -mod read_seek_source; -#[cfg(feature = "symphonia")] mod symphonia; #[cfg(feature = "lewton")] mod vorbis; diff --git a/src/decoder/read_seek_source.rs b/src/decoder/read_seek_source.rs deleted file mode 100644 index ccb68178..00000000 --- a/src/decoder/read_seek_source.rs +++ /dev/null @@ -1,250 +0,0 @@ -//! Read + Seek adapter for Symphonia MediaSource integration. -//! -//! This module provides a bridge between standard Rust I/O types and Symphonia's -//! MediaSource trait, enabling seamless integration of file handles, cursors, and -//! other I/O sources with Symphonia's audio decoding framework. -//! -//! # Purpose -//! -//! Symphonia requires audio sources to implement its `MediaSource` trait, which -//! provides metadata about stream characteristics like seekability and byte length. -//! This adapter wraps standard Rust I/O types to provide this interface. -//! -//! # Architecture -//! -//! The adapter acts as a thin wrapper that: -//! - Delegates I/O operations to the wrapped type -//! - Provides stream metadata from decoder settings -//! - Maintains compatibility with Symphonia's requirements -//! - Preserves performance characteristics of the underlying source -//! -//! # Performance -//! -//! The wrapper has minimal overhead: -//! - Zero-cost delegation for read/seek operations -//! - Inline functions for optimal performance -//! - No additional buffering or copying -//! - Metadata cached from initial configuration - -use std::io::{Read, Result, Seek, SeekFrom}; - -use symphonia::core::io::MediaSource; - -use crate::decoder::builder::Settings; - -/// A wrapper around a `Read + Seek` type that implements Symphonia's `MediaSource` trait. -/// -/// This adapter enables standard Rust I/O types to be used with Symphonia's media framework -/// by bridging the gap between Rust's I/O traits and Symphonia's requirements. It provides -/// stream metadata while delegating actual I/O operations to the wrapped type. -/// -/// # Use Cases -/// -/// - **File decoding**: Wrapping `std::fs::File` for audio file processing -/// - **Memory streams**: Adapting `std::io::Cursor>` for in-memory audio -/// - **Network streams**: Enabling seekable network streams with known lengths -/// - **Custom sources**: Integrating any `Read + Seek` implementation -/// -/// # Metadata Handling -/// -/// The wrapper provides Symphonia with essential stream characteristics: -/// - **Seekability**: Whether random access operations are supported -/// - **Byte length**: Total stream size for seeking and progress calculations -/// - **Configuration**: Stream properties from decoder builder settings -/// -/// # Generic Parameters -/// -/// * `T` - The wrapped I/O type, must implement `Read + Seek + Send + Sync` -pub struct ReadSeekSource { - /// The wrapped reader/seeker that provides actual I/O operations. - /// - /// All read and seek operations are delegated directly to this inner type, - /// ensuring that performance characteristics are preserved. - inner: T, - - /// Optional length of the media source in bytes. - /// - /// When known, this enables several optimizations: - /// - **Seeking calculations**: Supports percentage-based and end-relative seeks - /// - **Duration estimation**: Helps estimate playback duration for some formats - /// - **Progress tracking**: Enables accurate progress indication - /// - **Buffer management**: Assists with memory allocation decisions - /// - /// This value comes from the decoder settings and should represent the - /// exact byte length of the audio stream. - byte_len: Option, - - /// Whether this media source reports as seekable to Symphonia. - /// - /// This flag controls Symphonia's seeking behavior and optimization decisions: - /// - **`true`**: Enables random access seeking operations - /// - **`false`**: Restricts to forward-only streaming operations - /// - /// The flag should accurately reflect the underlying stream's capabilities. - /// Incorrect values may lead to seek failures or suboptimal performance. - is_seekable: bool, -} - -impl ReadSeekSource { - /// Creates a new `ReadSeekSource` by wrapping a reader/seeker. - /// - /// This constructor extracts relevant configuration from decoder settings - /// to provide Symphonia with appropriate stream metadata while preserving - /// the original I/O source's functionality. - /// - /// # Arguments - /// - /// * `inner` - The reader/seeker to wrap (takes ownership) - /// * `settings` - Decoder settings containing stream metadata - /// - /// # Examples - /// - /// ```ignore - /// use std::fs::File; - /// use rodio::decoder::{Settings, read_seek_source::ReadSeekSource}; - /// - /// let file = File::open("audio.mp3").unwrap(); - /// let mut settings = Settings::default(); - /// settings.byte_len = Some(1024000); - /// settings.is_seekable = true; - /// - /// let source = ReadSeekSource::new(file, &settings); - /// ``` - /// - /// # Performance - /// - /// This operation is very lightweight, involving only metadata copying - /// and ownership transfer. No I/O operations are performed. - #[inline] - pub fn new(inner: T, settings: &Settings) -> Self { - ReadSeekSource { - inner, - byte_len: settings.byte_len, - is_seekable: settings.is_seekable, - } - } -} - -impl MediaSource for ReadSeekSource { - /// Returns whether this media source supports random access seeking. - /// - /// This value is determined from the decoder settings and should accurately - /// reflect the underlying stream's capabilities. Symphonia uses this information - /// to decide whether to attempt seeking operations or restrict to forward-only access. - /// - /// # Returns - /// - /// - `true` if random access seeking is supported - /// - `false` if only forward access is available - /// - /// # Impact on Symphonia - /// - /// When `false`, Symphonia may: - /// - Avoid backward seeking operations - /// - Provide degraded seeking functionality - #[inline] - fn is_seekable(&self) -> bool { - self.is_seekable - } - - /// Returns the total length of the media source in bytes, if known. - /// - /// This length information enables various Symphonia optimizations including - /// seeking calculations, progress indication, and memory management decisions. - /// The value should represent the exact byte length of the audio stream. - /// - /// # Returns - /// - /// - `Some(length)` if the total byte length is known - /// - `None` if the length cannot be determined - /// - /// # Usage by Symphonia - /// - /// Symphonia may use this information for: - /// - **Seeking calculations**: Computing byte offsets for time-based seeks - /// - **Format detection**: Some formats benefit from knowing stream length - /// - /// # Accuracy Requirements - /// - /// The returned length must be accurate, as incorrect values may cause: - /// - Seeking errors or failures - /// - Incorrect duration calculations - /// - Progress indication inaccuracies - #[inline] - fn byte_len(&self) -> Option { - self.byte_len - } -} - -impl Read for ReadSeekSource { - /// Reads bytes from the underlying reader into the provided buffer. - /// - /// This method provides a zero-cost delegation to the wrapped reader's - /// implementation, preserving all performance characteristics and behavior - /// of the original I/O source. - /// - /// # Arguments - /// - /// * `buf` - Buffer to read data into - /// - /// # Returns - /// - /// - `Ok(n)` where `n` is the number of bytes read - /// - `Err(error)` if an I/O error occurred - /// - /// # Behavior - /// - /// The behavior is identical to the wrapped type's `read` implementation: - /// - May read fewer bytes than requested - /// - Returns 0 when end of stream is reached - /// - May block if the underlying source blocks - /// - Preserves all error conditions from the wrapped source - /// - /// # Performance - /// - /// This delegation has zero overhead and maintains the performance - /// characteristics of the underlying I/O implementation. - #[inline] - fn read(&mut self, buf: &mut [u8]) -> Result { - self.inner.read(buf) - } -} - -impl Seek for ReadSeekSource { - /// Seeks to a position in the underlying reader. - /// - /// This method provides a zero-cost delegation to the wrapped reader's - /// seek implementation, preserving all seeking behavior and performance - /// characteristics of the original I/O source. - /// - /// # Arguments - /// - /// * `pos` - The position to seek to, relative to various points in the stream - /// - /// # Returns - /// - /// - `Ok(position)` - The new absolute position from the start of the stream - /// - `Err(error)` - If a seek error occurred - /// - /// # Behavior - /// - /// The behavior is identical to the wrapped type's `seek` implementation: - /// - Supports all `SeekFrom` variants (Start, End, Current) - /// - May fail if the underlying source doesn't support seeking - /// - Preserves all error conditions from the wrapped source - /// - Updates the stream position for subsequent read operations - /// - /// # Performance - /// - /// This delegation has zero overhead and maintains the seeking performance - /// characteristics of the underlying I/O implementation. - /// - /// # Thread Safety - /// - /// Seeking operations are not automatically synchronized. If multiple threads - /// access the same source, external synchronization is required. - #[inline] - fn seek(&mut self, pos: SeekFrom) -> Result { - self.inner.seek(pos) - } -} diff --git a/src/decoder/symphonia.rs b/src/decoder/symphonia.rs index 91306d99..4a845ab7 100644 --- a/src/decoder/symphonia.rs +++ b/src/decoder/symphonia.rs @@ -77,7 +77,7 @@ //! } //! ``` -use std::{sync::Arc, time::Duration}; +use std::{io::{Read, Seek, SeekFrom}, sync::Arc, time::Duration}; use symphonia::{ core::{ @@ -100,6 +100,209 @@ use crate::{ }; use crate::{decoder::builder::Settings, BitDepth}; +/// A wrapper around a `Read + Seek` type that implements Symphonia's `MediaSource` trait. +/// +/// This adapter enables standard Rust I/O types to be used with Symphonia's media framework +/// by bridging the gap between Rust's I/O traits and Symphonia's requirements. It provides +/// stream metadata while delegating actual I/O operations to the wrapped type. +/// +/// # Use Cases +/// +/// - **File decoding**: Wrapping `std::fs::File` for audio file processing +/// - **Memory streams**: Adapting `std::io::Cursor>` for in-memory audio +/// - **Network streams**: Enabling seekable network streams with known lengths +/// - **Custom sources**: Integrating any `Read + Seek` implementation +/// +/// # Metadata Handling +/// +/// The wrapper provides Symphonia with essential stream characteristics: +/// - **Seekability**: Whether random access operations are supported +/// - **Byte length**: Total stream size for seeking and progress calculations +/// - **Configuration**: Stream properties from decoder builder settings +/// +/// # Generic Parameters +/// +/// * `T` - The wrapped I/O type, must implement `Read + Seek + Send + Sync` +pub struct ReadSeekSource { + /// The wrapped reader/seeker that provides actual I/O operations. + /// + /// All read and seek operations are delegated directly to this inner type, + /// ensuring that performance characteristics are preserved. + inner: T, + + /// Optional length of the media source in bytes. + /// + /// When known, this enables several optimizations: + /// - **Seeking calculations**: Supports percentage-based and end-relative seeks + /// - **Duration estimation**: Helps estimate playback duration for some formats + /// - **Progress tracking**: Enables accurate progress indication + /// - **Buffer management**: Assists with memory allocation decisions + /// + /// This value comes from the decoder settings and should represent the + /// exact byte length of the audio stream. + byte_len: Option, + + /// Whether this media source reports as seekable to Symphonia. + /// + /// This flag controls Symphonia's seeking behavior and optimization decisions: + /// - **`true`**: Enables random access seeking operations + /// - **`false`**: Restricts to forward-only streaming operations + /// + /// The flag should accurately reflect the underlying stream's capabilities. + /// Incorrect values may lead to seek failures or suboptimal performance. + is_seekable: bool, +} + +impl ReadSeekSource { + /// Creates a new `ReadSeekSource` by wrapping a reader/seeker. + /// + /// This constructor extracts relevant configuration from decoder settings + /// to provide Symphonia with appropriate stream metadata while preserving + /// the original I/O source's functionality. + /// + /// # Arguments + /// + /// * `inner` - The reader/seeker to wrap (takes ownership) + /// * `settings` - Decoder settings containing stream metadata + /// + /// # Performance + /// + /// This operation is very lightweight, involving only metadata copying + /// and ownership transfer. No I/O operations are performed. + #[inline] + pub fn new(inner: T, settings: &Settings) -> Self { + ReadSeekSource { + inner, + byte_len: settings.byte_len, + is_seekable: settings.is_seekable, + } + } +} + +impl MediaSource for ReadSeekSource { + /// Returns whether this media source supports random access seeking. + /// + /// This value is determined from the decoder settings and should accurately + /// reflect the underlying stream's capabilities. Symphonia uses this information + /// to decide whether to attempt seeking operations or restrict to forward-only access. + /// + /// # Returns + /// + /// - `true` if random access seeking is supported + /// - `false` if only forward access is available + /// + /// # Impact on Symphonia + /// + /// When `false`, Symphonia may: + /// - Avoid backward seeking operations + /// - Provide degraded seeking functionality + #[inline] + fn is_seekable(&self) -> bool { + self.is_seekable + } + + /// Returns the total length of the media source in bytes, if known. + /// + /// This length information enables various Symphonia optimizations including + /// seeking calculations, progress indication, and memory management decisions. + /// The value should represent the exact byte length of the audio stream. + /// + /// # Returns + /// + /// - `Some(length)` if the total byte length is known + /// - `None` if the length cannot be determined + /// + /// # Usage by Symphonia + /// + /// Symphonia may use this information for: + /// - **Seeking calculations**: Computing byte offsets for time-based seeks + /// - **Format detection**: Some formats benefit from knowing stream length + /// + /// # Accuracy Requirements + /// + /// The returned length must be accurate, as incorrect values may cause: + /// - Seeking errors or failures + /// - Incorrect duration calculations + /// - Progress indication inaccuracies + #[inline] + fn byte_len(&self) -> Option { + self.byte_len + } +} + +impl Read for ReadSeekSource { + /// Reads bytes from the underlying reader into the provided buffer. + /// + /// This method provides a zero-cost delegation to the wrapped reader's + /// implementation, preserving all performance characteristics and behavior + /// of the original I/O source. + /// + /// # Arguments + /// + /// * `buf` - Buffer to read data into + /// + /// # Returns + /// + /// - `Ok(n)` where `n` is the number of bytes read + /// - `Err(error)` if an I/O error occurred + /// + /// # Behavior + /// + /// The behavior is identical to the wrapped type's `read` implementation: + /// - May read fewer bytes than requested + /// - Returns 0 when end of stream is reached + /// - May block if the underlying source blocks + /// - Preserves all error conditions from the wrapped source + /// + /// # Performance + /// + /// This delegation has zero overhead and maintains the performance + /// characteristics of the underlying I/O implementation. + #[inline] + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.inner.read(buf) + } +} + +impl Seek for ReadSeekSource { + /// Seeks to a position in the underlying reader. + /// + /// This method provides a zero-cost delegation to the wrapped reader's + /// seek implementation, preserving all seeking behavior and performance + /// characteristics of the original I/O source. + /// + /// # Arguments + /// + /// * `pos` - The position to seek to, relative to various points in the stream + /// + /// # Returns + /// + /// - `Ok(position)` - The new absolute position from the start of the stream + /// - `Err(error)` - If a seek error occurred + /// + /// # Behavior + /// + /// The behavior is identical to the wrapped type's `seek` implementation: + /// - Supports all `SeekFrom` variants (Start, End, Current) + /// - May fail if the underlying source doesn't support seeking + /// - Preserves all error conditions from the wrapped source + /// - Updates the stream position for subsequent read operations + /// + /// # Performance + /// + /// This delegation has zero overhead and maintains the seeking performance + /// characteristics of the underlying I/O implementation. + /// + /// # Thread Safety + /// + /// Seeking operations are not automatically synchronized. If multiple threads + /// access the same source, external synchronization is required. + #[inline] + fn seek(&mut self, pos: SeekFrom) -> std::io::Result { + self.inner.seek(pos) + } +} + /// Multi-format audio decoder using the Symphonia library. /// /// This decoder provides comprehensive audio format support through Symphonia's From 712b61a50e1f41d9e35381ec61bfcd1e2f12d076 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 13 Sep 2025 15:29:15 +0200 Subject: [PATCH 14/17] fix: import Settings from decoder::builder in decoder modules --- src/decoder/flac.rs | 3 ++- src/decoder/mp3.rs | 3 ++- src/decoder/vorbis.rs | 3 ++- src/decoder/wav.rs | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/decoder/flac.rs b/src/decoder/flac.rs index 4f0052b7..59f21a3b 100644 --- a/src/decoder/flac.rs +++ b/src/decoder/flac.rs @@ -71,7 +71,8 @@ use claxon::{FlacReader, FlacReaderOptions}; use dasp_sample::Sample as _; use dasp_sample::I24; -use super::{utils, Settings}; +use super::utils; +use crate::decoder::builder::Settings; use crate::{source::SeekError, BitDepth, ChannelCount, Sample, SampleRate, Source}; /// Reader options for `claxon` FLAC decoder. diff --git a/src/decoder/mp3.rs b/src/decoder/mp3.rs index c26bce08..1ae05bfc 100644 --- a/src/decoder/mp3.rs +++ b/src/decoder/mp3.rs @@ -63,7 +63,8 @@ use dasp_sample::Sample as _; use minimp3::{Decoder, Frame}; use minimp3_fixed as minimp3; -use super::{utils, Settings}; +use super::utils; +use crate::decoder::builder::Settings; use crate::{ decoder::builder::SeekMode, source::SeekError, BitDepth, ChannelCount, Sample, SampleRate, Source, diff --git a/src/decoder/vorbis.rs b/src/decoder/vorbis.rs index 5cee2708..77862136 100644 --- a/src/decoder/vorbis.rs +++ b/src/decoder/vorbis.rs @@ -70,7 +70,8 @@ use lewton::{ VorbisError::{BadAudio, OggError}, }; -use super::{utils, Settings}; +use super::utils; +use crate::decoder::builder::Settings; use crate::{ decoder::builder::SeekMode, source::SeekError, BitDepth, ChannelCount, Sample, SampleRate, Source, diff --git a/src/decoder/wav.rs b/src/decoder/wav.rs index 52a45dc3..a4592650 100644 --- a/src/decoder/wav.rs +++ b/src/decoder/wav.rs @@ -74,7 +74,7 @@ use hound::{SampleFormat, WavReader}; use super::utils; use crate::{ - decoder::Settings, source::SeekError, BitDepth, ChannelCount, Sample, SampleRate, Source, + decoder::builder::Settings, source::SeekError, BitDepth, ChannelCount, Sample, SampleRate, Source, }; /// Decoder for the WAV format using the `hound` library. From 46ebd9ebe9d0332a09d6a10a1f9eaf6b393b7a7b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 13 Sep 2025 15:49:00 +0200 Subject: [PATCH 15/17] style: reorder imports and clean up formatting in decoder modules --- src/decoder/looped.rs | 2 +- src/decoder/mod.rs | 6 +----- src/decoder/symphonia.rs | 6 +++++- src/decoder/wav.rs | 3 ++- src/source/noise.rs | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/decoder/looped.rs b/src/decoder/looped.rs index e463e585..d4b154af 100644 --- a/src/decoder/looped.rs +++ b/src/decoder/looped.rs @@ -261,4 +261,4 @@ where )))), } } -} \ No newline at end of file +} diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index c8802a57..b0255b3a 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -63,8 +63,8 @@ use crate::{ pub mod builder; pub use builder::DecoderBuilder; -mod utils; mod looped; +mod utils; pub use looped::LoopedDecoder; #[cfg(feature = "claxon")] @@ -82,7 +82,6 @@ mod wav; /// See the [module-level documentation](self) for examples and usage. pub struct Decoder(DecoderImpl); - /// This enum dispatches to the appropriate decoder based on detected format /// and available features. Large enum variant size is acceptable here since /// these are infrequently created and moved. @@ -924,9 +923,6 @@ where } } - - - /// Errors that can occur when creating a decoder. #[derive(Debug, thiserror::Error, Clone)] pub enum DecoderError { diff --git a/src/decoder/symphonia.rs b/src/decoder/symphonia.rs index 4a845ab7..7163591d 100644 --- a/src/decoder/symphonia.rs +++ b/src/decoder/symphonia.rs @@ -77,7 +77,11 @@ //! } //! ``` -use std::{io::{Read, Seek, SeekFrom}, sync::Arc, time::Duration}; +use std::{ + io::{Read, Seek, SeekFrom}, + sync::Arc, + time::Duration, +}; use symphonia::{ core::{ diff --git a/src/decoder/wav.rs b/src/decoder/wav.rs index a4592650..54a48ca3 100644 --- a/src/decoder/wav.rs +++ b/src/decoder/wav.rs @@ -74,7 +74,8 @@ use hound::{SampleFormat, WavReader}; use super::utils; use crate::{ - decoder::builder::Settings, source::SeekError, BitDepth, ChannelCount, Sample, SampleRate, Source, + decoder::builder::Settings, source::SeekError, BitDepth, ChannelCount, Sample, SampleRate, + Source, }; /// Decoder for the WAV format using the `hound` library. diff --git a/src/source/noise.rs b/src/source/noise.rs index e6aa318b..f9d86226 100644 --- a/src/source/noise.rs +++ b/src/source/noise.rs @@ -20,7 +20,7 @@ //! use std::num::NonZero; //! use rodio::source::noise::{WhiteUniform, Pink, WhiteTriangular, Blue, Red}; //! -//! let sample_rate = SampleRate::new(44100).unwrap(); +//! let sample_rate = rodio::SampleRate::new(44100).unwrap(); //! //! // Simple usage - creates generators with `SmallRng` //! From 5ecd76b4b4bd347231386dcbb11161b193a065f7 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 17 Sep 2025 22:51:14 +0200 Subject: [PATCH 16/17] refactor: use SampleRate type throughout style: cargo fmt --- src/decoder/flac.rs | 9 +- src/decoder/looped_improved.rs | 186 +++++++++++ src/decoder/symphonia_improved.rs | 511 ++++++++++++++++++++++++++++++ src/decoder/utils.rs | 62 ++-- src/decoder/vorbis.rs | 8 +- src/decoder/wav.rs | 23 +- 6 files changed, 740 insertions(+), 59 deletions(-) create mode 100644 src/decoder/looped_improved.rs create mode 100644 src/decoder/symphonia_improved.rs diff --git a/src/decoder/flac.rs b/src/decoder/flac.rs index 59f21a3b..75254611 100644 --- a/src/decoder/flac.rs +++ b/src/decoder/flac.rs @@ -277,14 +277,14 @@ where let reader = FlacReader::new_ext(data, READER_OPTIONS).expect("should still be flac"); let spec = reader.streaminfo(); - let sample_rate = spec.sample_rate; + let sample_rate = SampleRate::new(spec.sample_rate) + .expect("flac data should never have a zero sample rate"); let max_block_size = spec.max_block_size as usize * spec.channels as usize; // `samples` in FLAC means "inter-channel samples" aka frames // so we do not divide by `self.channels` here. let total_samples = spec.samples; - let total_duration = - total_samples.map(|s| utils::samples_to_duration(s, sample_rate as u64)); + let total_duration = total_samples.map(|s| utils::samples_to_duration(s, sample_rate)); Ok(Self { reader: Some(reader), @@ -293,8 +293,7 @@ where current_block_off: 0, bits_per_sample: BitDepth::new(spec.bits_per_sample) .expect("flac should never have zero bits per sample"), - sample_rate: SampleRate::new(sample_rate) - .expect("flac data should never have a zero sample rate"), + sample_rate, channels: ChannelCount::new( spec.channels .try_into() diff --git a/src/decoder/looped_improved.rs b/src/decoder/looped_improved.rs new file mode 100644 index 00000000..b2a592a8 --- /dev/null +++ b/src/decoder/looped_improved.rs @@ -0,0 +1,186 @@ +use std::{ + io::{Read, Seek}, + marker::PhantomData, + sync::Arc, + time::Duration, +}; + +use crate::{ + common::{ChannelCount, SampleRate}, + math::nz, + source::{SeekError, Source}, + BitDepth, Sample, +}; + +use super::{builder::Settings, DecoderError, DecoderImpl}; + +#[cfg(feature = "claxon")] +use super::flac; +#[cfg(feature = "minimp3")] +use super::mp3; +#[cfg(feature = "symphonia")] +use super::symphonia; +#[cfg(feature = "lewton")] +use super::vorbis; +#[cfg(feature = "hound")] +use super::wav; + +/// Decoder that loops indefinitely by seeking back to the start when reaching the end. +/// +/// Uses fast seeking for seekable sources with gapless playback, otherwise recreates the +/// decoder while caching metadata to avoid expensive file scanning. +pub struct LoopedDecoder { + pub(super) inner: Option>, + pub(super) settings: Settings, + cached_duration: Option, +} + +impl LoopedDecoder +where + R: Read + Seek, +{ + pub(super) fn new(decoder: DecoderImpl, settings: Settings) -> Self { + Self { + inner: Some(decoder), + settings, + cached_duration: None, + } + } + + /// Recreates decoder with cached metadata to avoid expensive file scanning. + fn recreate_decoder_with_cache( + &mut self, + decoder: DecoderImpl, + ) -> Option<(DecoderImpl, Option)> { + let mut fast_settings = self.settings.clone(); + fast_settings.total_duration = self.cached_duration; + + let (new_decoder, sample) = match decoder { + #[cfg(feature = "hound")] + DecoderImpl::Wav(source) => { + let mut reader = source.into_inner(); + reader.rewind().ok()?; + let mut source = wav::WavDecoder::new_with_settings(reader, &fast_settings).ok()?; + let sample = source.next(); + (DecoderImpl::Wav(source), sample) + } + #[cfg(feature = "lewton")] + DecoderImpl::Vorbis(source) => { + let mut reader = source.into_inner().into_inner().into_inner(); + reader.rewind().ok()?; + let mut source = + vorbis::VorbisDecoder::new_with_settings(reader, &fast_settings).ok()?; + let sample = source.next(); + (DecoderImpl::Vorbis(source), sample) + } + #[cfg(feature = "claxon")] + DecoderImpl::Flac(source) => { + let mut reader = source.into_inner(); + reader.rewind().ok()?; + let mut source = + flac::FlacDecoder::new_with_settings(reader, &fast_settings).ok()?; + let sample = source.next(); + (DecoderImpl::Flac(source), sample) + } + #[cfg(feature = "minimp3")] + DecoderImpl::Mp3(source) => { + let mut reader = source.into_inner(); + reader.rewind().ok()?; + let mut source = mp3::Mp3Decoder::new_with_settings(reader, &fast_settings).ok()?; + let sample = source.next(); + (DecoderImpl::Mp3(source), sample) + } + #[cfg(feature = "symphonia")] + DecoderImpl::Symphonia(source, PhantomData) => { + let mut reader = source.into_inner(); + reader.rewind().ok()?; + let mut source = + symphonia::SymphoniaDecoder::new_with_settings(reader, &fast_settings).ok()?; + let sample = source.next(); + (DecoderImpl::Symphonia(source, PhantomData), sample) + } + DecoderImpl::None(_, _) => return None, + }; + Some((new_decoder, sample)) + } +} + +impl Iterator for LoopedDecoder +where + R: Read + Seek, +{ + type Item = Sample; + + fn next(&mut self) -> Option { + if let Some(inner) = &mut self.inner { + if let Some(sample) = inner.next() { + return Some(sample); + } + + // Cache duration on first loop to avoid recalculation + if self.cached_duration.is_none() { + self.cached_duration = inner.total_duration(); + } + + // Fast gapless seeking when available + if self.settings.gapless + && self.settings.is_seekable + && inner.try_seek(Duration::ZERO).is_ok() + { + return inner.next(); + } + + // Recreation fallback with cached metadata + let decoder = self.inner.take()?; + let (new_decoder, sample) = self.recreate_decoder_with_cache(decoder)?; + self.inner = Some(new_decoder); + sample + } else { + None + } + } + + fn size_hint(&self) -> (usize, Option) { + ( + self.inner.as_ref().map_or(0, |inner| inner.size_hint().0), + None, // Infinite + ) + } +} + +impl Source for LoopedDecoder +where + R: Read + Seek, +{ + fn current_span_len(&self) -> Option { + self.inner.as_ref()?.current_span_len() + } + + fn channels(&self) -> ChannelCount { + self.inner.as_ref().map_or(nz!(1), |inner| inner.channels()) + } + + fn sample_rate(&self) -> SampleRate { + self.inner + .as_ref() + .map_or(nz!(44100), |inner| inner.sample_rate()) + } + + /// Always returns `None` since looped decoders have no fixed end. + fn total_duration(&self) -> Option { + None + } + + fn bits_per_sample(&self) -> Option { + self.inner.as_ref()?.bits_per_sample() + } + + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + match &mut self.inner { + Some(inner) => inner.try_seek(pos), + None => Err(SeekError::Other(Arc::new(DecoderError::IoError( + "Looped source ended when it failed to loop back".to_string(), + )))), + } + } +} \ No newline at end of file diff --git a/src/decoder/symphonia_improved.rs b/src/decoder/symphonia_improved.rs new file mode 100644 index 00000000..16ca38bf --- /dev/null +++ b/src/decoder/symphonia_improved.rs @@ -0,0 +1,511 @@ +//! Symphonia multi-format decoder supporting AAC, FLAC, MP3, Vorbis, and more. + +use std::{io::{Read, Seek, SeekFrom}, sync::Arc, time::Duration}; + +use symphonia::{ + core::{ + audio::SampleBuffer, + codecs::{Decoder, DecoderOptions, CODEC_TYPE_NULL, CODEC_TYPE_VORBIS}, + errors::Error, + formats::{FormatOptions, FormatReader, SeekMode as SymphoniaSeekMode, SeekTo}, + io::{MediaSource, MediaSourceStream}, + meta::MetadataOptions, + probe::Hint, + }, + default::get_probe, +}; + +use super::DecoderError; +use crate::{ + common::{ChannelCount, Sample, SampleRate}, + decoder::builder::SeekMode, + source, Source, +}; +use crate::{decoder::builder::Settings, BitDepth}; + +/// Adapter to use `Read + Seek` types as Symphonia `MediaSource`. +pub struct ReadSeekSource { + inner: T, + byte_len: Option, + is_seekable: bool, +} + +impl ReadSeekSource { + pub fn new(inner: T, settings: &Settings) -> Self { + ReadSeekSource { + inner, + byte_len: settings.byte_len, + is_seekable: settings.is_seekable, + } + } +} + +impl MediaSource for ReadSeekSource { + fn is_seekable(&self) -> bool { + self.is_seekable + } + + fn byte_len(&self) -> Option { + self.byte_len + } +} + +impl Read for ReadSeekSource { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.inner.read(buf) + } +} + +impl Seek for ReadSeekSource { + fn seek(&mut self, pos: SeekFrom) -> std::io::Result { + self.inner.seek(pos) + } +} + +/// Multi-format decoder using Symphonia library. +/// +/// Automatically handles format detection, track selection, and codec management. +/// Supports seeking modes: fastest (coarse) vs nearest (sample-accurate). +pub struct SymphoniaDecoder { + decoder: Box, + current_span_offset: usize, + demuxer: Box, + total_duration: Option, + sample_rate: SampleRate, + channels: ChannelCount, + bits_per_sample: Option, + buffer: Option>, + seek_mode: SeekMode, + total_samples: Option, + samples_read: u64, + track_id: u32, + is_seekable: bool, + byte_len: Option, +} + +impl SymphoniaDecoder { + pub fn new(mss: MediaSourceStream) -> Result { + Self::new_with_settings(mss, &Settings::default()) + } + + pub fn new_with_settings( + mss: MediaSourceStream, + settings: &Settings, + ) -> Result { + match SymphoniaDecoder::init(mss, settings) { + Err(e) => match e { + Error::IoError(e) => Err(DecoderError::IoError(e.to_string())), + Error::DecodeError(e) => Err(DecoderError::DecodeError(e)), + Error::SeekError(_) => { + unreachable!("Seek errors should not occur during initialization") + } + Error::Unsupported(_) => Err(DecoderError::UnrecognizedFormat), + Error::LimitError(e) => Err(DecoderError::LimitError(e)), + Error::ResetRequired => Err(DecoderError::ResetRequired), + }, + Ok(Some(decoder)) => Ok(decoder), + Ok(None) => Err(DecoderError::NoStreams), + } + } + + pub fn into_inner(self) -> MediaSourceStream { + self.demuxer.into_inner() + } + + fn init( + mss: MediaSourceStream, + settings: &Settings, + ) -> symphonia::core::errors::Result> { + let mut hint = Hint::new(); + if let Some(ext) = settings.hint.as_ref() { + hint.with_extension(ext); + } + if let Some(typ) = settings.mime_type.as_ref() { + hint.mime_type(typ); + } + let format_opts: FormatOptions = FormatOptions { + enable_gapless: settings.gapless, + ..Default::default() + }; + let metadata_opts: MetadataOptions = Default::default(); + let is_seekable = mss.is_seekable(); + let byte_len = mss.byte_len(); + + // Find first supported track + let mut probed = get_probe().format(&hint, mss, &format_opts, &metadata_opts)?; + let track = probed + .format + .tracks() + .iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + .ok_or(Error::Unsupported("No track with supported codec"))?; + + let mut track_id = track.id; + let mut decoder = symphonia::default::get_codecs() + .make(&track.codec_params, &DecoderOptions::default())?; + let total_duration: Option = track + .codec_params + .time_base + .zip(track.codec_params.n_frames) + .map(|(base, spans)| base.calc_time(spans).into()); + + // Decode first packet to establish stream spec + let (spec, buffer) = loop { + let current_span = match probed.format.next_packet() { + Ok(packet) => packet, + Err(Error::ResetRequired) => { + track_id = recreate_decoder(&mut probed.format, &mut decoder, None)?; + continue; + } + Err(e) => return Err(e), + }; + + if current_span.track_id() != track_id { + continue; + } + + match decoder.decode(¤t_span) { + Ok(decoded) => { + if decoded.frames() > 0 { + let spec = decoded.spec().to_owned(); + let mut sample_buffer = + SampleBuffer::::new(decoded.capacity() as u64, *decoded.spec()); + sample_buffer.copy_interleaved_ref(decoded); + let buffer = Some(sample_buffer); + break (spec, buffer); + } + continue; + } + Err(e) => { + if should_continue_on_decode_error(&e, &mut decoder) { + continue; + } else { + return Err(e); + } + } + } + }; + + let sample_rate = SampleRate::new(spec.rate).expect("Invalid sample rate"); + let channels = spec + .channels + .count() + .try_into() + .ok() + .and_then(ChannelCount::new) + .expect("Invalid channel count"); + let bits_per_sample = decoder + .codec_params() + .bits_per_sample + .and_then(BitDepth::new); + + // Calculate total samples from metadata when available + let total_samples = { + if let (Some(n_frames), Some(max_frame_length)) = ( + decoder.codec_params().n_frames, + decoder.codec_params().max_frames_per_packet, + ) { + n_frames.checked_mul(max_frame_length) + } else if let Some(duration) = total_duration { + let total_secs = duration.as_secs_f64(); + Some((total_secs * sample_rate.get() as f64 * channels.get() as f64).ceil() as u64) + } else { + None + } + }; + + Ok(Some(Self { + decoder, + current_span_offset: 0, + demuxer: probed.format, + total_duration, + sample_rate, + channels, + bits_per_sample, + buffer, + seek_mode: settings.seek_mode, + total_samples, + samples_read: 0, + track_id, + is_seekable, + byte_len, + })) + } + + fn cache_spec(&mut self) { + if let Some(rate) = self.decoder.codec_params().sample_rate { + if let Some(rate) = SampleRate::new(rate) { + self.sample_rate = rate; + } + } + + if let Some(channels) = self.decoder.codec_params().channels { + if let Some(count) = channels.count().try_into().ok().and_then(ChannelCount::new) { + self.channels = count; + } + } + + if let Some(bits_per_sample) = self.decoder.codec_params().bits_per_sample { + self.bits_per_sample = BitDepth::new(bits_per_sample); + } + } +} + +impl Source for SymphoniaDecoder { + fn current_span_len(&self) -> Option { + self.buffer.as_ref().map(SampleBuffer::len).or(Some(0)) + } + + fn channels(&self) -> ChannelCount { + self.channels + } + + fn sample_rate(&self) -> SampleRate { + self.sample_rate + } + + fn total_duration(&self) -> Option { + self.total_duration + } + + fn bits_per_sample(&self) -> Option { + self.bits_per_sample + } + + /// Seeks to the specified position. + /// + /// Behavior varies by format: + /// - MP3 requires byte_len for coarse mode + /// - OGG requires is_seekable flag + /// - Backward seeks need is_seekable=true + fn try_seek(&mut self, pos: Duration) -> Result<(), source::SeekError> { + // Clamp to stream end + let mut target = pos; + if let Some(total_duration) = self.total_duration() { + if target > total_duration { + target = total_duration; + } + } + + let target_samples = (target.as_secs_f64() + * self.sample_rate().get() as f64 + * self.channels().get() as f64) as u64; + + let active_channel = self.current_span_offset % self.channels().get() as usize; + + if !self.is_seekable { + if target_samples < self.samples_read { + return Err(source::SeekError::ForwardOnly); + } + + // Linear seeking workaround for Vorbis + if self.decoder.codec_params().codec == CODEC_TYPE_VORBIS { + for _ in self.samples_read..target_samples { + let _ = self.next(); + } + return Ok(()); + } + } + + let seek_mode = if self.seek_mode == SeekMode::Fastest && self.byte_len.is_none() { + SymphoniaSeekMode::Accurate // Fallback when no byte length + } else { + self.seek_mode.into() + }; + + let seek_res = self + .demuxer + .seek( + seek_mode, + SeekTo::Time { + time: target.into(), + track_id: Some(self.track_id), + }, + ) + .map_err(Arc::new)?; + + self.decoder.reset(); + self.buffer = None; + + // Update position counter based on actual seek result + self.samples_read = if let Some(time_base) = self.decoder.codec_params().time_base { + let actual_time = Duration::from(time_base.calc_time(seek_res.actual_ts)); + (actual_time.as_secs_f64() + * self.sample_rate().get() as f64 + * self.channels().get() as f64) as u64 + } else { + seek_res.actual_ts * self.sample_rate().get() as u64 * self.channels().get() as u64 + }; + + // Fine-tune to exact position for precise mode + let mut samples_to_skip = 0; + if self.seek_mode == SeekMode::Nearest { + samples_to_skip = (Duration::from( + self.decoder + .codec_params() + .time_base + .expect("time base availability guaranteed by caller") + .calc_time(seek_res.required_ts.saturating_sub(seek_res.actual_ts)), + ) + .as_secs_f32() + * self.sample_rate().get() as f32 + * self.channels().get() as f32) + .ceil() as usize; + + samples_to_skip -= samples_to_skip % self.channels().get() as usize + }; + + // Advance to correct channel position + for _ in 0..(samples_to_skip + active_channel) { + let _ = self.next(); + } + + Ok(()) + } +} + +impl Iterator for SymphoniaDecoder { + type Item = Sample; + + fn next(&mut self) -> Option { + // Return sample from current buffer if available + if let Some(buffer) = &self.buffer { + if self.current_span_offset < buffer.len() { + let sample = buffer.samples()[self.current_span_offset]; + self.current_span_offset += 1; + self.samples_read += 1; + return Some(sample); + } + } + + // Decode next packet + let decoded = loop { + let packet = match self.demuxer.next_packet() { + Ok(packet) => packet, + Err(Error::ResetRequired) => { + self.track_id = + recreate_decoder(&mut self.demuxer, &mut self.decoder, Some(self.track_id)) + .ok()?; + self.cache_spec(); + self.buffer = None; + continue; + } + Err(_) => return None, + }; + + match self.decoder.decode(&packet) { + Ok(decoded) => { + if decoded.frames() > 0 { + break decoded; + } + continue; + } + Err(e) => { + if should_continue_on_decode_error(&e, &mut self.decoder) { + if let Some(buffer) = self.buffer.as_mut() { + buffer.clear(); + } + continue; + } else { + self.buffer = None; + return None; + } + } + } + }; + + // Update buffer with new packet + let buffer = match self.buffer.as_mut() { + Some(buffer) => buffer, + None => { + self.buffer.insert(SampleBuffer::new( + decoded.capacity() as u64, + *decoded.spec(), + )) + } + }; + buffer.copy_interleaved_ref(decoded); + self.current_span_offset = 0; + + if !buffer.is_empty() { + let sample = buffer.samples()[0]; + self.current_span_offset = 1; + self.samples_read += 1; + Some(sample) + } else { + self.next() // Try again for metadata-only packets + } + } + + fn size_hint(&self) -> (usize, Option) { + let buffered_samples = self + .current_span_len() + .unwrap_or(0) + .saturating_sub(self.current_span_offset); + + if let Some(total_samples) = self.total_samples { + let total_remaining = total_samples.saturating_sub(self.samples_read) as usize; + (buffered_samples, Some(total_remaining)) + } else if self.buffer.is_none() { + (0, Some(0)) + } else { + (buffered_samples, None) + } + } +} + +fn recreate_decoder( + format: &mut Box, + decoder: &mut Box, + current_track_id: Option, +) -> Result { + let track = if let Some(current_id) = current_track_id { + let tracks = format.tracks(); + let current_index = tracks.iter().position(|t| t.id == current_id); + + if let Some(idx) = current_index { + tracks + .iter() + .skip(idx + 1) + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + } else { + None + } + } else { + format + .tracks() + .iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + } + .ok_or(Error::Unsupported( + "No supported track found after current track", + ))?; + + *decoder = + symphonia::default::get_codecs().make(&track.codec_params, &DecoderOptions::default())?; + + Ok(track.id) +} + +fn should_continue_on_decode_error( + error: &symphonia::core::errors::Error, + decoder: &mut Box, +) -> bool { + match error { + Error::DecodeError(_) | Error::IoError(_) => true, + Error::ResetRequired => { + decoder.reset(); + true + } + _ => false, + } +} + +impl From for SymphoniaSeekMode { + fn from(mode: SeekMode) -> Self { + match mode { + SeekMode::Fastest => SymphoniaSeekMode::Coarse, + SeekMode::Nearest => SymphoniaSeekMode::Accurate, + } + } +} \ No newline at end of file diff --git a/src/decoder/utils.rs b/src/decoder/utils.rs index 40ced726..768b4754 100644 --- a/src/decoder/utils.rs +++ b/src/decoder/utils.rs @@ -30,6 +30,9 @@ #[cfg(any(feature = "claxon", feature = "hound"))] use std::time::Duration; +#[cfg(any(feature = "claxon", feature = "hound"))] +use crate::SampleRate; + #[cfg(any( feature = "claxon", feature = "hound", @@ -62,33 +65,11 @@ use std::io::{Read, Seek, SeekFrom}; /// /// # Edge Cases /// -/// - **Zero sample rate**: Returns `Duration::ZERO` to prevent division by zero /// - **Zero samples**: Returns `Duration::ZERO` (mathematically correct) -/// - **Large values**: Handles overflow gracefully within Duration limits -/// -/// # Examples -/// -/// ```ignore -/// use std::time::Duration; -/// # use rodio::decoder::utils::samples_to_duration; -/// -/// // 1 second at 44.1kHz -/// assert_eq!(samples_to_duration(44100, 44100), Duration::from_secs(1)); -/// -/// // 0.5 seconds at 44.1kHz -/// assert_eq!(samples_to_duration(22050, 44100), Duration::from_millis(500)); -/// ``` -/// -/// # Performance -/// -/// This function is optimized for common audio sample rates and performs -/// integer arithmetic only, making it suitable for real-time applications. +/// - **Large values**: Handles overflow gracefully within `Duration` limits #[cfg(any(feature = "claxon", feature = "hound",))] -pub(super) fn samples_to_duration(samples: u64, sample_rate: u64) -> Duration { - if sample_rate == 0 { - return Duration::ZERO; - } - +pub(super) fn samples_to_duration(samples: u64, sample_rate: SampleRate) -> Duration { + let sample_rate = sample_rate.get() as u64; let secs = samples / sample_rate; let nanos = ((samples % sample_rate) * 1_000_000_000) / sample_rate; Duration::new(secs, nanos as u32) @@ -182,34 +163,31 @@ mod tests { #[test] fn test_samples_to_duration() { // Standard CD quality: 1 second at 44.1kHz - assert_eq!(samples_to_duration(44100, 44100), Duration::from_secs(1)); + let rate_44_1k = SampleRate::new(44100).unwrap(); + assert_eq!( + samples_to_duration(rate_44_1k.get() as u64, rate_44_1k), + Duration::from_secs(1) + ); // Half second at CD quality assert_eq!( - samples_to_duration(22050, 44100), + samples_to_duration(rate_44_1k.get() as u64 / 2, rate_44_1k), Duration::from_millis(500) ); - // Professional audio: 1 second at 48kHz - assert_eq!(samples_to_duration(48000, 48000), Duration::from_secs(1)); - - // High resolution: 1 second at 96kHz - assert_eq!(samples_to_duration(96000, 96000), Duration::from_secs(1)); - // Edge case: Zero samples should return zero duration - assert_eq!(samples_to_duration(0, 44100), Duration::ZERO); - - // Edge case: Zero sample rate should not panic and return zero - assert_eq!(samples_to_duration(44100, 0), Duration::ZERO); + assert_eq!(samples_to_duration(0, rate_44_1k), Duration::ZERO); // Precision test: Fractional milliseconds // 441 samples at 44.1kHz = 10ms exactly - assert_eq!(samples_to_duration(441, 44100), Duration::from_millis(10)); + assert_eq!( + samples_to_duration(rate_44_1k.get() as u64 / 100, rate_44_1k), + Duration::from_millis(10) + ); // Very small durations should have nanosecond precision - // 1 sample at 44.1kHz ≈ 22.676 microseconds - let one_sample_duration = samples_to_duration(1, 44100); - assert!(one_sample_duration.as_nanos() > 22000); - assert!(one_sample_duration.as_nanos() < 23000); + // 1 sample at 44.1kHz ≈ 22.675 microseconds + let one_sample_duration = samples_to_duration(1, rate_44_1k); + assert_eq!(one_sample_duration.as_nanos(), 22675); } } diff --git a/src/decoder/vorbis.rs b/src/decoder/vorbis.rs index 77862136..299b115c 100644 --- a/src/decoder/vorbis.rs +++ b/src/decoder/vorbis.rs @@ -840,6 +840,8 @@ fn read_next_non_empty_packet( /// point a final linear scan ensures all granule positions are found. The packet /// limit during binary search prevents excessive scanning in dense regions. fn find_last_granule(data: &mut R, byte_len: u64) -> Option { + const BINARY_SEARCH_PACKET_LIMIT: usize = 50; + // Save current position let original_pos = data.stream_position().unwrap_or_default(); let _ = data.rewind(); @@ -853,7 +855,7 @@ fn find_last_granule(data: &mut R, byte_len: u64) -> Option let mid = left + (right - left) / 2; // Try to find a granule from this position (limited packet scan during binary search) - match find_granule_from_position(data, mid, Some(50)) { + match find_granule_from_position(data, mid, Some(BINARY_SEARCH_PACKET_LIMIT)) { Some(_granule) => { // Found a granule, this means there's content at or after this position best_start_position = mid; @@ -894,9 +896,11 @@ fn find_last_granule(data: &mut R, byte_len: u64) -> Option /// /// # Packet Limit Rationale /// +/// Maximum number of packets to scan during binary search optimization. +/// /// When used during binary search, the packet limit prevents excessive scanning: /// - **Typical Ogg pages**: Contain 1-10 packets depending on content -/// - **50 packet limit**: Covers roughly 5-50 pages (~20-400KB depending on bitrate) +/// - **Binary search limit**: Covers roughly 5-50 pages (~20-400KB depending on bitrate) /// - **Balance**: Finding granules quickly vs. avoiding excessive I/O during binary search /// - **Final scan**: No limit ensures complete coverage from optimized position /// diff --git a/src/decoder/wav.rs b/src/decoder/wav.rs index 54a48ca3..5a72ee42 100644 --- a/src/decoder/wav.rs +++ b/src/decoder/wav.rs @@ -256,29 +256,32 @@ where let spec = reader.spec(); let len = reader.len() as u64; let total_samples = reader.len(); + + let sample_rate = SampleRate::new(spec.sample_rate) + .expect("wav should have a sample rate higher than zero"); + let channels = + ChannelCount::new(spec.channels).expect("wav should have at least one channel"); + let bits_per_sample = BitDepth::new(spec.bits_per_sample.into()) + .expect("wav should have a bit depth higher than zero"); + let reader = SamplesIterator { reader, samples_read: 0, total_samples, }; - let sample_rate = spec.sample_rate; - let channels = spec.channels; - // len is number of samples, not bytes, so use samples_to_duration // Note: hound's len() returns total samples across all channels - let samples_per_channel = len / (channels as u64); - let total_duration = utils::samples_to_duration(samples_per_channel, sample_rate as u64); + let samples_per_channel = len / (channels.get() as u64); + let total_duration = utils::samples_to_duration(samples_per_channel, sample_rate); Ok(Self { reader, total_duration, - sample_rate: SampleRate::new(sample_rate) - .expect("wav should have a sample rate higher then zero"), - channels: ChannelCount::new(channels).expect("wav should have a least one channel"), + sample_rate, + channels, is_seekable: settings.is_seekable, - bits_per_sample: BitDepth::new(spec.bits_per_sample.into()) - .expect("wav should have a bit depth higher then zero"), + bits_per_sample, }) } From 2d6071376b99b770ecbdead6b48879f0b95cb9df Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 18 Sep 2025 00:10:17 +0200 Subject: [PATCH 17/17] fix: warnings with no default features --- src/decoder/looped.rs | 21 +++++++++++---------- src/decoder/mod.rs | 12 ++++++------ 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/decoder/looped.rs b/src/decoder/looped.rs index d4b154af..dca9d467 100644 --- a/src/decoder/looped.rs +++ b/src/decoder/looped.rs @@ -1,10 +1,12 @@ use std::{ io::{Read, Seek}, - marker::PhantomData, sync::Arc, time::Duration, }; +#[cfg(feature = "symphonia")] +use std::marker::PhantomData; + use crate::{ common::{ChannelCount, SampleRate}, math::nz, @@ -80,14 +82,14 @@ where let mut fast_settings = self.settings.clone(); fast_settings.total_duration = self.cached_duration; - let (new_decoder, sample) = match decoder { + match decoder { #[cfg(feature = "hound")] DecoderImpl::Wav(source) => { let mut reader = source.into_inner(); reader.rewind().ok()?; let mut source = wav::WavDecoder::new_with_settings(reader, &fast_settings).ok()?; let sample = source.next(); - (DecoderImpl::Wav(source), sample) + Some((DecoderImpl::Wav(source), sample)) } #[cfg(feature = "lewton")] DecoderImpl::Vorbis(source) => { @@ -96,7 +98,7 @@ where let mut source = vorbis::VorbisDecoder::new_with_settings(reader, &fast_settings).ok()?; let sample = source.next(); - (DecoderImpl::Vorbis(source), sample) + Some((DecoderImpl::Vorbis(source), sample)) } #[cfg(feature = "claxon")] DecoderImpl::Flac(source) => { @@ -105,7 +107,7 @@ where let mut source = flac::FlacDecoder::new_with_settings(reader, &fast_settings).ok()?; let sample = source.next(); - (DecoderImpl::Flac(source), sample) + Some((DecoderImpl::Flac(source), sample)) } #[cfg(feature = "minimp3")] DecoderImpl::Mp3(source) => { @@ -113,7 +115,7 @@ where reader.rewind().ok()?; let mut source = mp3::Mp3Decoder::new_with_settings(reader, &fast_settings).ok()?; let sample = source.next(); - (DecoderImpl::Mp3(source), sample) + Some((DecoderImpl::Mp3(source), sample)) } #[cfg(feature = "symphonia")] DecoderImpl::Symphonia(source, PhantomData) => { @@ -122,11 +124,10 @@ where let mut source = symphonia::SymphoniaDecoder::new_with_settings(reader, &fast_settings).ok()?; let sample = source.next(); - (DecoderImpl::Symphonia(source, PhantomData), sample) + Some((DecoderImpl::Symphonia(source, PhantomData), sample)) } - DecoderImpl::None(_, _) => return None, - }; - Some((new_decoder, sample)) + DecoderImpl::None(_, _) => None, + } } } diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index b0255b3a..59f5f8b3 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -249,18 +249,18 @@ impl DecoderImpl { /// Attempts to seek to a given position in the current source. #[inline] - fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + fn try_seek(&mut self, _pos: Duration) -> Result<(), SeekError> { match self { #[cfg(feature = "hound")] - DecoderImpl::Wav(source) => source.try_seek(pos), + DecoderImpl::Wav(source) => source.try_seek(_pos), #[cfg(feature = "lewton")] - DecoderImpl::Vorbis(source) => source.try_seek(pos), + DecoderImpl::Vorbis(source) => source.try_seek(_pos), #[cfg(feature = "claxon")] - DecoderImpl::Flac(source) => source.try_seek(pos), + DecoderImpl::Flac(source) => source.try_seek(_pos), #[cfg(feature = "minimp3")] - DecoderImpl::Mp3(source) => source.try_seek(pos), + DecoderImpl::Mp3(source) => source.try_seek(_pos), #[cfg(feature = "symphonia")] - DecoderImpl::Symphonia(source, PhantomData) => source.try_seek(pos), + DecoderImpl::Symphonia(source, PhantomData) => source.try_seek(_pos), DecoderImpl::None(_, _) => unreachable!(), } }