diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index acfa0ff3..0fdcffdc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: fail-fast: false matrix: os: [macos-latest, windows-latest, ubuntu-latest] - toolchain: ["1.87"] + toolchain: ["1.89"] include: - os: macos-latest MACOS: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 167f87cc..48f72cd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added Opus support via `symphonia-adapter-libopus`. +- Third-party Symphonia codecs can be registered with `DecoderBuilder::with_decoder`. + ## Version [0.22.1] (2026-02-22) ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 677e0090..eb7d4ea9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,6 +124,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -182,6 +184,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "combine" version = "4.6.7" @@ -431,6 +442,24 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" +[[package]] +name = "fdk-aac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb67e142688083cb9afb63f2424203fc98c4e7afb494bf912b60b55513b177e" +dependencies = [ + "fdk-aac-sys", +] + +[[package]] +name = "fdk-aac-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24516d2611506d5cb1833555adc75f6baf9fe2706b9c13e6fc33a6b22c51ca83" +dependencies = [ + "cc", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -505,6 +534,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.1" @@ -612,11 +653,21 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "93f0862381daaec758576dcc22eb7bbf4d7efd67328553f3b45a412a51a3fb21" dependencies = [ "once_cell", "wasm-bindgen", @@ -955,6 +1006,16 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "opusic-sys" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f43c183739dc81651487e7c9f3e4330fe4a3fd26c0fa961c788ce5fb21fa75b" +dependencies = [ + "cmake", + "libc", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -1192,6 +1253,8 @@ dependencies = [ "rstest_reuse", "rtrb", "symphonia", + "symphonia-adapter-fdk-aac", + "symphonia-adapter-libopus", "thiserror 2.0.18", "tracing", ] @@ -1436,6 +1499,28 @@ dependencies = [ "symphonia-metadata", ] +[[package]] +name = "symphonia-adapter-fdk-aac" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82194865c2b438a75a2b40a12495d5d1a6c3cb2bb36671f9921ab44756ee77d2" +dependencies = [ + "fdk-aac", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-adapter-libopus" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb56e67fc0f362731bc7a2920562ebb5668281b202444a0d274ddd3b5af61e6" +dependencies = [ + "log", + "opusic-sys", + "symphonia-core", +] + [[package]] name = "symphonia-bundle-flac" version = "0.5.5" @@ -1825,9 +1910,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "1de241cdc66a9d91bd84f097039eb140cdc6eec47e0cdbaf9d932a1dd6c35866" dependencies = [ "cfg-if", "once_cell", @@ -1838,9 +1923,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "a42e96ea38f49b191e08a1bab66c7ffdba24b06f9995b39a9dd60222e5b6f1da" dependencies = [ "cfg-if", "futures-util", @@ -1852,9 +1937,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "e12fdf6649048f2e3de6d7d5ff3ced779cdedee0e0baffd7dff5cdfa3abc8a52" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1862,9 +1947,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "0e63d1795c565ac3462334c1e396fd46dbf481c40f51f5072c310717bc4fb309" dependencies = [ "bumpalo", "proc-macro2", @@ -1875,9 +1960,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "e9f9cdac23a5ce71f6bf9f8824898a501e511892791ea2a0c6b8568c68b9cb53" dependencies = [ "unicode-ident", ] @@ -1918,9 +2003,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "f2c7c5718134e770ee62af3b6b4a84518ec10101aad610c024b64d6ff29bb1ff" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 0f7ff62b..13da284c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/RustAudio/rodio" documentation = "https://docs.rs/rodio" exclude = ["assets/**", "tests/**"] edition = "2021" -rust-version = "1.87" +rust-version = "1.89" [features] # Default feature set provides audio playback and common format support @@ -50,6 +50,9 @@ noise = ["rand", "rand_distr"] # Enable WebAssembly support for web browsers wasm-bindgen = ["cpal/wasm-bindgen"] +# Base symphonia feature (doesn't enable any codecs) +symphonia = ["dep:symphonia"] + # To decode an audio source with Rodio, you need to enable the appropriate features for *both* the # demuxer and the decoder. # @@ -96,6 +99,9 @@ symphonia-wav = ["symphonia/wav"] # Enable SIMD optimisations for Symphonia symphonia-simd = ["symphonia/opt-simd"] +# libopus adapter for Symphonia +symphonia-libopus = ["symphonia", "dep:symphonia-adapter-libopus"] + # Alternative decoders and demuxers claxon = ["dep:claxon"] # FLAC hound = ["dep:hound"] # WAV @@ -126,6 +132,8 @@ atomic_float = { version = "1.1.0", optional = true } rtrb = { version = "0.3.2", optional = true } num-rational = "0.4.2" +symphonia-adapter-libopus = { version = "0.2", optional = true } + [dev-dependencies] quickcheck = "1" rstest = "0.26" @@ -133,6 +141,7 @@ rstest_reuse = "0.7" approx = "0.5.1" divan = "0.1.14" inquire = "0.9.3" +symphonia-adapter-fdk-aac = "0.1" [[bench]] name = "effects" @@ -230,6 +239,10 @@ required-features = ["playback", "vorbis"] name = "music_wav" required-features = ["playback", "wav"] +[[example]] +name = "music_opus" +required-features = ["playback", "symphonia-libopus"] + [[example]] name = "noise_generator" required-features = ["playback", "noise"] @@ -253,3 +266,7 @@ required-features = ["playback", "vorbis"] [[example]] name = "stereo" required-features = ["playback", "vorbis"] + +[[example]] +name = "third_party_codec" +required-features = ["playback", "symphonia", "symphonia-isomp4"] diff --git a/assets/music.opus b/assets/music.opus new file mode 100644 index 00000000..4c8e8224 Binary files /dev/null and b/assets/music.opus differ diff --git a/examples/music_opus.rs b/examples/music_opus.rs new file mode 100644 index 00000000..f0f4d725 --- /dev/null +++ b/examples/music_opus.rs @@ -0,0 +1,13 @@ +use std::error::Error; + +fn main() -> Result<(), Box> { + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; + let player = rodio::Player::connect_new(stream_handle.mixer()); + + let file = std::fs::File::open("assets/music.opus")?; + player.append(rodio::Decoder::try_from(file)?); + + player.sleep_until_end(); + + Ok(()) +} diff --git a/examples/third_party_codec.rs b/examples/third_party_codec.rs new file mode 100644 index 00000000..4ed7f6ea --- /dev/null +++ b/examples/third_party_codec.rs @@ -0,0 +1,23 @@ +use rodio::decoder::DecoderBuilder; +use std::error::Error; +use symphonia_adapter_fdk_aac::AacDecoder; + +fn main() -> Result<(), Box> { + let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()?; + let sink = rodio::Player::connect_new(stream_handle.mixer()); + + let file = std::fs::File::open("assets/music.m4a")?; + let len = file.metadata()?.len(); + let decoder = DecoderBuilder::new() + .with_data(file) + // Note: the length must be known for Symphonia to properly detect the format for this file + // This limitation will be removed in Symphonia 0.6 + .with_byte_len(len) + .with_decoder::() + .build()?; + sink.append(decoder); + + sink.sleep_until_end(); + + Ok(()) +} diff --git a/flake.nix b/flake.nix index c3b7b2d9..43397cbc 100644 --- a/flake.nix +++ b/flake.nix @@ -8,7 +8,7 @@ inputs: inputs.utils.lib.eachDefaultSystem ( system: let pkgs = inputs.nixpkgs.legacyPackages.${system}.extend inputs.rust-overlay.overlays.default; - rust = pkgs.rust-bin.stable."1.87.0".default.override { + rust = pkgs.rust-bin.stable."1.89.0".default.override { extensions = [ "rust-src" "rust-analyzer" ]; }; in { diff --git a/src/decoder/builder.rs b/src/decoder/builder.rs index 8d6dc247..7897d239 100644 --- a/src/decoder/builder.rs +++ b/src/decoder/builder.rs @@ -39,10 +39,18 @@ use std::io::{Read, Seek}; +#[cfg(feature = "symphonia")] +use crate::decoder::symphonia::Registry; + #[cfg(feature = "symphonia")] use self::read_seek_source::ReadSeekSource; #[cfg(feature = "symphonia")] -use ::symphonia::core::io::{MediaSource, MediaSourceStream}; +use ::symphonia::core::{ + codecs::CodecRegistry, + io::{MediaSource, MediaSourceStream}, +}; +#[cfg(feature = "symphonia")] +use ::symphonia::default::register_enabled_codecs; use super::*; @@ -77,6 +85,9 @@ pub struct Settings { /// Whether the decoder should report as seekable. pub(crate) is_seekable: bool, + + #[cfg(feature = "symphonia")] + pub(crate) codec_registry: Registry, } impl Default for Settings { @@ -88,6 +99,14 @@ impl Default for Settings { hint: None, mime_type: None, is_seekable: false, + #[cfg(feature = "symphonia")] + codec_registry: { + let mut codec_registry = CodecRegistry::new(); + register_enabled_codecs(&mut codec_registry); + #[cfg(feature = "symphonia-libopus")] + codec_registry.register_all::(); + Registry::new(codec_registry) + }, } } } @@ -231,6 +250,19 @@ impl DecoderBuilder { self } + /// Adds a third-party decoder. + /// If multiple decoders for the same codec are registered, the one registered last will take precedence. + /// + /// See the documentation for [CodecRegistry] for more info. + #[cfg(feature = "symphonia")] + pub fn with_decoder(self) -> Self + where + D: ::symphonia::core::codecs::Decoder, + { + self.settings.codec_registry.write().register_all::(); + self + } + /// Configure whether the data supports random access seeking. Without this, /// only forward seeking may work. /// diff --git a/src/decoder/symphonia.rs b/src/decoder/symphonia.rs index fa98f307..0774c31d 100644 --- a/src/decoder/symphonia.rs +++ b/src/decoder/symphonia.rs @@ -1,9 +1,12 @@ use core::time::Duration; -use std::sync::Arc; +use std::{ + fmt::{self, Debug}, + sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}, +}; use symphonia::{ core::{ audio::{AudioBufferRef, SampleBuffer, SignalSpec}, - codecs::{Decoder, DecoderOptions, CODEC_TYPE_NULL}, + codecs::{CodecRegistry, Decoder, DecoderOptions, CODEC_TYPE_NULL}, errors::Error, formats::{FormatOptions, FormatReader, SeekMode, SeekTo, SeekedTo}, io::MediaSourceStream, @@ -20,6 +23,29 @@ use crate::{ source, Source, }; +#[derive(Clone)] +pub(crate) struct Registry(Arc>); + +impl Debug for Registry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Registry") + } +} + +impl Registry { + pub(crate) fn new(registry: CodecRegistry) -> Self { + Self(Arc::new(RwLock::new(registry))) + } + + pub(crate) fn write(&self) -> RwLockWriteGuard<'_, CodecRegistry> { + self.0.write().unwrap() + } + + pub(crate) fn read(&self) -> RwLockReadGuard<'_, CodecRegistry> { + self.0.read().unwrap() + } +} + pub(crate) struct SymphoniaDecoder { decoder: Box, current_span_offset: usize, @@ -102,9 +128,12 @@ impl SymphoniaDecoder { None => return Ok(None), }; - let mut decoder = symphonia::default::get_codecs() + let mut decoder = settings + .codec_registry + .read() .make(&track.codec_params, &DecoderOptions::default())?; - let total_duration: Option = track + + let total_duration = track .codec_params .time_base .zip(stream.codec_params.n_frames) diff --git a/src/lib.rs b/src/lib.rs index 27bda4ae..92e840cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -118,6 +118,9 @@ //! or enabling specific codecs using one of the `symphonia-{codec name}` features. //! By default, decoders for the most common file types (flac, mp3, mp4, vorbis, wav) are enabled. //! +//! See the [symphonia docs](https://docs.rs/symphonia/latest/symphonia) for the complete +//! list of supported formats. +//! //! ### Alternative Decoders //! //! Alternative decoder libraries are available for some filetypes: