diff --git a/.licensure.yml b/.licensure.yml
index 2d15c450..2197e8c9 100644
--- a/.licensure.yml
+++ b/.licensure.yml
@@ -10,6 +10,12 @@ excludes:
- Cargo.toml
- .*\.yaml
- .*\.wav
+ - .*\.mp3
+ - .*\.flac
+ - .*\.ogg
+ - .*\.aac
+ - .*\.alac
+ - .*\.m4a
- .*\.mid
- lcov.info
- .*\.tosc
diff --git a/Cargo.lock b/Cargo.lock
index f07f16da..0786c292 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -95,6 +95,12 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
[[package]]
name = "async-stream"
version = "0.3.6"
@@ -253,6 +259,12 @@ version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+[[package]]
+name = "bytemuck"
+version = "1.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
+
[[package]]
name = "byteorder"
version = "1.5.0"
@@ -626,6 +638,12 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "extended"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
+
[[package]]
name = "fastrand"
version = "2.3.0"
@@ -1111,6 +1129,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
[[package]]
name = "matchit"
version = "0.7.3"
@@ -1200,6 +1227,7 @@ dependencies = [
"serde_yml",
"shh",
"spin_sleep",
+ "symphonia",
"tempfile",
"thiserror 2.0.17",
"tokio",
@@ -2104,6 +2132,201 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+[[package]]
+name = "symphonia"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039"
+dependencies = [
+ "lazy_static",
+ "symphonia-bundle-flac",
+ "symphonia-bundle-mp3",
+ "symphonia-codec-aac",
+ "symphonia-codec-adpcm",
+ "symphonia-codec-alac",
+ "symphonia-codec-pcm",
+ "symphonia-codec-vorbis",
+ "symphonia-core",
+ "symphonia-format-caf",
+ "symphonia-format-isomp4",
+ "symphonia-format-mkv",
+ "symphonia-format-ogg",
+ "symphonia-format-riff",
+ "symphonia-metadata",
+]
+
+[[package]]
+name = "symphonia-bundle-flac"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976"
+dependencies = [
+ "log",
+ "symphonia-core",
+ "symphonia-metadata",
+ "symphonia-utils-xiph",
+]
+
+[[package]]
+name = "symphonia-bundle-mp3"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed"
+dependencies = [
+ "lazy_static",
+ "log",
+ "symphonia-core",
+ "symphonia-metadata",
+]
+
+[[package]]
+name = "symphonia-codec-aac"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790"
+dependencies = [
+ "lazy_static",
+ "log",
+ "symphonia-core",
+]
+
+[[package]]
+name = "symphonia-codec-adpcm"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dddc50e2bbea4cfe027441eece77c46b9f319748605ab8f3443350129ddd07f"
+dependencies = [
+ "log",
+ "symphonia-core",
+]
+
+[[package]]
+name = "symphonia-codec-alac"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8413fa754942ac16a73634c9dfd1500ed5c61430956b33728567f667fdd393ab"
+dependencies = [
+ "log",
+ "symphonia-core",
+]
+
+[[package]]
+name = "symphonia-codec-pcm"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95"
+dependencies = [
+ "log",
+ "symphonia-core",
+]
+
+[[package]]
+name = "symphonia-codec-vorbis"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73"
+dependencies = [
+ "log",
+ "symphonia-core",
+ "symphonia-utils-xiph",
+]
+
+[[package]]
+name = "symphonia-core"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af"
+dependencies = [
+ "arrayvec",
+ "bitflags 1.3.2",
+ "bytemuck",
+ "lazy_static",
+ "log",
+]
+
+[[package]]
+name = "symphonia-format-caf"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8faf379316b6b6e6bbc274d00e7a592e0d63ff1a7e182ce8ba25e24edd3d096"
+dependencies = [
+ "log",
+ "symphonia-core",
+ "symphonia-metadata",
+]
+
+[[package]]
+name = "symphonia-format-isomp4"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5"
+dependencies = [
+ "encoding_rs",
+ "log",
+ "symphonia-core",
+ "symphonia-metadata",
+ "symphonia-utils-xiph",
+]
+
+[[package]]
+name = "symphonia-format-mkv"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "122d786d2c43a49beb6f397551b4a050d8229eaa54c7ddf9ee4b98899b8742d0"
+dependencies = [
+ "lazy_static",
+ "log",
+ "symphonia-core",
+ "symphonia-metadata",
+ "symphonia-utils-xiph",
+]
+
+[[package]]
+name = "symphonia-format-ogg"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb"
+dependencies = [
+ "log",
+ "symphonia-core",
+ "symphonia-metadata",
+ "symphonia-utils-xiph",
+]
+
+[[package]]
+name = "symphonia-format-riff"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f"
+dependencies = [
+ "extended",
+ "log",
+ "symphonia-core",
+ "symphonia-metadata",
+]
+
+[[package]]
+name = "symphonia-metadata"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16"
+dependencies = [
+ "encoding_rs",
+ "lazy_static",
+ "log",
+ "symphonia-core",
+]
+
+[[package]]
+name = "symphonia-utils-xiph"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16"
+dependencies = [
+ "symphonia-core",
+ "symphonia-metadata",
+]
+
[[package]]
name = "syn"
version = "1.0.109"
@@ -2448,10 +2671,14 @@ version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [
+ "matchers",
"nu-ansi-term",
+ "once_cell",
+ "regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
+ "tracing",
"tracing-core",
"tracing-log",
]
diff --git a/Cargo.toml b/Cargo.toml
index c3a921be..19534ec7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,7 +25,7 @@ cpal = "0.15.3"
rayon = "1.8.0"
num_cpus = "1.16.0"
duration-string = "0.5.2"
-hound = "3.5.1"
+symphonia = { version = "0.5", features = ["all"] }
midir = "0.10.1"
midly = "0.5.3"
nodi = { version = "1.0.3", features = ["hybrid-sleep", "midir"] }
@@ -50,13 +50,14 @@ tokio = { version = "1.42.0", features = [
tonic = "0.12.3"
tonic-reflection = "0.12.3"
tracing = "0.1.41"
-tracing-subscriber = "0.3.19"
+tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
crossbeam-channel = "0.5.15"
pest = "2.7"
pest_derive = "2.7"
[dev-dependencies]
tempfile = "3.14.0"
+hound = "3.5.1"
[build-dependencies]
prost-build = "0.13.4"
diff --git a/README.md b/README.md
index c3c8ee93..240621d5 100644
--- a/README.md
+++ b/README.md
@@ -81,10 +81,25 @@ MIDI device, you would use the string `UltraLite-mk5`.
## File formats
+### Configuration files
+
`mtrack` now uses [config-rs](https://github.com/rust-cli/config-rs) for configuration parsing, which
means we should support any of the configuration file formats that it supports. Testing for anything
other than YAML is limited at the moment.
+### Audio files
+
+`mtrack` supports a wide variety of audio formats through the [symphonia](https://github.com/pdeljanov/Symphonia) library. Supported formats include:
+
+- **WAV** (PCM, various bit depths)
+- **FLAC** (Free Lossless Audio Codec)
+- **MP3** (MPEG Audio Layer III)
+- **OGG Vorbis**
+- **AAC** (Advanced Audio Coding)
+- **ALAC** (Apple Lossless, in M4A containers)
+
+All audio files are automatically transcoded to match your audio device's configuration (sample rate, bit depth, and format). Files can be mixed and matched within a song - for example, you can use a WAV file for your click track and an MP3 file for your backing track.
+
## Structure of an mtrack repository and supporting files
### Song repository
@@ -163,6 +178,7 @@ tracks:
file: Backing Tracks.wav
file_channel: 2
# Our keys file has two channels, but we're only interested in one.
+# Note: You can use any supported audio format (WAV, MP3, FLAC, OGG, AAC, ALAC, etc.)
- name: keys
file: Keys.wav
file_channel: 1
@@ -205,7 +221,7 @@ $ mtrack songs --init /mnt/song-storage
```
This will create a file called `song.yaml` in each subfolder of `/mnt/storage`. The name of the
-subfolder determines the song's name. WAV files are used as tracks. The track's name is
+subfolder determines the song's name. Audio files (WAV, MP3, FLAC, OGG, AAC, ALAC, etc.) are used as tracks. The track's name is
determined using the file name and the number of channels within the file. MIDI files are used as
MIDI playback, MIDI files that start with `dmx_` will be used as light shows. You can edit the generated files to refine the settings to your needs.
@@ -247,12 +263,11 @@ audio:
# Run `mtrack devices` to see a list of the devices that mtrack recognizes.
device: UltraLite-mk5
- # (Optional) The buffer size to use for background reads. Defaults to 1024 samples.
+ # (Optional) The buffer size for decoded audio samples. This controls how many samples
+ # per channel are buffered internally before being returned. Larger values reduce I/O
+ # operations but use more memory. Defaults to 1024 samples per channel.
buffer_size: 1024
- # (Optional) The threshold for triggering background reads. Defaults to 256 samples.
- buffer_threshold: 256
-
# (Optional) The sample rate to use for the audio device. Defaults to 44100.
sample_rate: 44100
@@ -768,7 +783,7 @@ lighting:
- file: "lighting/outro.light" # Multiple shows can be referenced
tracks:
- name: "backing-track"
- file: "backing-track.wav"
+ file: "backing-track.wav" # Can be WAV, MP3, FLAC, OGG, AAC, ALAC, etc.
```
The `.light` files use the DSL format and can reference logical groups defined in your `mtrack.yaml`:
@@ -1415,6 +1430,7 @@ DMX is expected to be well supported through OLA, but the devices that have been
- Entec DMX USB Pro
- RatPac Satellite (Art-Net and sACN)
+- Cinelex Skycast A (sACN)
### General disclaimer
diff --git a/assets/1Channel44.1k.aac b/assets/1Channel44.1k.aac
new file mode 100644
index 00000000..8dc33ce1
Binary files /dev/null and b/assets/1Channel44.1k.aac differ
diff --git a/assets/1Channel44.1k.flac b/assets/1Channel44.1k.flac
new file mode 100644
index 00000000..6ac12905
Binary files /dev/null and b/assets/1Channel44.1k.flac differ
diff --git a/assets/1Channel44.1k.mp3 b/assets/1Channel44.1k.mp3
new file mode 100644
index 00000000..b7f7d55b
Binary files /dev/null and b/assets/1Channel44.1k.mp3 differ
diff --git a/assets/1Channel44.1k.ogg b/assets/1Channel44.1k.ogg
new file mode 100644
index 00000000..28e2be6a
Binary files /dev/null and b/assets/1Channel44.1k.ogg differ
diff --git a/assets/1Channel44.1k_alac.m4a b/assets/1Channel44.1k_alac.m4a
new file mode 100644
index 00000000..9276fa12
Binary files /dev/null and b/assets/1Channel44.1k_alac.m4a differ
diff --git a/assets/2Channel44.1k_alac.m4a b/assets/2Channel44.1k_alac.m4a
new file mode 100644
index 00000000..670642e6
Binary files /dev/null and b/assets/2Channel44.1k_alac.m4a differ
diff --git a/examples/songs/a-really-cool-song/backing-track.mp3 b/examples/songs/a-really-cool-song/backing-track.mp3
new file mode 100644
index 00000000..1364df20
Binary files /dev/null and b/examples/songs/a-really-cool-song/backing-track.mp3 differ
diff --git a/examples/songs/a-really-cool-song/backing-track.wav b/examples/songs/a-really-cool-song/backing-track.wav
deleted file mode 100644
index 8f415804..00000000
Binary files a/examples/songs/a-really-cool-song/backing-track.wav and /dev/null differ
diff --git a/examples/songs/a-really-cool-song/song.yaml b/examples/songs/a-really-cool-song/song.yaml
index 05935c77..2909ecfc 100755
--- a/examples/songs/a-really-cool-song/song.yaml
+++ b/examples/songs/a-really-cool-song/song.yaml
@@ -109,8 +109,8 @@ tracks:
- name: click
file: click.wav
- name: backing-track-l
- file: backing-track.wav
+ file: backing-track.mp3 # MP3 format example
file_channel: 1
- name: backing-track-r
- file: backing-track.wav
+ file: backing-track.mp3 # MP3 format example
file_channel: 2
\ No newline at end of file
diff --git a/examples/songs/a-really-fast-one/backing-track.aac b/examples/songs/a-really-fast-one/backing-track.aac
new file mode 100644
index 00000000..998161e3
Binary files /dev/null and b/examples/songs/a-really-fast-one/backing-track.aac differ
diff --git a/examples/songs/a-really-fast-one/backing-track.wav b/examples/songs/a-really-fast-one/backing-track.wav
deleted file mode 100644
index 8f415804..00000000
Binary files a/examples/songs/a-really-fast-one/backing-track.wav and /dev/null differ
diff --git a/examples/songs/a-really-fast-one/song.yaml b/examples/songs/a-really-fast-one/song.yaml
index bed88cd2..82be1928 100755
--- a/examples/songs/a-really-fast-one/song.yaml
+++ b/examples/songs/a-really-fast-one/song.yaml
@@ -9,8 +9,8 @@ tracks:
- name: click
file: click.wav
- name: backing-track-l
- file: backing-track.wav
+ file: backing-track.aac # AAC format example
file_channel: 1
- name: backing-track-r
- file: backing-track.wav
+ file: backing-track.aac # AAC format example
file_channel: 2
\ No newline at end of file
diff --git a/examples/songs/another-cool-song/backing-track.flac b/examples/songs/another-cool-song/backing-track.flac
new file mode 100644
index 00000000..a2cfcf8f
Binary files /dev/null and b/examples/songs/another-cool-song/backing-track.flac differ
diff --git a/examples/songs/another-cool-song/backing-track.wav b/examples/songs/another-cool-song/backing-track.wav
deleted file mode 100644
index 8f415804..00000000
Binary files a/examples/songs/another-cool-song/backing-track.wav and /dev/null differ
diff --git a/examples/songs/another-cool-song/song.yaml b/examples/songs/another-cool-song/song.yaml
index b6935412..377fee4f 100755
--- a/examples/songs/another-cool-song/song.yaml
+++ b/examples/songs/another-cool-song/song.yaml
@@ -20,8 +20,8 @@ tracks:
- name: click
file: click.wav
- name: backing-track-l
- file: backing-track.wav
+ file: backing-track.flac # FLAC format example
file_channel: 1
- name: backing-track-r
- file: backing-track.wav
+ file: backing-track.flac # FLAC format example
file_channel: 2
\ No newline at end of file
diff --git a/examples/songs/dsl-light-show-song/song.mid b/examples/songs/dsl-light-show-song/song.mid
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/songs/dsl-light-show-song/song.mp3 b/examples/songs/dsl-light-show-song/song.mp3
new file mode 100644
index 00000000..1364df20
Binary files /dev/null and b/examples/songs/dsl-light-show-song/song.mp3 differ
diff --git a/examples/songs/dsl-light-show-song/song.yaml b/examples/songs/dsl-light-show-song/song.yaml
index 50dd6024..f61c5097 100644
--- a/examples/songs/dsl-light-show-song/song.yaml
+++ b/examples/songs/dsl-light-show-song/song.yaml
@@ -7,4 +7,5 @@ lighting:
file: "lighting/outro.light"
tracks:
- name: "Main Track"
- audio_file: "song.wav"
+ file: "song.mp3" # MP3 format example - can use WAV, MP3, FLAC, OGG, AAC, ALAC, etc.
+ file_channel: 1
diff --git a/examples/songs/outro-tape/backing-track.m4a b/examples/songs/outro-tape/backing-track.m4a
new file mode 100644
index 00000000..670642e6
Binary files /dev/null and b/examples/songs/outro-tape/backing-track.m4a differ
diff --git a/examples/songs/outro-tape/backing-track.wav b/examples/songs/outro-tape/backing-track.wav
deleted file mode 100644
index 8f415804..00000000
Binary files a/examples/songs/outro-tape/backing-track.wav and /dev/null differ
diff --git a/examples/songs/outro-tape/song.yaml b/examples/songs/outro-tape/song.yaml
index dc039893..a33c3efe 100755
--- a/examples/songs/outro-tape/song.yaml
+++ b/examples/songs/outro-tape/song.yaml
@@ -7,8 +7,8 @@ midi_event:
tracks:
- name: backing-track-l
- file: backing-track.wav
+ file: backing-track.m4a # ALAC/Apple Lossless format example (M4A container)
file_channel: 1
- name: backing-track-r
- file: backing-track.wav
+ file: backing-track.m4a # ALAC/Apple Lossless format example (M4A container)
file_channel: 2
\ No newline at end of file
diff --git a/examples/songs/sound-check/backing-track.flac b/examples/songs/sound-check/backing-track.flac
new file mode 100644
index 00000000..a2cfcf8f
Binary files /dev/null and b/examples/songs/sound-check/backing-track.flac differ
diff --git a/examples/songs/sound-check/backing-track.wav b/examples/songs/sound-check/backing-track.wav
deleted file mode 100644
index 8f415804..00000000
Binary files a/examples/songs/sound-check/backing-track.wav and /dev/null differ
diff --git a/examples/songs/sound-check/song.yaml b/examples/songs/sound-check/song.yaml
index 324fc736..8d098c6d 100755
--- a/examples/songs/sound-check/song.yaml
+++ b/examples/songs/sound-check/song.yaml
@@ -7,8 +7,8 @@ midi_event:
tracks:
- name: backing-track-l
- file: backing-track.wav
+ file: backing-track.flac # FLAC format example
file_channel: 1
- name: backing-track-r
- file: backing-track.wav
+ file: backing-track.flac # FLAC format example
file_channel: 2
\ No newline at end of file
diff --git a/examples/songs/the-slow-one/backing-track.ogg b/examples/songs/the-slow-one/backing-track.ogg
new file mode 100644
index 00000000..eea7b457
Binary files /dev/null and b/examples/songs/the-slow-one/backing-track.ogg differ
diff --git a/examples/songs/the-slow-one/backing-track.wav b/examples/songs/the-slow-one/backing-track.wav
deleted file mode 100644
index 8f415804..00000000
Binary files a/examples/songs/the-slow-one/backing-track.wav and /dev/null differ
diff --git a/examples/songs/the-slow-one/song.yaml b/examples/songs/the-slow-one/song.yaml
index c16db74a..e5ec45e6 100755
--- a/examples/songs/the-slow-one/song.yaml
+++ b/examples/songs/the-slow-one/song.yaml
@@ -57,8 +57,8 @@ tracks:
- name: click
file: click.wav
- name: backing-track-l
- file: backing-track.wav
+ file: backing-track.ogg # OGG Vorbis format example
file_channel: 1
- name: backing-track-r
- file: backing-track.wav
+ file: backing-track.ogg # OGG Vorbis format example
file_channel: 2
\ No newline at end of file
diff --git a/src/audio/cpal.rs b/src/audio/cpal.rs
index 0221fa65..1f6f3a9f 100644
--- a/src/audio/cpal.rs
+++ b/src/audio/cpal.rs
@@ -338,7 +338,7 @@ impl OutputManager {
};
// Create the output stream based on the target format
- // Map hound::SampleFormat to the appropriate CPAL stream type
+ // Map SampleFormat to the appropriate CPAL stream type
let stream_result = if target_format.sample_format == crate::audio::SampleFormat::Float
{
@@ -575,7 +575,6 @@ impl AudioDevice for Device {
mappings,
self.target_format.clone(),
self.audio_config.buffer_size(),
- self.audio_config.buffer_threshold(),
)?;
// Add all sources to the output manager
diff --git a/src/audio/sample_source.rs b/src/audio/sample_source.rs
index 54d88b47..02391ced 100644
--- a/src/audio/sample_source.rs
+++ b/src/audio/sample_source.rs
@@ -11,13 +11,13 @@
// You should have received a copy of the GNU General Public License along with
// this program. If not, see .
//
+pub mod audio;
pub mod channel_mapped;
pub mod error;
pub mod factory;
pub mod memory;
pub mod traits;
pub mod transcoder;
-pub mod wav;
#[cfg(test)]
mod tests;
@@ -26,11 +26,8 @@ mod tests;
pub use channel_mapped::create_channel_mapped_sample_source;
#[cfg(test)]
pub use channel_mapped::ChannelMappedSource;
-pub use factory::{create_sample_source_from_file, create_sample_source_from_file_with_seek};
-pub use traits::{ChannelMappedSampleSource, SampleSource};
-pub use wav::WavSampleSource;
+pub use factory::create_sample_source_from_file;
+pub use traits::ChannelMappedSampleSource;
#[cfg(test)]
pub use memory::MemorySampleSource;
-#[cfg(test)]
-pub use traits::SampleSourceTestExt;
diff --git a/src/audio/sample_source/audio.rs b/src/audio/sample_source/audio.rs
new file mode 100644
index 00000000..007fabe8
--- /dev/null
+++ b/src/audio/sample_source/audio.rs
@@ -0,0 +1,549 @@
+// Copyright (C) 2026 Michael Wilson
+//
+// This program is free software: you can redistribute it and/or modify it under
+// the terms of the GNU General Public License as published by the Free Software
+// Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program. If not, see .
+//
+use std::fs::File;
+use std::path::Path;
+
+use symphonia::core::audio::{AudioBuffer, AudioBufferRef, Signal};
+use symphonia::core::codecs::{DecoderOptions, CODEC_TYPE_NULL};
+use symphonia::core::errors::Error as SymphoniaError;
+use symphonia::core::formats::{FormatOptions, FormatReader, SeekMode, SeekTo};
+use symphonia::core::io::MediaSourceStream;
+use symphonia::core::meta::MetadataOptions;
+use symphonia::core::probe::Hint;
+use symphonia::default::get_codecs;
+use symphonia::default::get_probe;
+
+use super::error::SampleSourceError;
+use super::traits::SampleSource;
+
+#[cfg(test)]
+use super::traits::SampleSourceTestExt;
+
+/// A sample source that reads audio files (WAV, MP3, FLAC, etc.) and provides scaled samples
+/// This uses symphonia to decode various audio formats - no transcoding logic
+pub struct AudioSampleSource {
+ format_reader: Box,
+ decoder: Box,
+ track_id: u32,
+ is_finished: bool,
+ // Buffered reading to reduce I/O operations
+ sample_buffer: Vec,
+ buffer_position: usize,
+ buffer_size: usize,
+ // Leftover samples from the last decoded packet that didn't fit in the buffer
+ leftover_samples: Vec,
+ // WAV / PCM metadata for scaling & reporting
+ bits_per_sample: u16,
+ channels: u16,
+ sample_rate: u32,
+ sample_format: crate::audio::SampleFormat,
+ duration: std::time::Duration,
+}
+
+impl SampleSource for AudioSampleSource {
+ fn next_sample(&mut self) -> Result