From bf1241e0a572c7fb5722d0a4e2fa84fe056a3e31 Mon Sep 17 00:00:00 2001 From: rm-dr <96270320+rm-dr@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:19:41 -0800 Subject: [PATCH] `pile-audio` refactor --- Cargo.lock | 216 ++-- Cargo.toml | 32 +- crates/pile-audio/src/common/mod.rs | 5 - crates/pile-audio/src/common/vorbiscomment.rs | 355 ------- crates/pile-audio/src/flac/blockread.rs | 867 ---------------- crates/pile-audio/src/flac/mod.rs | 943 ------------------ crates/pile-audio/src/flac/proc/metastrip.rs | 213 ---- crates/pile-audio/src/flac/proc/mod.rs | 5 - crates/pile-audio/src/flac/proc/pictures.rs | 229 ----- crates/pile-audio/src/lib.rs | 3 - crates/pile-audio/src/nodes/extractcovers.rs | 112 --- crates/pile-audio/src/nodes/extracttags.rs | 164 --- crates/pile-audio/src/nodes/mod.rs | 36 - crates/pile-audio/src/nodes/striptags.rs | 181 ---- crates/pile-dataset/Cargo.toml | 3 +- crates/pile-dataset/src/item/flac.rs | 69 +- crates/{pile-audio => pile-flac}/Cargo.toml | 3 +- .../src}/blocks/application.rs | 2 +- .../src}/blocks/audiodata.rs | 3 +- .../flac => pile-flac/src}/blocks/comment.rs | 6 +- .../flac => pile-flac/src}/blocks/cuesheet.rs | 3 +- .../flac => pile-flac/src}/blocks/header.rs | 2 +- .../src/flac => pile-flac/src}/blocks/mod.rs | 12 +- .../flac => pile-flac/src}/blocks/padding.rs | 3 +- .../flac => pile-flac/src}/blocks/picture.rs | 13 +- .../src}/blocks/seektable.rs | 3 +- .../src}/blocks/streaminfo.rs | 3 +- .../src/flac => pile-flac/src}/errors.rs | 40 +- crates/pile-flac/src/lib.rs | 19 + .../common => pile-flac/src}/picturetype.rs | 22 +- crates/pile-flac/src/reader/block.rs | 63 ++ crates/pile-flac/src/reader/mod.rs | 9 + crates/pile-flac/src/reader/reader.rs | 184 ++++ crates/pile-flac/src/reader/tests.rs | 393 ++++++++ .../src/common => pile-flac/src}/tagtype.rs | 38 + crates/pile-flac/src/tests.rs | 919 +++++++++++++++++ crates/pile-flac/src/vorbiscomment.rs | 194 ++++ .../tests/files/README.md | 0 .../files/flac_custom/01 - many images.flac | Bin .../02 - picture in vorbis comment.flac | Bin ...03 - faulty picture in vorbis comment.flac | Bin .../tests/files/flac_custom/LICENSE.txt | 0 .../tests/files/flac_custom/README.md | 0 .../flac_faulty/01 - wrong max blocksize.flac | Bin .../02 - wrong maximum framesize.flac | Bin .../flac_faulty/03 - wrong bit depth.flac | Bin .../04 - wrong number of channels.flac | Bin .../05 - wrong total number of samples.flac | Bin ...6 - missing streaminfo metadata block.flac | Bin ...s preceding streaminfo metadata block.flac | Bin .../flac_faulty/08 - blocksize 65536.flac | Bin .../files/flac_faulty/09 - blocksize 1.flac | Bin ...invalid vorbis comment metadata block.flac | Bin .../11 - incorrect metadata block length.flac | Bin .../tests/files/flac_faulty/LICENSE.txt | 0 .../tests/files/flac_faulty/README.md | 0 .../flac_subset/01 - blocksize 4096.flac | Bin .../flac_subset/02 - blocksize 4608.flac | Bin .../files/flac_subset/03 - blocksize 16.flac | Bin .../files/flac_subset/04 - blocksize 192.flac | Bin .../files/flac_subset/05 - blocksize 254.flac | Bin .../files/flac_subset/06 - blocksize 512.flac | Bin .../files/flac_subset/07 - blocksize 725.flac | Bin .../flac_subset/08 - blocksize 1000.flac | Bin .../flac_subset/09 - blocksize 1937.flac | Bin .../flac_subset/10 - blocksize 2304.flac | Bin .../flac_subset/11 - partition order 8.flac | Bin .../12 - qlp precision 15 bit.flac | Bin .../flac_subset/13 - qlp precision 2 bit.flac | Bin .../files/flac_subset/14 - wasted bits.flac | Bin .../15 - only verbatim subframes.flac | Bin ...order 8 containing escaped partitions.flac | Bin .../flac_subset/17 - all fixed orders.flac | Bin .../flac_subset/18 - precision search.flac | Bin .../flac_subset/19 - samplerate 35467Hz.flac | Bin .../flac_subset/20 - samplerate 39kHz.flac | Bin .../flac_subset/21 - samplerate 22050Hz.flac | Bin .../flac_subset/22 - 12 bit per sample.flac | Bin .../flac_subset/23 - 8 bit per sample.flac | Bin ... file created with flake revision 264.flac | Bin ...64, modified to create smaller blocks.flac | Bin ...ile created with CUETools.Flake 2.1.6.flac | Bin ...locksize file created with Flake 0.11.flac | Bin ...gh resolution audio, default settings.flac | Bin ...igh resolution audio, blocksize 16384.flac | Bin ...igh resolution audio, blocksize 13456.flac | Bin ...dio, using only 32nd order predictors.flac | Bin ...order 8 containing escaped partitions.flac | Bin .../flac_subset/33 - samplerate 192kHz.flac | Bin ...kHz, using only 32nd order predictors.flac | Bin .../flac_subset/35 - samplerate 134560Hz.flac | Bin .../flac_subset/36 - samplerate 384kHz.flac | Bin .../flac_subset/37 - 20 bit per sample.flac | Bin .../flac_subset/38 - 3 channels (3.0).flac | Bin .../flac_subset/39 - 4 channels (4.0).flac | Bin .../flac_subset/40 - 5 channels (5.0).flac | Bin .../flac_subset/41 - 6 channels (5.1).flac | Bin .../flac_subset/42 - 7 channels (6.1).flac | Bin .../flac_subset/43 - 8 channels (7.1).flac | Bin ...bit, using only 32nd order predictors.flac | Bin .../45 - no total number of samples set.flac | Bin .../46 - no min-max framesize set.flac | Bin .../flac_subset/47 - only STREAMINFO.flac | Bin .../48 - Extremely large SEEKTABLE.flac | Bin .../49 - Extremely large PADDING.flac | Bin .../50 - Extremely large PICTURE.flac | Bin .../51 - Extremely large VORBISCOMMENT.flac | Bin .../52 - Extremely large APPLICATION.flac | Bin .../53 - CUESHEET with very many indexes.flac | Bin .../54 - 1000x repeating VORBISCOMMENT.flac | Bin .../flac_subset/55 - file 48-53 combined.flac | Bin .../files/flac_subset/56 - JPG PICTURE.flac | Bin .../files/flac_subset/57 - PNG PICTURE.flac | Bin .../files/flac_subset/58 - GIF PICTURE.flac | Bin .../files/flac_subset/59 - AVIF PICTURE.flac | Bin .../files/flac_subset/60 - mono audio.flac | Bin ...61 - predictor overflow check, 16-bit.flac | Bin ...62 - predictor overflow check, 20-bit.flac | Bin ...63 - predictor overflow check, 24-bit.flac | Bin ...rice partitions with escape code zero.flac | Bin .../tests/files/flac_subset/LICENSE.txt | 0 .../tests/files/flac_subset/README.md | 0 .../01 - changing samplerate.flac | Bin .../02 - increasing number of channels.flac | Bin .../03 - decreasing number of channels.flac | Bin .../flac_uncommon/04 - changing bitdepth.flac | Bin .../files/flac_uncommon/05 - 32bps audio.flac | Bin .../flac_uncommon/06 - samplerate 768kHz.flac | Bin .../flac_uncommon/07 - 15 bit per sample.flac | Bin .../flac_uncommon/08 - blocksize 65535.flac | Bin .../09 - Rice partition order 15.flac | Bin .../10 - file starting at frame header.flac | Bin ... - file starting with unparsable data.flac | Bin .../tests/files/flac_uncommon/LICENSE.txt | 0 .../tests/files/flac_uncommon/README.md | 0 crates/pile/src/config/logging.rs | 14 +- 136 files changed, 1991 insertions(+), 3390 deletions(-) delete mode 100644 crates/pile-audio/src/common/mod.rs delete mode 100644 crates/pile-audio/src/common/vorbiscomment.rs delete mode 100644 crates/pile-audio/src/flac/blockread.rs delete mode 100644 crates/pile-audio/src/flac/mod.rs delete mode 100644 crates/pile-audio/src/flac/proc/metastrip.rs delete mode 100644 crates/pile-audio/src/flac/proc/mod.rs delete mode 100644 crates/pile-audio/src/flac/proc/pictures.rs delete mode 100644 crates/pile-audio/src/lib.rs delete mode 100644 crates/pile-audio/src/nodes/extractcovers.rs delete mode 100644 crates/pile-audio/src/nodes/extracttags.rs delete mode 100644 crates/pile-audio/src/nodes/mod.rs delete mode 100644 crates/pile-audio/src/nodes/striptags.rs rename crates/{pile-audio => pile-flac}/Cargo.toml (89%) rename crates/{pile-audio/src/flac => pile-flac/src}/blocks/application.rs (96%) rename crates/{pile-audio/src/flac => pile-flac/src}/blocks/audiodata.rs (93%) rename crates/{pile-audio/src/flac => pile-flac/src}/blocks/comment.rs (91%) rename crates/{pile-audio/src/flac => pile-flac/src}/blocks/cuesheet.rs (94%) rename crates/{pile-audio/src/flac => pile-flac/src}/blocks/header.rs (97%) rename crates/{pile-audio/src/flac => pile-flac/src}/blocks/mod.rs (81%) rename crates/{pile-audio/src/flac => pile-flac/src}/blocks/padding.rs (94%) rename crates/{pile-audio/src/flac => pile-flac/src}/blocks/picture.rs (95%) rename crates/{pile-audio/src/flac => pile-flac/src}/blocks/seektable.rs (94%) rename crates/{pile-audio/src/flac => pile-flac/src}/blocks/streaminfo.rs (98%) rename crates/{pile-audio/src/flac => pile-flac/src}/errors.rs (53%) create mode 100644 crates/pile-flac/src/lib.rs rename crates/{pile-audio/src/common => pile-flac/src}/picturetype.rs (82%) create mode 100644 crates/pile-flac/src/reader/block.rs create mode 100644 crates/pile-flac/src/reader/mod.rs create mode 100644 crates/pile-flac/src/reader/reader.rs create mode 100644 crates/pile-flac/src/reader/tests.rs rename crates/{pile-audio/src/common => pile-flac/src}/tagtype.rs (56%) create mode 100644 crates/pile-flac/src/tests.rs create mode 100644 crates/pile-flac/src/vorbiscomment.rs rename crates/{pile-audio => pile-flac}/tests/files/README.md (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_custom/01 - many images.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_custom/02 - picture in vorbis comment.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_custom/03 - faulty picture in vorbis comment.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_custom/LICENSE.txt (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_custom/README.md (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_faulty/01 - wrong max blocksize.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_faulty/02 - wrong maximum framesize.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_faulty/03 - wrong bit depth.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_faulty/04 - wrong number of channels.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_faulty/05 - wrong total number of samples.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_faulty/06 - missing streaminfo metadata block.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_faulty/07 - other metadata blocks preceding streaminfo metadata block.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_faulty/08 - blocksize 65536.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_faulty/09 - blocksize 1.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_faulty/10 - invalid vorbis comment metadata block.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_faulty/11 - incorrect metadata block length.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_faulty/LICENSE.txt (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_faulty/README.md (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/01 - blocksize 4096.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/02 - blocksize 4608.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/03 - blocksize 16.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/04 - blocksize 192.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/05 - blocksize 254.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/06 - blocksize 512.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/07 - blocksize 725.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/08 - blocksize 1000.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/09 - blocksize 1937.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/10 - blocksize 2304.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/11 - partition order 8.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/12 - qlp precision 15 bit.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/13 - qlp precision 2 bit.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/14 - wasted bits.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/15 - only verbatim subframes.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/16 - partition order 8 containing escaped partitions.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/17 - all fixed orders.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/18 - precision search.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/19 - samplerate 35467Hz.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/20 - samplerate 39kHz.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/21 - samplerate 22050Hz.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/22 - 12 bit per sample.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/23 - 8 bit per sample.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/24 - variable blocksize file created with flake revision 264.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/25 - variable blocksize file created with flake revision 264, modified to create smaller blocks.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/26 - variable blocksize file created with CUETools.Flake 2.1.6.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/27 - old format variable blocksize file created with Flake 0.11.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/28 - high resolution audio, default settings.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/29 - high resolution audio, blocksize 16384.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/30 - high resolution audio, blocksize 13456.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/31 - high resolution audio, using only 32nd order predictors.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/32 - high resolution audio, partition order 8 containing escaped partitions.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/33 - samplerate 192kHz.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/34 - samplerate 192kHz, using only 32nd order predictors.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/35 - samplerate 134560Hz.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/36 - samplerate 384kHz.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/37 - 20 bit per sample.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/38 - 3 channels (3.0).flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/39 - 4 channels (4.0).flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/40 - 5 channels (5.0).flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/41 - 6 channels (5.1).flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/42 - 7 channels (6.1).flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/43 - 8 channels (7.1).flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/44 - 8-channel surround, 192kHz, 24 bit, using only 32nd order predictors.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/45 - no total number of samples set.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/46 - no min-max framesize set.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/47 - only STREAMINFO.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/48 - Extremely large SEEKTABLE.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/49 - Extremely large PADDING.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/50 - Extremely large PICTURE.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/51 - Extremely large VORBISCOMMENT.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/52 - Extremely large APPLICATION.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/53 - CUESHEET with very many indexes.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/54 - 1000x repeating VORBISCOMMENT.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/55 - file 48-53 combined.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/56 - JPG PICTURE.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/57 - PNG PICTURE.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/58 - GIF PICTURE.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/59 - AVIF PICTURE.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/60 - mono audio.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/61 - predictor overflow check, 16-bit.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/62 - predictor overflow check, 20-bit.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/63 - predictor overflow check, 24-bit.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/64 - rice partitions with escape code zero.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/LICENSE.txt (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_subset/README.md (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_uncommon/01 - changing samplerate.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_uncommon/02 - increasing number of channels.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_uncommon/03 - decreasing number of channels.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_uncommon/04 - changing bitdepth.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_uncommon/05 - 32bps audio.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_uncommon/06 - samplerate 768kHz.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_uncommon/07 - 15 bit per sample.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_uncommon/08 - blocksize 65535.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_uncommon/09 - Rice partition order 15.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_uncommon/10 - file starting at frame header.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_uncommon/11 - file starting with unparsable data.flac (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_uncommon/LICENSE.txt (100%) rename crates/{pile-audio => pile-flac}/tests/files/flac_uncommon/README.md (100%) diff --git a/Cargo.lock b/Cargo.lock index cefcf80..f3a62d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arc-swap" @@ -122,9 +122,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bitpacking" @@ -235,9 +235,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.53" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -245,9 +245,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -257,9 +257,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", @@ -269,9 +269,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "colorchoice" @@ -294,9 +294,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dabb6555f92fb9ee4140454eb5dcd14c7960e1225c6d1a6cc361f032947713e" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "core-foundation-sys" @@ -374,9 +374,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.0-rc.9" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b8986f836d4aeb30ccf4c9d3bd562fd716074cfd7fc4a2948359fbd21ed809" +checksum = "211f05e03c7d03754740fd9e585de910a095d6b99f8bcfffdef8319fa02a8331" dependencies = [ "hybrid-array", ] @@ -437,13 +437,13 @@ dependencies = [ [[package]] name = "digest" -version = "0.11.0-rc.5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebf9423bafb058e4142194330c52273c343f8a5beb7176d052f0e73b17dd35b9" +checksum = "f8bf3682cdec91817be507e4aa104314898b95b84d74f3d43882210101a545b6" dependencies = [ "block-buffer 0.11.0", "const-oid", - "crypto-common 0.2.0-rc.9", + "crypto-common 0.2.0", ] [[package]] @@ -477,7 +477,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -633,9 +633,9 @@ checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" [[package]] name = "hybrid-array" -version = "0.4.5" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f471e0a81b2f90ffc0cb2f951ae04da57de8baa46fa99112b062a5173a5088d0" +checksum = "e1b229d73f5803b562cc26e4da0396c8610a4ee209f4fac8fa4f8d709166dc45" dependencies = [ "typenum", ] @@ -687,9 +687,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", @@ -699,9 +699,9 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.18.3" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" dependencies = [ "console", "portable-atomic", @@ -729,9 +729,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jobserver" @@ -786,9 +786,9 @@ checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libm" @@ -852,9 +852,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" @@ -910,7 +910,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1048,21 +1048,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "pile-audio" -version = "0.0.1" -dependencies = [ - "base64", - "itertools", - "mime", - "paste", - "rand 0.9.2", - "sha2 0.11.0-rc.3", - "smartstring", - "strum", - "thiserror", -] - [[package]] name = "pile-config" version = "0.0.1" @@ -1081,8 +1066,8 @@ dependencies = [ "chrono", "itertools", "jsonpath-rust", - "pile-audio", "pile-config", + "pile-flac", "pile-toolbox", "serde_json", "tantivy", @@ -1092,6 +1077,20 @@ dependencies = [ "walkdir", ] +[[package]] +name = "pile-flac" +version = "0.0.1" +dependencies = [ + "base64", + "itertools", + "mime", + "paste", + "sha2 0.11.0-rc.5", + "smartstring", + "strum", + "thiserror", +] + [[package]] name = "pile-toolbox" version = "0.0.1" @@ -1114,9 +1113,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "powerfmt" @@ -1145,18 +1144,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -1174,18 +1173,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_chacha", + "rand_core", ] [[package]] @@ -1195,17 +1184,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", + "rand_core", ] [[package]] @@ -1217,15 +1196,6 @@ dependencies = [ "getrandom 0.2.17", ] -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.4", -] - [[package]] name = "rand_distr" version = "0.4.3" @@ -1233,7 +1203,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" dependencies = [ "num-traits", - "rand 0.8.5", + "rand", ] [[package]] @@ -1320,7 +1290,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1382,9 +1352,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.147" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af14725505314343e673e9ecb7cd7e8a36aa9791eb936235a3567cc31447ae4" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", @@ -1415,13 +1385,13 @@ dependencies = [ [[package]] name = "sha2" -version = "0.11.0-rc.3" +version = "0.11.0-rc.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d43dc0354d88b791216bb5c1bfbb60c0814460cc653ae0ebd71f286d0bd927" +checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.11.0-rc.5", + "digest 0.11.0", ] [[package]] @@ -1441,9 +1411,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" -version = "0.3.18" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +checksum = "3b57709da74f9ff9f4a27dce9526eec25ca8407c45a7887243b031a58935fb8e" dependencies = [ "libc", "signal-hook-registry", @@ -1493,9 +1463,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", "windows-sys 0.60.2", @@ -1542,9 +1512,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1704,26 +1674,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -1772,9 +1742,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", @@ -1800,9 +1770,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.10+spec-1.1.0" +version = "1.0.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" +checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" dependencies = [ "indexmap", "serde_core", @@ -1815,18 +1785,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.0.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow", ] @@ -1937,9 +1907,9 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" @@ -2167,7 +2137,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2493,18 +2463,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "fdea86ddd5568519879b8187e1cf04e24fce28f7fe046ceecbce472ff19a2572" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "0c15e1b46eff7c6c91195752e0eeed8ef040e391cdece7c25376957d5f15df22" dependencies = [ "proc-macro2", "quote", @@ -2513,9 +2483,9 @@ dependencies = [ [[package]] name = "zmij" -version = "0.1.9" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0095ecd462946aa3927d9297b63ef82fb9a5316d7a37d134eeb36e58228615a" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zstd" diff --git a/Cargo.toml b/Cargo.toml index efcd09f..2f24117 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,37 +65,37 @@ type_complexity = "allow" [workspace.dependencies] pile-toolbox = { path = "crates/pile-toolbox" } pile-config = { path = "crates/pile-config" } -pile-audio = { path = "crates/pile-audio" } +pile-flac = { path = "crates/pile-flac" } pile-dataset = { path = "crates/pile-dataset" } # Clients tantivy = "0.25.0" # Async & Parallelism -tokio = { version = "1.44.1", features = ["full"] } +tokio = { version = "1.49.0", features = ["full"] } # CLI & logging -tracing = "0.1.41" -tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] } -indicatif = { version = "0.18.0", features = ["improved_unicode"] } -tracing-indicatif = "0.3.13" -anstyle = "1.0.10" -clap = { version = "4.5.37", features = ["derive"] } +tracing = "0.1.44" +tracing-subscriber = { version = "0.3.22", features = ["env-filter", "json"] } +indicatif = { version = "0.18.4", features = ["improved_unicode"] } +tracing-indicatif = "0.3.14" +anstyle = "1.0.13" +clap = { version = "4.5.60", features = ["derive"] } # Serialization & formats -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.140" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" base64 = "0.22.1" -toml = "0.9.8" +toml = "1.0.3" jsonpath-rust = "1.0.4" -sha2 = "0.11.0-rc.3" +sha2 = "0.11.0-rc.5" # Misc helpers -thiserror = "2.0.12" -anyhow = "1.0.97" +thiserror = "2.0.18" +anyhow = "1.0.102" itertools = "0.14.0" -signal-hook = "0.3.18" -rand = "0.9.2" +signal-hook = "0.4.3" +rand = "0.10.0" strum = { version = "0.27.2", features = ["derive"] } walkdir = "2.5.0" mime = "0.3.17" diff --git a/crates/pile-audio/src/common/mod.rs b/crates/pile-audio/src/common/mod.rs deleted file mode 100644 index 7782dcf..0000000 --- a/crates/pile-audio/src/common/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Components shared between many different formats - -pub mod picturetype; -pub mod tagtype; -pub mod vorbiscomment; diff --git a/crates/pile-audio/src/common/vorbiscomment.rs b/crates/pile-audio/src/common/vorbiscomment.rs deleted file mode 100644 index 5370354..0000000 --- a/crates/pile-audio/src/common/vorbiscomment.rs +++ /dev/null @@ -1,355 +0,0 @@ -//! Decode and write Vorbis comment blocks - -use base64::Engine; -use smartstring::{LazyCompact, SmartString}; -use std::{ - fmt::Display, - io::{Cursor, Read, Write}, - string::FromUtf8Error, -}; - -use crate::flac::blocks::{FlacMetablockDecode, FlacMetablockEncode, FlacPictureBlock}; - -use super::tagtype::TagType; - -#[derive(Debug)] -#[expect(missing_docs)] -pub enum VorbisCommentDecodeError { - /// We encountered an IoError while processing a block - IoError(std::io::Error), - - /// We tried to decode a string, but got invalid data - FailedStringDecode(FromUtf8Error), - - /// The given comment string isn't within spec - MalformedCommentString(String), - - /// The comment we're reading is invalid - MalformedData, - - /// We tried to decode picture data, but it was malformed. - MalformedPicture, -} - -impl Display for VorbisCommentDecodeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::IoError(_) => write!(f, "io error while reading vorbis comments"), - Self::FailedStringDecode(_) => { - write!(f, "string decode error while reading vorbis comments") - } - Self::MalformedCommentString(x) => { - write!(f, "malformed comment string `{x}`") - } - Self::MalformedData => { - write!(f, "malformed comment data") - } - Self::MalformedPicture => { - write!(f, "malformed picture data") - } - } - } -} - -impl std::error::Error for VorbisCommentDecodeError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::IoError(x) => Some(x), - Self::FailedStringDecode(x) => Some(x), - _ => None, - } - } -} - -impl From for VorbisCommentDecodeError { - fn from(value: std::io::Error) -> Self { - Self::IoError(value) - } -} - -impl From for VorbisCommentDecodeError { - fn from(value: FromUtf8Error) -> Self { - Self::FailedStringDecode(value) - } -} - -#[derive(Debug)] -#[expect(missing_docs)] -pub enum VorbisCommentEncodeError { - /// We encountered an IoError while processing a block - IoError(std::io::Error), - - /// We could not encode picture data - PictureEncodeError, -} - -impl Display for VorbisCommentEncodeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::IoError(_) => write!(f, "io error while reading vorbis comments"), - Self::PictureEncodeError => { - write!(f, "could not encode picture") - } - } - } -} - -impl std::error::Error for VorbisCommentEncodeError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::IoError(x) => Some(x), - _ => None, - } - } -} - -impl From for VorbisCommentEncodeError { - fn from(value: std::io::Error) -> Self { - Self::IoError(value) - } -} - -/// A decoded vorbis comment block -#[derive(Debug)] -pub struct VorbisComment { - /// This comment's vendor string - pub vendor: SmartString, - - /// List of (tag, value) - /// Repeated tags are allowed! - pub comments: Vec<(TagType, SmartString)>, - - /// A list of pictures found in this comment - pub pictures: Vec, -} - -impl VorbisComment { - /// Try to decode the given data as a vorbis comment block - pub fn decode(data: &[u8]) -> Result { - let mut d = Cursor::new(data); - - // This is re-used whenever we need to read four bytes - let mut block = [0u8; 4]; - - let vendor = { - d.read_exact(&mut block) - .map_err(|_err| VorbisCommentDecodeError::MalformedData)?; - - let length = u32::from_le_bytes(block); - - #[expect(clippy::expect_used)] - let mut text = vec![ - 0u8; - length - .try_into() - .expect("vendor length does not fit into usize") - ]; - - d.read_exact(&mut text) - .map_err(|_err| VorbisCommentDecodeError::MalformedData)?; - - String::from_utf8(text)? - }; - - d.read_exact(&mut block) - .map_err(|_err| VorbisCommentDecodeError::MalformedData)?; - - #[expect(clippy::expect_used)] - let n_comments: usize = u32::from_le_bytes(block) - .try_into() - .expect("comment count does not fit into usize"); - - let mut comments = Vec::new(); - let mut pictures = Vec::new(); - for _ in 0..n_comments { - let comment = { - d.read_exact(&mut block) - .map_err(|_err| VorbisCommentDecodeError::MalformedData)?; - - let length = u32::from_le_bytes(block); - - #[expect(clippy::expect_used)] - let mut text = vec![ - 0u8; - length - .try_into() - .expect("comment length does not fit into usize") - ]; - - d.read_exact(&mut text) - .map_err(|_err| VorbisCommentDecodeError::MalformedData)?; - - String::from_utf8(text)? - }; - let (var, val) = - comment - .split_once('=') - .ok_or(VorbisCommentDecodeError::MalformedCommentString( - comment.clone(), - ))?; - - if !val.is_empty() { - if var.to_uppercase() == "METADATA_BLOCK_PICTURE" { - #[expect(clippy::map_err_ignore)] - pictures.push( - FlacPictureBlock::decode( - &base64::prelude::BASE64_STANDARD - .decode(val) - .map_err(|_| VorbisCommentDecodeError::MalformedPicture)?, - ) - .map_err(|_| VorbisCommentDecodeError::MalformedPicture)?, - ); - } else { - // Make sure empty strings are saved as "None" - comments.push(( - match &var.to_uppercase()[..] { - "TITLE" => TagType::TrackTitle, - "ALBUM" => TagType::Album, - "TRACKNUMBER" => TagType::TrackNumber, - "ARTIST" => TagType::TrackArtist, - "ALBUMARTIST" => TagType::AlbumArtist, - "GENRE" => TagType::Genre, - "ISRC" => TagType::Isrc, - "DATE" => TagType::ReleaseDate, - "TOTALTRACKS" => TagType::TrackTotal, - "LYRICS" => TagType::Lyrics, - x => TagType::Other(x.into()), - }, - val.into(), - )); - } - }; - } - - Ok(Self { - vendor: vendor.into(), - comments, - pictures, - }) - } -} - -impl VorbisComment { - /// Get the number of bytes that `encode()` will write. - #[expect(clippy::expect_used)] - pub fn get_len(&self) -> u32 { - let mut sum: u32 = 0; - sum += u32::try_from(self.vendor.len()).expect("vendor length does not fit into u32") + 4; - sum += 4; - - for (tagtype, value) in &self.comments { - let tagtype_str = match tagtype { - TagType::TrackTitle => "TITLE", - TagType::Album => "ALBUM", - TagType::TrackNumber => "TRACKNUMBER", - TagType::TrackArtist => "ARTIST", - TagType::AlbumArtist => "ALBUMARTIST", - TagType::Genre => "GENRE", - TagType::Isrc => "ISRC", - TagType::ReleaseDate => "DATE", - TagType::TrackTotal => "TOTALTRACKS", - TagType::Lyrics => "LYRICS", - TagType::Comment => "COMMENT", - TagType::DiskNumber => "DISKNUMBER", - TagType::DiskTotal => "DISKTOTAL", - TagType::Year => "YEAR", - TagType::Other(x) => x, - } - .to_uppercase(); - - let str = format!("{tagtype_str}={value}"); - sum += - 4 + u32::try_from(str.len()).expect("comment string length does not fit into u32"); - } - - for p in &self.pictures { - // Compute b64 len - let mut x = p.get_len(); - if x % 3 != 0 { - x -= x % 3; - x += 3; - } - - #[expect(clippy::integer_division)] - { - sum += 4 * (x / 3); - } - - // Add "METADATA_BLOCK_PICTURE=" - sum += 23; - - // Add length bytes - sum += 4; - } - - return sum; - } - - /// Try to encode this vorbis comment - #[expect(clippy::expect_used)] - pub fn encode(&self, target: &mut impl Write) -> Result<(), VorbisCommentEncodeError> { - target.write_all( - &u32::try_from(self.vendor.len()) - .expect("vendor length does not fit into u32") - .to_le_bytes(), - )?; - target.write_all(self.vendor.as_bytes())?; - - target.write_all( - &u32::try_from(self.comments.len() + self.pictures.len()) - .expect("total comment count does not fit into u32") - .to_le_bytes(), - )?; - - for (tagtype, value) in &self.comments { - let tagtype_str = match tagtype { - TagType::TrackTitle => "TITLE", - TagType::Album => "ALBUM", - TagType::TrackNumber => "TRACKNUMBER", - TagType::TrackArtist => "ARTIST", - TagType::AlbumArtist => "ALBUMARTIST", - TagType::Genre => "GENRE", - TagType::Isrc => "ISRC", - TagType::ReleaseDate => "DATE", - TagType::TrackTotal => "TOTALTRACKS", - TagType::Lyrics => "LYRICS", - TagType::Comment => "COMMENT", - TagType::DiskNumber => "DISKNUMBER", - TagType::DiskTotal => "DISKTOTAL", - TagType::Year => "YEAR", - TagType::Other(x) => x, - } - .to_uppercase(); - - let str = format!("{tagtype_str}={value}"); - target.write_all( - &u32::try_from(str.len()) - .expect("comment string length does not fit into u32") - .to_le_bytes(), - )?; - target.write_all(str.as_bytes())?; - } - - for p in &self.pictures { - let mut pic_data = Vec::new(); - - #[expect(clippy::map_err_ignore)] - p.encode(false, false, &mut pic_data) - .map_err(|_| VorbisCommentEncodeError::PictureEncodeError)?; - - let pic_string = format!( - "METADATA_BLOCK_PICTURE={}", - &base64::prelude::BASE64_STANDARD.encode(&pic_data) - ); - - target.write_all( - &u32::try_from(pic_string.len()) - .expect("picture string length does not fit into u32") - .to_le_bytes(), - )?; - target.write_all(pic_string.as_bytes())?; - } - - return Ok(()); - } -} diff --git a/crates/pile-audio/src/flac/blockread.rs b/crates/pile-audio/src/flac/blockread.rs deleted file mode 100644 index aeec046..0000000 --- a/crates/pile-audio/src/flac/blockread.rs +++ /dev/null @@ -1,867 +0,0 @@ -//! Strip metadata from a FLAC file without loading the whole thing into memory. - -use std::{ - collections::VecDeque, - io::{Cursor, Read, Seek, Write}, -}; -use thiserror::Error; - -use super::{ - blocks::{ - FlacAudioFrame, FlacCommentBlock, FlacMetablockDecode, FlacMetablockEncode, - FlacMetablockHeader, FlacMetablockType, - }, - errors::{FlacDecodeError, FlacEncodeError}, -}; -use crate::flac::blocks::{ - FlacApplicationBlock, FlacCuesheetBlock, FlacPaddingBlock, FlacPictureBlock, - FlacSeektableBlock, FlacStreaminfoBlock, -}; - -const MIN_AUDIO_FRAME_LEN: usize = 5000; - -/// Select which blocks we want to keep. -/// All values are `false` by default. -#[derive(Debug, Default, Clone, Copy)] -pub struct FlacBlockSelector { - /// Select `FlacMetablockType::Streaminfo` blocks. - pub pick_streaminfo: bool, - - /// Select `FlacMetablockType::Padding` blocks. - pub pick_padding: bool, - - /// Select `FlacMetablockType::Application` blocks. - pub pick_application: bool, - - /// Select `FlacMetablockType::SeekTable` blocks. - pub pick_seektable: bool, - - /// Select `FlacMetablockType::VorbisComment` blocks. - pub pick_vorbiscomment: bool, - - /// Select `FlacMetablockType::CueSheet` blocks. - pub pick_cuesheet: bool, - - /// Select `FlacMetablockType::Picture` blocks. - pub pick_picture: bool, - - /// Select audio frames. - pub pick_audio: bool, -} - -impl FlacBlockSelector { - /// Make a new [`FlacBlockSelector`] - pub fn new() -> Self { - Self::default() - } - - fn should_pick_meta(&self, block_type: FlacMetablockType) -> bool { - match block_type { - FlacMetablockType::Streaminfo => self.pick_streaminfo, - FlacMetablockType::Padding => self.pick_padding, - FlacMetablockType::Application => self.pick_application, - FlacMetablockType::Seektable => self.pick_seektable, - FlacMetablockType::VorbisComment => self.pick_vorbiscomment, - FlacMetablockType::Cuesheet => self.pick_cuesheet, - FlacMetablockType::Picture => self.pick_picture, - } - } -} - -enum FlacBlockType { - MagicBits { - data: [u8; 4], - left_to_read: usize, - }, - MetablockHeader { - is_first: bool, - data: [u8; 4], - left_to_read: usize, - }, - MetaBlock { - header: FlacMetablockHeader, - data: Vec, - }, - AudioData { - data: Vec, - }, -} - -#[derive(Debug)] -#[expect(missing_docs)] -pub enum FlacBlock { - Streaminfo(FlacStreaminfoBlock), - Picture(FlacPictureBlock), - Padding(FlacPaddingBlock), - Application(FlacApplicationBlock), - SeekTable(FlacSeektableBlock), - VorbisComment(FlacCommentBlock), - CueSheet(FlacCuesheetBlock), - AudioFrame(FlacAudioFrame), -} - -impl FlacBlock { - /// Encode this block - pub fn encode( - &self, - is_last: bool, - with_header: bool, - target: &mut impl Write, - ) -> Result<(), FlacEncodeError> { - match self { - Self::Streaminfo(b) => b.encode(is_last, with_header, target), - Self::SeekTable(b) => b.encode(is_last, with_header, target), - Self::Picture(b) => b.encode(is_last, with_header, target), - Self::Padding(b) => b.encode(is_last, with_header, target), - Self::Application(b) => b.encode(is_last, with_header, target), - Self::VorbisComment(b) => b.encode(is_last, with_header, target), - Self::CueSheet(b) => b.encode(is_last, with_header, target), - Self::AudioFrame(b) => b.encode(target), - } - } - - /// Try to decode the given data as a block - pub fn decode(block_type: FlacMetablockType, data: &[u8]) -> Result { - Ok(match block_type { - FlacMetablockType::Streaminfo => { - FlacBlock::Streaminfo(FlacStreaminfoBlock::decode(data)?) - } - FlacMetablockType::Application => { - FlacBlock::Application(FlacApplicationBlock::decode(data)?) - } - FlacMetablockType::Cuesheet => FlacBlock::CueSheet(FlacCuesheetBlock::decode(data)?), - FlacMetablockType::Padding => FlacBlock::Padding(FlacPaddingBlock::decode(data)?), - FlacMetablockType::Picture => FlacBlock::Picture(FlacPictureBlock::decode(data)?), - FlacMetablockType::Seektable => FlacBlock::SeekTable(FlacSeektableBlock::decode(data)?), - FlacMetablockType::VorbisComment => { - FlacBlock::VorbisComment(FlacCommentBlock::decode(data)?) - } - }) - } -} - -/// An error produced by a [`FlacBlockReader`] -#[derive(Debug, Error)] -pub enum FlacBlockReaderError { - /// Could not decode flac data - #[error("decode error while reading flac blocks")] - DecodeError(#[from] FlacDecodeError), - - /// Tried to finish or push data to a finished reader. - #[error("flac block reader is already finished")] - AlreadyFinished, -} - -/// A buffered flac block reader. -/// Use `push_data` to add flac data into this struct, -/// use `pop_block` to read flac blocks. -/// -/// This is the foundation of all other flac processors -/// we offer in this crate. -pub struct FlacBlockReader { - // Which blocks should we return? - selector: FlacBlockSelector, - - // The block we're currently reading. - // If this is `None`, we've called `finish()`. - current_block: Option, - - // Blocks we pick go here - output_blocks: VecDeque, -} - -impl FlacBlockReader { - /// Pop the next block we've read, if any. - pub fn pop_block(&mut self) -> Option { - self.output_blocks.pop_front() - } - - /// If true, this reader has received all the data it needs. - pub fn is_done(&self) -> bool { - self.current_block.is_none() - } - - /// If true, this reader has at least one block ready to pop. - /// Calling `pop_block` will return `Some(_)` if this is true. - pub fn has_block(&self) -> bool { - !self.output_blocks.is_empty() - } - - /// Make a new [`FlacBlockReader`]. - pub fn new(selector: FlacBlockSelector) -> Self { - Self { - selector, - current_block: Some(FlacBlockType::MagicBits { - data: [0; 4], - left_to_read: 4, - }), - - output_blocks: VecDeque::new(), - } - } - - /// Pass the given data through this block extractor. - /// Output data is stored in an internal buffer, and should be accessed - /// through `Read`. - pub fn push_data(&mut self, buf: &[u8]) -> Result<(), FlacBlockReaderError> { - let mut buf = Cursor::new(buf); - let mut last_read_size = 1; - - if self.current_block.is_none() { - return Err(FlacBlockReaderError::AlreadyFinished); - } - - 'outer: while last_read_size != 0 { - #[expect(clippy::expect_used)] - match self - .current_block - .as_mut() - .expect("current_block is Some, checked above") - { - FlacBlockType::MagicBits { data, left_to_read } => { - last_read_size = buf - .read(&mut data[4 - *left_to_read..4]) - .map_err(FlacDecodeError::from)?; - *left_to_read -= last_read_size; - - if *left_to_read == 0 { - if *data != [0x66, 0x4C, 0x61, 0x43] { - return Err(FlacDecodeError::BadMagicBytes.into()); - }; - - self.current_block = Some(FlacBlockType::MetablockHeader { - is_first: true, - data: [0; 4], - left_to_read: 4, - }) - } - } - - FlacBlockType::MetablockHeader { - is_first, - data, - left_to_read, - } => { - last_read_size = buf - .read(&mut data[4 - *left_to_read..4]) - .map_err(FlacDecodeError::from)?; - *left_to_read -= last_read_size; - - if *left_to_read == 0 { - let header = FlacMetablockHeader::decode(data)?; - if *is_first && !matches!(header.block_type, FlacMetablockType::Streaminfo) - { - return Err(FlacDecodeError::BadFirstBlock.into()); - } - - self.current_block = Some(FlacBlockType::MetaBlock { - header, - data: Vec::new(), - }) - } - } - - FlacBlockType::MetaBlock { header, data } => { - #[expect(clippy::expect_used)] - { - last_read_size = buf - .by_ref() - .take( - u64::from(header.length) - - u64::try_from(data.len()) - .expect("data length does not fit into u64"), - ) - .read_to_end(data) - .map_err(FlacDecodeError::from)?; - } - - #[expect(clippy::expect_used)] - if data.len() - == usize::try_from(header.length) - .expect("header length does not fit into usize") - { - // If we picked this block type, add it to the queue - if self.selector.should_pick_meta(header.block_type) { - let b = FlacBlock::decode(header.block_type, data)?; - self.output_blocks.push_back(b); - } - - // Start next block - if header.is_last { - self.current_block = Some(FlacBlockType::AudioData { data: Vec::new() }) - } else { - self.current_block = Some(FlacBlockType::MetablockHeader { - is_first: false, - data: [0; 4], - left_to_read: 4, - }) - } - } - } - - FlacBlockType::AudioData { data } => { - // Limit the number of bytes we read at once, so we don't re-clone - // large amounts of data if `buf` contains multiple sync sequences. - // 5kb is a pretty reasonable frame size. - last_read_size = buf - .by_ref() - .take(5_000) - .read_to_end(data) - .map_err(FlacDecodeError::from)?; - if last_read_size == 0 { - continue 'outer; - } - - // We can't run checks if we don't have enough data. - if data.len() <= 2 { - continue; - } - - // Check frame sync header - // (`if` makes sure we only do this once) - if data.len() - last_read_size <= 2 - && !(data[0] == 0b1111_1111 && data[1] & 0b1111_1100 == 0b1111_1000) - { - return Err(FlacDecodeError::BadSyncBytes.into()); - } - - if data.len() >= MIN_AUDIO_FRAME_LEN { - // Look for a frame sync header in the data we read - // - // This isn't the *correct* way to split audio frames (false sync bytes can occur in audio data), - // but it's good enough for now---we don't decode audio data anyway. - // - // We could split on every sequence of sync bytes, but that's not any less wrong than the approach here. - // Also, it's slower---we'd rather have few large frames than many small ones. - - let first_byte = if data.len() - last_read_size < MIN_AUDIO_FRAME_LEN { - MIN_AUDIO_FRAME_LEN + 1 - } else { - data.len() - last_read_size + MIN_AUDIO_FRAME_LEN + 1 - }; - - // `i` is the index of the first byte *after* the sync sequence. - // - // This may seem odd, but it makes the odd edge case easier to handle: - // If we instead have `i` be the index of the first byte *of* the frame sequence, - // dealing with the case where `data` contained half the sync sequence before - // reading is tricky. - for i in first_byte..data.len() { - if data[i - 2] == 0b1111_1111 - && data[i - 1] & 0b1111_1100 == 0b1111_1000 - { - // We found another frame sync header. Split at this index. - if self.selector.pick_audio { - self.output_blocks.push_back(FlacBlock::AudioFrame( - FlacAudioFrame::decode(&data[0..i - 2])?, - )); - } - - // Backtrack to the first bit AFTER this new sync sequence - buf.seek(std::io::SeekFrom::Current( - -i64::try_from(data.len() - i) - .expect("seek offset does not fit into i64"), - )) - .map_err(FlacDecodeError::from)?; - - self.current_block = Some(FlacBlockType::AudioData { - data: { - let mut v = Vec::with_capacity(MIN_AUDIO_FRAME_LEN); - v.extend(&data[i - 2..i]); - v - }, - }); - continue 'outer; - } - } - } - } - } - } - - return Ok(()); - } - - /// Finish reading data. - /// This tells the reader that it has received the entire stream. - /// - /// `finish()` should be called exactly once once we have finished each stream. - /// Finishing twice or pushing data to a finished reader results in a panic. - pub fn finish(&mut self) -> Result<(), FlacBlockReaderError> { - match self.current_block.take() { - None => return Err(FlacBlockReaderError::AlreadyFinished), - - Some(FlacBlockType::AudioData { data }) => { - // We can't run checks if we don't have enough data. - if data.len() <= 2 { - return Err(FlacDecodeError::MalformedBlock.into()); - } - - if !(data[0] == 0b1111_1111 && data[1] & 0b1111_1100 == 0b1111_1000) { - return Err(FlacDecodeError::BadSyncBytes.into()); - } - - if self.selector.pick_audio { - self.output_blocks - .push_back(FlacBlock::AudioFrame(FlacAudioFrame::decode(&data)?)); - } - - self.current_block = None; - return Ok(()); - } - - // All other blocks have a known length and - // are finished automatically. - _ => return Err(FlacDecodeError::MalformedBlock.into()), - } - } -} - -#[cfg(test)] -mod tests { - use itertools::Itertools; - use paste::paste; - use rand::Rng; - use sha2::{Digest, Sha256}; - use std::{io::Write, ops::Range, str::FromStr}; - - use super::*; - use crate::{ - common::tagtype::TagType, - flac::tests::{FlacBlockOutput, FlacTestCase, VorbisCommentTestValue, manifest}, - }; - - #[expect(clippy::unwrap_used)] - fn read_file( - test_case: &FlacTestCase, - fragment_size_range: Option>, - selector: FlacBlockSelector, - ) -> Result, FlacBlockReaderError> { - let file_data = std::fs::read(test_case.get_path()).unwrap(); - - // Make sure input file is correct - let mut hasher = Sha256::new(); - hasher.update(&file_data); - assert_eq!( - test_case.get_in_hash(), - hasher.finalize().map(|x| format!("{x:02x}")).join("") - ); - - let mut reader = FlacBlockReader::new(selector); - let mut out_blocks = Vec::new(); - - // Push file data to the reader, in parts or as a whole. - if let Some(fragment_size_range) = fragment_size_range { - let mut head = 0; - while head < file_data.len() { - let mut frag_size = rand::rng().random_range(fragment_size_range.clone()); - if head + frag_size > file_data.len() { - frag_size = file_data.len() - head; - } - reader.push_data(&file_data[head..head + frag_size])?; - head += frag_size; - } - } else { - reader.push_data(&file_data)?; - } - - reader.finish()?; - while let Some(b) = reader.pop_block() { - out_blocks.push(b) - } - - return Ok(out_blocks); - } - - #[expect(clippy::unwrap_used)] - fn test_identical( - test_case: &FlacTestCase, - fragment_size_range: Option>, - ) -> Result<(), FlacBlockReaderError> { - let out_blocks = read_file( - test_case, - fragment_size_range, - FlacBlockSelector { - pick_streaminfo: true, - pick_padding: true, - pick_application: true, - pick_seektable: true, - pick_vorbiscomment: true, - pick_cuesheet: true, - pick_picture: true, - pick_audio: true, - }, - )?; - - let mut out = Vec::new(); - out.write_all(&[0x66, 0x4C, 0x61, 0x43]).unwrap(); - - for i in 0..out_blocks.len() { - let b = &out_blocks[i]; - let is_last = if i == out_blocks.len() - 1 { - false - } else { - !matches!(b, FlacBlock::AudioFrame(_)) - && matches!(&out_blocks[i + 1], FlacBlock::AudioFrame(_)) - }; - - b.encode(is_last, true, &mut out).unwrap(); - } - - let mut hasher = Sha256::new(); - hasher.update(&out); - let result = hasher.finalize().map(|x| format!("{x:02x}")).join(""); - assert_eq!(result, test_case.get_in_hash(), "Output hash doesn't match"); - return Ok(()); - } - - fn test_blockread( - test_case: &FlacTestCase, - fragment_size_range: Option>, - ) -> Result<(), FlacBlockReaderError> { - let out_blocks = read_file( - test_case, - fragment_size_range, - FlacBlockSelector { - pick_streaminfo: true, - pick_padding: true, - pick_application: true, - pick_seektable: true, - pick_vorbiscomment: true, - pick_cuesheet: true, - pick_picture: true, - pick_audio: true, - }, - )?; - - assert_eq!( - test_case.get_blocks().unwrap().len(), - out_blocks - .iter() - .filter(|x| !matches!(*x, FlacBlock::AudioFrame(_))) - .count(), - "Number of blocks didn't match" - ); - - let mut audio_data_hasher = Sha256::new(); - let mut result_i = 0; - - for b in out_blocks { - match b { - FlacBlock::Streaminfo(s) => match &test_case.get_blocks().unwrap()[result_i] { - FlacBlockOutput::Streaminfo { - min_block_size, - max_block_size, - min_frame_size, - max_frame_size, - sample_rate, - channels, - bits_per_sample, - total_samples, - md5_signature, - } => { - assert_eq!(*min_block_size, s.min_block_size,); - assert_eq!(*max_block_size, s.max_block_size); - assert_eq!(*min_frame_size, s.min_frame_size); - assert_eq!(*max_frame_size, s.max_frame_size); - assert_eq!(*sample_rate, s.sample_rate); - assert_eq!(*channels, s.channels); - assert_eq!(*bits_per_sample, s.bits_per_sample); - assert_eq!(*total_samples, s.total_samples); - assert_eq!( - *md5_signature, - s.md5_signature.iter().map(|x| format!("{x:02x}")).join("") - ); - } - _ => panic!("Unexpected block type"), - }, - - FlacBlock::Application(a) => match &test_case.get_blocks().unwrap()[result_i] { - FlacBlockOutput::Application { - application_id, - hash, - } => { - assert_eq!( - *application_id, a.application_id, - "Application id doesn't match" - ); - assert_eq!( - *hash, - { - let mut hasher = Sha256::new(); - - hasher.update(&a.data); - hasher.finalize().map(|x| format!("{x:02x}")).join("") - }, - "Application content hash doesn't match" - ); - } - _ => panic!("Unexpected block type"), - }, - - FlacBlock::CueSheet(c) => match &test_case.get_blocks().unwrap()[result_i] { - FlacBlockOutput::CueSheet { hash } => { - assert_eq!(*hash, { - let mut hasher = Sha256::new(); - - hasher.update(&c.data); - hasher.finalize().map(|x| format!("{x:02x}")).join("") - }); - } - _ => panic!("Unexpected block type"), - }, - - FlacBlock::Padding(p) => match &test_case.get_blocks().unwrap()[result_i] { - FlacBlockOutput::Padding { size } => { - assert_eq!(p.size, *size); - } - _ => panic!("Unexpected block type"), - }, - - FlacBlock::SeekTable(t) => match &test_case.get_blocks().unwrap()[result_i] { - FlacBlockOutput::Seektable { hash } => { - assert_eq!(*hash, { - let mut hasher = Sha256::new(); - - hasher.update(&t.data); - hasher.finalize().map(|x| format!("{x:02x}")).join("") - }); - } - _ => panic!("Unexpected block type"), - }, - - FlacBlock::Picture(p) => match &test_case.get_blocks().unwrap()[result_i] { - FlacBlockOutput::Picture { - picture_type, - mime, - description, - width, - height, - bit_depth, - color_count, - img_data, - } => { - assert_eq!(*picture_type, p.picture_type, "{}", test_case.get_name()); - assert_eq!(*mime, p.mime, "{}", test_case.get_name()); - assert_eq!(*description, p.description, "{}", test_case.get_name()); - assert_eq!(*width, p.width, "{}", test_case.get_name()); - assert_eq!(*height, p.height, "{}", test_case.get_name()); - assert_eq!(*bit_depth, p.bit_depth, "{}", test_case.get_name()); - assert_eq!(*color_count, p.color_count, "{}", test_case.get_name()); - assert_eq!( - *img_data, - { - let mut hasher = Sha256::new(); - - hasher.update(&p.img_data); - hasher.finalize().map(|x| format!("{x:02x}")).join("") - }, - "{}", - test_case.get_name() - ); - } - _ => panic!("Unexpected block type"), - }, - - FlacBlock::VorbisComment(v) => match &test_case.get_blocks().unwrap()[result_i] { - FlacBlockOutput::VorbisComment { - vendor, - comments, - pictures, - } => { - assert_eq!(*vendor, v.comment.vendor, "Comment vendor doesn't match"); - - assert_eq!( - v.comment.pictures.len(), - pictures.len(), - "Number of pictures doesn't match" - ); - - for (p, e) in v.comment.pictures.iter().zip(*pictures) { - match e { - FlacBlockOutput::Picture { - picture_type, - mime, - description, - width, - height, - bit_depth, - color_count, - img_data, - } => { - assert_eq!(*picture_type, p.picture_type); - assert_eq!(*mime, p.mime); - assert_eq!(*description, p.description); - assert_eq!(*width, p.width); - assert_eq!(*height, p.height); - assert_eq!(*bit_depth, p.bit_depth); - assert_eq!(*color_count, p.color_count); - assert_eq!(*img_data, { - let mut hasher = Sha256::new(); - hasher.update(&p.img_data); - hasher.finalize().map(|x| format!("{x:02x}")).join("") - }); - } - _ => panic!("Bad test data: expected only Picture blocks."), - } - } - - match comments { - VorbisCommentTestValue::Raw { tags } => { - assert_eq!( - v.comment.comments.len(), - tags.len(), - "Number of comments doesn't match" - ); - - for ((got_tag, got_val), (exp_tag, exp_val)) in - v.comment.comments.iter().zip(*tags) - { - assert_eq!( - *got_tag, - TagType::from_str(exp_tag).unwrap(), - "Tag key doesn't match" - ); - assert_eq!( - got_val, exp_val, - "Tag value of {exp_tag} doesn't match" - ); - } - } - - VorbisCommentTestValue::Hash { n_comments, hash } => { - assert_eq!( - v.comment.comments.len(), - *n_comments, - "Number of comments doesn't match" - ); - - let mut hasher = Sha256::new(); - - for (got_tag, got_val) in v.comment.comments { - hasher.update(format!("{got_tag}={got_val};").as_bytes()); - } - assert_eq!( - &hasher.finalize().map(|x| format!("{x:02x}")).join(""), - hash, - "Comment hash doesn't match" - ); - } - } - } - _ => panic!("Unexpected block type"), - }, - - FlacBlock::AudioFrame(data) => { - let mut vec = Vec::new(); - data.encode(&mut vec).unwrap(); - audio_data_hasher.update(&vec); - - if result_i != test_case.get_blocks().unwrap().len() { - panic!("There are metadata blocks between audio frames!") - } - - // Don't increment result_i - continue; - } - } - - result_i += 1; - } - - // Check audio data hash - assert_eq!( - test_case.get_audio_hash().unwrap(), - audio_data_hasher - .finalize() - .map(|x| format!("{x:02x}")) - .join("") - ); - - return Ok(()); - } - - // Helper macros to generate tests - macro_rules! gen_tests { - ( $test_name:ident ) => { - paste! { - #[test] - pub fn []() { - let manifest = manifest(); - let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap(); - - match test_case { - FlacTestCase::Success { .. } => { - for _ in 0..5 { - test_blockread( - test_case, - Some(1..256), - ).unwrap() - } - }, - - FlacTestCase::Error { check_error, .. } => { - let e = test_blockread(test_case, Some(1..256)).unwrap_err(); - match e { - FlacBlockReaderError::DecodeError(e) => assert!(check_error(&e), "Unexpected error {e:?}"), - _ => panic!("Unexpected error {e:?}") - } - } - } - } - - #[test] - pub fn []() { - let manifest = manifest(); - let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap(); - - match test_case { - FlacTestCase::Success { .. } => { - for _ in 0..5 { - test_identical( - test_case, - Some(1..256), - ).unwrap() - } - }, - - FlacTestCase::Error { check_error, .. } => { - let e = test_identical(test_case, Some(1..256)).unwrap_err(); - match e { - FlacBlockReaderError::DecodeError(e) => assert!(check_error(&e), "Unexpected error {e:?}"), - _ => panic!("Unexpected error {e:?}") - } - } - } - } - } - }; - } - - gen_tests!(custom_01); - gen_tests!(custom_02); - gen_tests!(custom_03); - - gen_tests!(uncommon_10); - - gen_tests!(faulty_06); - gen_tests!(faulty_07); - gen_tests!(faulty_10); - gen_tests!(faulty_11); - - gen_tests!(subset_45); - gen_tests!(subset_46); - gen_tests!(subset_47); - gen_tests!(subset_48); - gen_tests!(subset_49); - gen_tests!(subset_50); - gen_tests!(subset_51); - gen_tests!(subset_52); - gen_tests!(subset_53); - gen_tests!(subset_54); - gen_tests!(subset_55); - gen_tests!(subset_56); - gen_tests!(subset_57); - gen_tests!(subset_58); - gen_tests!(subset_59); -} diff --git a/crates/pile-audio/src/flac/mod.rs b/crates/pile-audio/src/flac/mod.rs deleted file mode 100644 index 1751247..0000000 --- a/crates/pile-audio/src/flac/mod.rs +++ /dev/null @@ -1,943 +0,0 @@ -//! Parse FLAC metadata. - -pub mod blockread; -pub mod blocks; -pub mod errors; -pub mod proc; - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use itertools::Itertools; - use mime::Mime; - - use super::errors::FlacDecodeError; - use crate::common::{picturetype::PictureType, vorbiscomment::VorbisCommentDecodeError}; - - /// The value of a vorbis comment. - /// - /// Some files have VERY large comments, and providing them - /// explicitly here doesn't make sense. - #[derive(Clone)] - pub enum VorbisCommentTestValue { - /// The comments, in order - Raw { - tags: &'static [(&'static str, &'static str)], - }, - /// The hash of all comments concatenated together, - /// stringified as `{key}={value};` - Hash { - n_comments: usize, - hash: &'static str, - }, - } - - #[derive(Clone)] - pub enum FlacBlockOutput { - Application { - application_id: u32, - hash: &'static str, - }, - Streaminfo { - min_block_size: u32, - max_block_size: u32, - min_frame_size: u32, - max_frame_size: u32, - sample_rate: u32, - channels: u8, - bits_per_sample: u8, - total_samples: u128, - md5_signature: &'static str, - }, - CueSheet { - // Hash of this block's data, without the header. - // This is easy to get with - // - // ```notrust - // metaflac \ - // --list \ - // --block-number= \ - // --data-format=binary-headerless \ - // \ - // | sha256sum - //``` - hash: &'static str, - }, - Seektable { - hash: &'static str, - }, - Padding { - size: u32, - }, - Picture { - picture_type: PictureType, - mime: Mime, - description: &'static str, - width: u32, - height: u32, - bit_depth: u32, - color_count: u32, - img_data: &'static str, - }, - VorbisComment { - vendor: &'static str, - comments: VorbisCommentTestValue, - pictures: &'static [FlacBlockOutput], - }, - } - - pub enum FlacTestCase { - Success { - /// This test's name - test_name: &'static str, - - /// The file to use for this test - file_path: &'static str, - - /// The hash of the input files - in_hash: &'static str, - - /// The flac metablocks we expect to find in this file, in order. - blocks: Vec, - - /// The hash of the audio frames in this file - /// - /// Get this hash by running `metaflac --remove-all --dont-use-padding`, - /// then by manually deleting remaining headers in a hex editor - /// (Remember that the sync sequence is 0xFF 0xF8) - audio_hash: &'static str, - - /// The hash we should get when we strip this file's tags. - /// - /// A stripped flac file has unmodified STREAMINFO, SEEKTABLE, - /// CUESHEET, and audio data blocks; and nothing else (not even padding). - /// - /// Reference implementation: - /// ```notrust - /// metaflac \ - /// --remove \ - /// --block-type=PADDING,APPLICATION,VORBIS_COMMENT,PICTURE \ - /// --dont-use-padding \ - /// - /// ``` - stripped_hash: &'static str, - }, - Error { - /// This test's name - test_name: &'static str, - - /// The file to use for this test - file_path: &'static str, - - /// The hash of the input files - in_hash: &'static str, - - /// The error we should encounter while reading this file - check_error: &'static dyn Fn(&FlacDecodeError) -> bool, - - /// If some, stripping this file's metadata should produce the given hash. - /// If none, trying to strip metadata should produce `check_error` - stripped_hash: Option<&'static str>, - - /// If some, the following images should be extracted from this file - /// If none, trying to strip images should produce `check_error` - pictures: Option>, - }, - } - - impl FlacTestCase { - pub fn get_name(&self) -> &str { - match self { - Self::Error { test_name, .. } | Self::Success { test_name, .. } => test_name, - } - } - - pub fn get_path(&self) -> &str { - match self { - Self::Success { file_path, .. } | Self::Error { file_path, .. } => file_path, - } - } - - pub fn get_in_hash(&self) -> &str { - match self { - Self::Success { in_hash, .. } | Self::Error { in_hash, .. } => in_hash, - } - } - - pub fn get_stripped_hash(&self) -> Option<&str> { - match self { - Self::Success { stripped_hash, .. } => Some(stripped_hash), - Self::Error { stripped_hash, .. } => *stripped_hash, - } - } - - pub fn get_audio_hash(&self) -> Option<&str> { - match self { - Self::Success { audio_hash, .. } => Some(audio_hash), - _ => None, - } - } - - pub fn get_blocks(&self) -> Option<&[FlacBlockOutput]> { - match self { - Self::Success { blocks, .. } => Some(blocks), - _ => None, - } - } - - pub fn get_pictures(&self) -> Option> { - match self { - Self::Success { blocks, .. } => { - let mut out = Vec::new(); - for b in blocks { - match b { - FlacBlockOutput::Picture { .. } => out.push(b.clone()), - FlacBlockOutput::VorbisComment { pictures, .. } => { - for p in *pictures { - out.push(p.clone()) - } - } - _ => {} - } - } - - return Some(out); - } - - Self::Error { pictures, .. } => { - pictures.as_ref().map(|x| x.iter().cloned().collect()) - } - } - } - } - - /// A list of test files and their expected output - #[expect(clippy::unwrap_used)] - pub fn manifest() -> [FlacTestCase; 23] { - [ - FlacTestCase::Error { - test_name: "uncommon_10", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_uncommon/10 - file starting at frame header.flac" - ), - in_hash: "d95f63e8101320f5ac7ffe249bc429a209eb0e10996a987301eaa63386a8faa1", - check_error: &|x| matches!(x, FlacDecodeError::BadMagicBytes), - stripped_hash: None, - pictures: None, - }, - FlacTestCase::Error { - test_name: "faulty_06", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_faulty/06 - missing streaminfo metadata block.flac" - ), - in_hash: "53aed5e7fde7a652b82ba06a8382b2612b02ebbde7b0d2016276644d17cc76cd", - check_error: &|x| matches!(x, FlacDecodeError::BadFirstBlock), - stripped_hash: None, - pictures: None, - }, - FlacTestCase::Error { - test_name: "faulty_07", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_faulty/07 - other metadata blocks preceding streaminfo metadata block.flac" - ), - in_hash: "6d46725991ba5da477187fde7709ea201c399d00027257c365d7301226d851ea", - check_error: &|x| matches!(x, FlacDecodeError::BadFirstBlock), - stripped_hash: None, - pictures: None, - }, - FlacTestCase::Error { - test_name: "faulty_10", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_faulty/10 - invalid vorbis comment metadata block.flac" - ), - in_hash: "c79b0514a61634035a5653c5493797bbd1fcc78982116e4d429630e9e462d29b", - check_error: &|x| { - matches!( - x, - FlacDecodeError::VorbisComment(VorbisCommentDecodeError::MalformedData) - ) - }, - // This file's vorbis comment is invalid, but that shouldn't stop us from removing it. - // As a general rule, we should NOT encounter an error when stripping invalid blocks. - // - // We should, however, get errors when we try to strip flac files with invalid *structure* - // (For example, the out-of-order streaminfo test in faulty_07). - stripped_hash: Some( - "4b994f82dc1699a58e2b127058b37374220ee41dc294d4887ac14f056291a1b0", - ), - pictures: None, - }, - FlacTestCase::Error { - test_name: "faulty_11", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_faulty/11 - incorrect metadata block length.flac" - ), - in_hash: "3732151ba8c4e66a785165aa75a444aad814c16807ddc97b793811376acacfd6", - check_error: &|x| matches!(x, FlacDecodeError::BadMetablockType(127)), - stripped_hash: None, - pictures: None, - }, - FlacTestCase::Success { - test_name: "subset_45", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_subset/45 - no total number of samples set.flac" - ), - in_hash: "336a18eb7a78f7fc0ab34980348e2895bc3f82db440a2430d9f92e996f889f9a", - blocks: vec![ - FlacBlockOutput::Streaminfo { - min_block_size: 4096, - max_block_size: 4096, - min_frame_size: 907, - max_frame_size: 8053, - sample_rate: 48000, - channels: 2, - bits_per_sample: 16, - total_samples: 0, - md5_signature: "c41ae3b82c35d8f5c3dab1729f948fde", - }, - FlacBlockOutput::VorbisComment { - vendor: "reference libFLAC 1.3.2 20170101", - comments: VorbisCommentTestValue::Raw { tags: &[] }, - pictures: &[], - }, - ], - audio_hash: "3fb3482ebc1724559bdd57f34de472458563d78a676029614e76e32b5d2b8816", - stripped_hash: "31631ac227ebe2689bac7caa1fa964b47e71a9f1c9c583a04ea8ebd9371508d0", - }, - FlacTestCase::Success { - test_name: "subset_46", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_subset/46 - no min-max framesize set.flac" - ), - in_hash: "9dc39732ce17815832790901b768bb50cd5ff0cd21b28a123c1cabc16ed776cc", - blocks: vec![ - FlacBlockOutput::Streaminfo { - min_block_size: 4096, - max_block_size: 4096, - min_frame_size: 0, - max_frame_size: 0, - sample_rate: 48000, - channels: 2, - bits_per_sample: 16, - total_samples: 282866, - md5_signature: "fd131e6ebc75251ed83f8f4c07df36a4", - }, - FlacBlockOutput::VorbisComment { - vendor: "reference libFLAC 1.3.2 20170101", - comments: VorbisCommentTestValue::Raw { tags: &[] }, - pictures: &[], - }, - ], - audio_hash: "a1eed422462b386a932b9eb3dff3aea3687b41eca919624fb574aadb7eb50040", - stripped_hash: "9e57cd77f285fc31f87fa4e3a31ab8395d68d5482e174c8e0d0bba9a0c20ba27", - }, - FlacTestCase::Success { - test_name: "subset_47", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_subset/47 - only STREAMINFO.flac" - ), - in_hash: "9a62c79f634849e74cb2183f9e3a9bd284f51e2591c553008d3e6449967eef85", - blocks: vec![FlacBlockOutput::Streaminfo { - min_block_size: 4096, - max_block_size: 4096, - min_frame_size: 4747, - max_frame_size: 7034, - sample_rate: 48000, - channels: 2, - bits_per_sample: 16, - total_samples: 232608, - md5_signature: "bba30c5f70789910e404b7ac727c3853", - }], - audio_hash: "5ee1450058254087f58c91baf0f70d14bde8782cf2dc23c741272177fe0fce6e", - stripped_hash: "9a62c79f634849e74cb2183f9e3a9bd284f51e2591c553008d3e6449967eef85", - }, - FlacTestCase::Success { - test_name: "subset_48", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_subset/48 - Extremely large SEEKTABLE.flac" - ), - in_hash: "4417aca6b5f90971c50c28766d2f32b3acaa7f9f9667bd313336242dae8b2531", - blocks: vec![ - FlacBlockOutput::Streaminfo { - min_block_size: 4096, - max_block_size: 4096, - min_frame_size: 2445, - max_frame_size: 7364, - sample_rate: 48000, - channels: 2, - bits_per_sample: 16, - total_samples: 259884, - md5_signature: "97a0574290237563fbaa788ad77d2cdf", - }, - FlacBlockOutput::Seektable { - hash: "21ca2184ae22fe26b690fd7cbd8d25fcde1d830ff6e5796ced4107bab219d7c0", - }, - FlacBlockOutput::VorbisComment { - vendor: "reference libFLAC 1.3.2 20170101", - comments: VorbisCommentTestValue::Raw { tags: &[] }, - pictures: &[], - }, - ], - audio_hash: "c2d691f2c4c986fe3cd5fd7864d9ba9ce6dd68a4ffc670447f008434b13102c2", - stripped_hash: "abc9a0c40a29c896bc6e1cc0b374db1c8e157af716a5a3c43b7db1591a74c4e8", - }, - FlacTestCase::Success { - test_name: "subset_49", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_subset/49 - Extremely large PADDING.flac", - ), - in_hash: "7bc44fa2754536279fde4f8fb31d824f43b8d0b3f93d27d055d209682914f20e", - blocks: vec![ - FlacBlockOutput::Streaminfo { - min_block_size: 4096, - max_block_size: 4096, - min_frame_size: 1353, - max_frame_size: 7117, - sample_rate: 48000, - channels: 2, - bits_per_sample: 16, - total_samples: 258939, - md5_signature: "6e78f221caaaa5d570a53f1714d84ded", - }, - FlacBlockOutput::VorbisComment { - vendor: "reference libFLAC 1.3.2 20170101", - comments: VorbisCommentTestValue::Raw { tags: &[] }, - pictures: &[], - }, - FlacBlockOutput::Padding { size: 16777215 }, - ], - audio_hash: "5007be7109b28b0149d1b929d2a0e93a087381bd3e68cf2a3ef78ea265ea20c3", - stripped_hash: "a2283bbacbc4905ad3df1bf9f43a0ea7aa65cf69523d84a7dd8eb54553cc437e", - }, - FlacTestCase::Success { - test_name: "subset_50", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_subset/50 - Extremely large PICTURE.flac" - ), - in_hash: "1f04f237d74836104993a8072d4223e84a5d3bd76fbc44555c221c7e69a23594", - blocks: vec![ - FlacBlockOutput::Streaminfo { - min_block_size: 4096, - max_block_size: 4096, - min_frame_size: 5099, - max_frame_size: 7126, - sample_rate: 48000, - channels: 2, - bits_per_sample: 16, - total_samples: 265617, - md5_signature: "82164e4da30ed43b47e6027cef050648", - }, - FlacBlockOutput::VorbisComment { - vendor: "reference libFLAC 1.3.2 20170101", - comments: VorbisCommentTestValue::Raw { tags: &[] }, - pictures: &[], - }, - FlacBlockOutput::Picture { - picture_type: PictureType::FrontCover, - mime: mime::IMAGE_JPEG, - description: "", - width: 3200, - height: 2252, - bit_depth: 24, - color_count: 0, - img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563", - }, - ], - audio_hash: "9778b25c5d1f56cfcd418e550baed14f9d6a4baf29489a83ed450fbebb28de8c", - stripped_hash: "20df129287d94f9ae5951b296d7f65fcbed92db423ba7db4f0d765f1f0a7e18c", - }, - FlacTestCase::Success { - test_name: "subset_51", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_subset/51 - Extremely large VORBISCOMMENT.flac" - ), - in_hash: "033160e8124ed287b0b5d615c94ac4139477e47d6e4059b1c19b7141566f5ef9", - blocks: vec![ - FlacBlockOutput::Streaminfo { - min_block_size: 4096, - max_block_size: 4096, - min_frame_size: 4531, - max_frame_size: 7528, - sample_rate: 48000, - channels: 2, - bits_per_sample: 16, - total_samples: 289972, - md5_signature: "5ff622c88f8dd9bc201a6a541f3890d3", - }, - FlacBlockOutput::VorbisComment { - vendor: "reference libFLAC 1.3.2 20170101", - comments: VorbisCommentTestValue::Hash { - n_comments: 39, - hash: "01984e9ec0cfad41f27b3b4e84184966f6725ead84b7815bd0b3313549ee4229", - }, - pictures: &[], - }, - ], - audio_hash: "76419865d10eb22a74f020423a4e515e800f0177441676afd0418557c2d76c36", - stripped_hash: "c0ca6c6099b5d9ec53d6bb370f339b2b1570055813a6cd3616fac2db83a2185e", - }, - FlacTestCase::Success { - test_name: "subset_52", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_subset/52 - Extremely large APPLICATION.flac" - ), - in_hash: "0e45a4f8dbef15cbebdd8dfe690d8ae60e0c6abb596db1270a9161b62a7a3f1c", - blocks: vec![ - FlacBlockOutput::Streaminfo { - min_block_size: 4096, - max_block_size: 4096, - min_frame_size: 3711, - max_frame_size: 7056, - sample_rate: 48000, - channels: 2, - bits_per_sample: 16, - total_samples: 317876, - md5_signature: "eb7140266bc194527488c21ab49bc47b", - }, - FlacBlockOutput::VorbisComment { - vendor: "reference libFLAC 1.3.2 20170101", - comments: VorbisCommentTestValue::Raw { tags: &[] }, - pictures: &[], - }, - FlacBlockOutput::Application { - application_id: 0x74657374, - hash: "cfc0b8969e4ba6bd507999ba89dea2d274df69d94749d6ae3cf117a7780bba09", - }, - ], - audio_hash: "89ad1a5c86a9ef35d33189c81c8a90285a23964a13f8325bf2c02043e8c83d63", - stripped_hash: "cc4a0afb95ec9bcde8ee33f13951e494dc4126a9a3a668d79c80ce3c14a3acd9", - }, - FlacTestCase::Success { - test_name: "subset_53", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_subset/53 - CUESHEET with very many indexes.flac" - ), - in_hash: "513fad18578f3225fae5de1bda8f700415be6fd8aa1e7af533b5eb796ed2d461", - blocks: vec![ - FlacBlockOutput::Streaminfo { - min_block_size: 4096, - max_block_size: 4096, - min_frame_size: 2798, - max_frame_size: 7408, - sample_rate: 48000, - channels: 2, - bits_per_sample: 16, - total_samples: 2910025, - md5_signature: "d11f3717d628cfe6a90a10facc478340", - }, - FlacBlockOutput::Seektable { - hash: "18629e1b874cb27e4364da72fb3fec2141eb0618baae4a1cee6ed09562aa00a8", - }, - FlacBlockOutput::VorbisComment { - vendor: "reference libFLAC 1.3.2 20170101", - comments: VorbisCommentTestValue::Raw { tags: &[] }, - pictures: &[], - }, - FlacBlockOutput::CueSheet { - hash: "70638a241ca06881a52c0a18258ea2d8946a830137a70479c49746d2a1344bdd", - }, - ], - audio_hash: "e993070f2080f2c598be1d61d208e9187a55ddea4be1d2ed1f8043e7c03e97a5", - stripped_hash: "57c5b945e14c6fcd06916d6a57e5b036d67ff35757893c24ed872007aabbcf4b", - }, - FlacTestCase::Success { - test_name: "subset_54", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_subset/54 - 1000x repeating VORBISCOMMENT.flac" - ), - in_hash: "b68dc6644784fac35aa07581be8603a360d1697e07a2265d7eb24001936fd247", - blocks: vec![ - FlacBlockOutput::Streaminfo { - min_block_size: 4096, - max_block_size: 4096, - min_frame_size: 1694, - max_frame_size: 7145, - sample_rate: 48000, - channels: 2, - bits_per_sample: 16, - total_samples: 433151, - md5_signature: "1d950e92b357dedbc5290a7f2210a2ef", - }, - FlacBlockOutput::VorbisComment { - vendor: "reference libFLAC 1.3.2 20170101", - comments: VorbisCommentTestValue::Hash { - n_comments: 20000, - hash: "433f34ae532d265835153139b1db79352a26ad0d3b03e2f1a1b88ada34abfc77", - }, - pictures: &[], - }, - ], - audio_hash: "4721b784058410c6263f73680079e9a71aee914c499afcf5580c121fce00e874", - stripped_hash: "5c8b92b83c0fa17821add38263fa323d1c66cfd2ee57aca054b50bd05b9df5c2", - }, - FlacTestCase::Success { - test_name: "subset_55", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_subset/55 - file 48-53 combined.flac" - ), - in_hash: "a756b460df79b7cc492223f80cda570e4511f2024e5fa0c4d505ba51b86191f6", - blocks: vec![ - FlacBlockOutput::Streaminfo { - min_block_size: 4096, - max_block_size: 4096, - min_frame_size: 3103, - max_frame_size: 11306, - sample_rate: 44100, - channels: 2, - bits_per_sample: 16, - total_samples: 2646000, - md5_signature: "2c78978cbbff11daac296fee97c3e061", - }, - FlacBlockOutput::Seektable { - hash: "58dfa7bac4974edf1956b068f5aa72d1fbd9301c36a3085a8a57b9db11a2dbf0", - }, - FlacBlockOutput::VorbisComment { - vendor: "reference libFLAC 1.3.3 20190804", - comments: VorbisCommentTestValue::Hash { - n_comments: 40036, - hash: "66cac9f9c42f48128e9fc24e1e96b46a06e885d233155556da16d9b05a23486e", - }, - pictures: &[], - }, - FlacBlockOutput::CueSheet { - hash: "db11916c8f5f39648256f93f202e00ff8d73d7d96b62f749b4c77cf3ea744f90", - }, - FlacBlockOutput::Application { - application_id: 0x74657374, - hash: "6088a557a1bad7bfa5ebf79a324669fbf4fa2f8e708f5487305dfc5b2ff2249a", - }, - FlacBlockOutput::Picture { - picture_type: PictureType::FrontCover, - mime: mime::IMAGE_JPEG, - description: "", - width: 3200, - height: 2252, - bit_depth: 24, - color_count: 0, - img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563", - }, - FlacBlockOutput::Padding { size: 16777215 }, - ], - audio_hash: "f1285b77cec7fa9a0979033244489a9d06b8515b2158e9270087a65a4007084d", - stripped_hash: "401038fce06aff5ebdc7a5f2fc01fa491cbf32d5da9ec99086e414b2da3f8449", - }, - FlacTestCase::Success { - test_name: "subset_56", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_subset/56 - JPG PICTURE.flac" - ), - in_hash: "5cebe7a3710cf8924bd2913854e9ca60b4cd53cfee5a3af0c3c73fddc1888963", - blocks: vec![ - FlacBlockOutput::Streaminfo { - min_block_size: 4096, - max_block_size: 4096, - min_frame_size: 3014, - max_frame_size: 7219, - sample_rate: 44100, - channels: 2, - bits_per_sample: 16, - total_samples: 220026, - md5_signature: "5b0e898d9c2626d0c28684f5a586813f", - }, - FlacBlockOutput::VorbisComment { - vendor: "reference libFLAC 1.3.2 20170101", - comments: VorbisCommentTestValue::Raw { tags: &[] }, - pictures: &[], - }, - FlacBlockOutput::Picture { - picture_type: PictureType::FrontCover, - mime: mime::IMAGE_JPEG, - description: "", - width: 1920, - height: 1080, - bit_depth: 24, - color_count: 0, - img_data: "7a3ed658f80f433eee3914fff451ea0312807de0af709e37cc6a4f3f6e8a47c6", - }, - ], - audio_hash: "ccfe90b0f15cd9662f7a18f40cd4c347538cf8897a08228e75351206f7804573", - stripped_hash: "31a38d59db2010790b7abf65ec0cc03f2bbe1fed5952bc72bee4ca4d0c92e79f", - }, - FlacTestCase::Success { - test_name: "subset_57", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_subset/57 - PNG PICTURE.flac" - ), - in_hash: "c6abff7f8bb63c2821bd21dd9052c543f10ba0be878e83cb419c248f14f72697", - blocks: vec![ - FlacBlockOutput::Streaminfo { - min_block_size: 4096, - max_block_size: 4096, - min_frame_size: 463, - max_frame_size: 6770, - sample_rate: 44100, - channels: 2, - bits_per_sample: 16, - total_samples: 221623, - md5_signature: "ad16957bcf8d5a3ec8caf261e43d5ff7", - }, - FlacBlockOutput::VorbisComment { - vendor: "reference libFLAC 1.3.2 20170101", - comments: VorbisCommentTestValue::Raw { tags: &[] }, - pictures: &[], - }, - FlacBlockOutput::Picture { - picture_type: PictureType::FrontCover, - mime: mime::IMAGE_PNG, - description: "", - width: 960, - height: 540, - bit_depth: 24, - color_count: 0, - img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2", - }, - ], - audio_hash: "39bf9981613ac2f35d253c0c21b76a48abba7792c27da5dbf23e6021e2e6673f", - stripped_hash: "3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1", - }, - FlacTestCase::Success { - test_name: "subset_58", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_subset/58 - GIF PICTURE.flac" - ), - in_hash: "7c2b1a963a665847167a7275f9924f65baeb85c21726c218f61bf3f803f301c8", - blocks: vec![ - FlacBlockOutput::Streaminfo { - min_block_size: 4096, - max_block_size: 4096, - min_frame_size: 2853, - max_frame_size: 6683, - sample_rate: 44100, - channels: 2, - bits_per_sample: 16, - total_samples: 219826, - md5_signature: "7c1810602a7db96d7a48022ac4aa495c", - }, - FlacBlockOutput::VorbisComment { - vendor: "reference libFLAC 1.3.2 20170101", - comments: VorbisCommentTestValue::Raw { tags: &[] }, - pictures: &[], - }, - FlacBlockOutput::Picture { - picture_type: PictureType::FrontCover, - mime: mime::IMAGE_GIF, - description: "", - width: 1920, - height: 1080, - bit_depth: 24, - color_count: 32, - img_data: "e33cccc1d799eb2bb618f47be7099cf02796df5519f3f0e1cc258606cf6e8bb1", - }, - ], - audio_hash: "30e3292e9f56cf88658eeadfdec8ad3a440690ce6d813e1b3374f60518c8e0ae", - stripped_hash: "4cd771e27870e2a586000f5b369e0426183a521b61212302a2f5802b046910b2", - }, - FlacTestCase::Success { - test_name: "subset_59", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_subset/59 - AVIF PICTURE.flac" - ), - in_hash: "7395d02bf8d9533dc554cce02dee9de98c77f8731a45f62d0a243bd0d6f9a45c", - blocks: vec![ - FlacBlockOutput::Streaminfo { - min_block_size: 4096, - max_block_size: 4096, - min_frame_size: 153, - max_frame_size: 7041, - sample_rate: 44100, - channels: 2, - bits_per_sample: 16, - total_samples: 221423, - md5_signature: "d354246011ca204159c06f52cad5f634", - }, - FlacBlockOutput::VorbisComment { - vendor: "reference libFLAC 1.3.2 20170101", - comments: VorbisCommentTestValue::Raw { tags: &[] }, - pictures: &[], - }, - FlacBlockOutput::Picture { - picture_type: PictureType::FrontCover, - mime: Mime::from_str("image/avif").unwrap(), - description: "", - width: 1920, - height: 1080, - bit_depth: 24, - color_count: 0, - img_data: "a431123040c74f75096237f20544a7fb56b4eb71ddea62efa700b0a016f5b2fc", - }, - ], - audio_hash: "b208c73d274e65b27232bfffbfcbcf4805ee3cbc9cfbf7d2104db8f53370273b", - stripped_hash: "d5215e16c6b978fc2c3e6809e1e78981497cb8514df297c5169f3b4a28fd875c", - }, - FlacTestCase::Success { - test_name: "custom_01", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_custom/01 - many images.flac" - ), - in_hash: "8a5df37488866cd91ac16773e549ef4e3a85d9f88a0d9d345f174807bb536b96", - blocks: vec![ - FlacBlockOutput::Streaminfo { - min_block_size: 4096, - max_block_size: 4096, - min_frame_size: 5099, - max_frame_size: 7126, - sample_rate: 48000, - channels: 2, - bits_per_sample: 16, - total_samples: 265617, - md5_signature: "82164e4da30ed43b47e6027cef050648", - }, - FlacBlockOutput::VorbisComment { - vendor: "reference libFLAC 1.3.2 20170101", - comments: VorbisCommentTestValue::Raw { tags: &[] }, - pictures: &[FlacBlockOutput::Picture { - picture_type: PictureType::FrontCover, - mime: mime::IMAGE_PNG, - description: "", - width: 960, - height: 540, - bit_depth: 24, - color_count: 0, - img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2", - }], - }, - FlacBlockOutput::Picture { - picture_type: PictureType::FrontCover, - mime: mime::IMAGE_JPEG, - description: "", - width: 3200, - height: 2252, - bit_depth: 24, - color_count: 0, - img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563", - }, - FlacBlockOutput::Picture { - picture_type: PictureType::ABrightColoredFish, - mime: mime::IMAGE_JPEG, - description: "lorem", - width: 1920, - height: 1080, - bit_depth: 24, - color_count: 0, - img_data: "7a3ed658f80f433eee3914fff451ea0312807de0af709e37cc6a4f3f6e8a47c6", - }, - FlacBlockOutput::Picture { - picture_type: PictureType::OtherFileIcon, - mime: mime::IMAGE_PNG, - description: "ipsum", - width: 960, - height: 540, - bit_depth: 24, - color_count: 0, - img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2", - }, - FlacBlockOutput::Picture { - picture_type: PictureType::Lyricist, - mime: mime::IMAGE_GIF, - description: "dolor", - width: 1920, - height: 1080, - bit_depth: 24, - color_count: 32, - img_data: "e33cccc1d799eb2bb618f47be7099cf02796df5519f3f0e1cc258606cf6e8bb1", - }, - FlacBlockOutput::Picture { - picture_type: PictureType::BackCover, - mime: Mime::from_str("image/avif").unwrap(), - description: "est", - width: 1920, - height: 1080, - bit_depth: 24, - color_count: 0, - img_data: "a431123040c74f75096237f20544a7fb56b4eb71ddea62efa700b0a016f5b2fc", - }, - ], - audio_hash: "9778b25c5d1f56cfcd418e550baed14f9d6a4baf29489a83ed450fbebb28de8c", - stripped_hash: "20df129287d94f9ae5951b296d7f65fcbed92db423ba7db4f0d765f1f0a7e18c", - }, - FlacTestCase::Success { - test_name: "custom_02", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_custom/02 - picture in vorbis comment.flac" - ), - in_hash: "f6bb1a726fe6a3e25a4337d36e29fdced8ff01a46d627b7c2e1988c88f461f8c", - blocks: vec![ - FlacBlockOutput::Streaminfo { - min_block_size: 4096, - max_block_size: 4096, - min_frame_size: 463, - max_frame_size: 6770, - sample_rate: 44100, - channels: 2, - bits_per_sample: 16, - total_samples: 221623, - md5_signature: "ad16957bcf8d5a3ec8caf261e43d5ff7", - }, - FlacBlockOutput::VorbisComment { - vendor: "reference libFLAC 1.3.2 20170101", - comments: VorbisCommentTestValue::Raw { tags: &[] }, - pictures: &[FlacBlockOutput::Picture { - picture_type: PictureType::FrontCover, - mime: mime::IMAGE_PNG, - description: "", - width: 960, - height: 540, - bit_depth: 24, - color_count: 0, - img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2", - }], - }, - ], - audio_hash: "39bf9981613ac2f35d253c0c21b76a48abba7792c27da5dbf23e6021e2e6673f", - stripped_hash: "3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1", - }, - FlacTestCase::Error { - test_name: "custom_03", - file_path: concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/files/flac_custom/03 - faulty picture in vorbis comment.flac" - ), - in_hash: "7177f0ae4f04a563292be286ec05967f81ab16eb0a28b70fc07a1e47da9cafd0", - check_error: &|x| { - matches!( - x, - FlacDecodeError::VorbisComment(VorbisCommentDecodeError::MalformedPicture) - ) - }, - stripped_hash: Some( - "3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1", - ), - pictures: None, - }, - ] - } - - #[test] - fn manifest_sanity_check() { - assert!(manifest().iter().map(|x| x.get_name()).all_unique()); - assert!(manifest().iter().map(|x| x.get_path()).all_unique()); - } -} diff --git a/crates/pile-audio/src/flac/proc/metastrip.rs b/crates/pile-audio/src/flac/proc/metastrip.rs deleted file mode 100644 index 4fa60f3..0000000 --- a/crates/pile-audio/src/flac/proc/metastrip.rs +++ /dev/null @@ -1,213 +0,0 @@ -//! A flac processor that strips metadata blocks from flac files - -use std::io::Write; - -use super::super::{ - blockread::{FlacBlock, FlacBlockReader, FlacBlockReaderError, FlacBlockSelector}, - errors::FlacEncodeError, -}; - -/// Removes all metadata from a flac file -pub struct FlacMetaStrip { - reader: FlacBlockReader, - - /// The last block that `reader` produced. - /// - /// We need this to detect the last metadata block - /// that `reader` produces. - last_block: Option, - - /// Set to `false` on the first call to `self.write_data`. - /// Used to write fLaC magic bytes. - first_write: bool, -} - -impl FlacMetaStrip { - /// Make a new [`FlacMetaStrip`] - pub fn new() -> Self { - Self { - first_write: true, - last_block: None, - reader: FlacBlockReader::new(FlacBlockSelector { - pick_streaminfo: true, - pick_padding: false, - pick_application: false, - pick_seektable: true, - pick_vorbiscomment: false, - pick_cuesheet: true, - pick_picture: false, - pick_audio: true, - }), - } - } - - /// Push some data to this flac processor - pub fn push_data(&mut self, buf: &[u8]) -> Result<(), FlacBlockReaderError> { - self.reader.push_data(buf) - } - - /// Call after sending the entire flac file to this reader - pub fn finish(&mut self) -> Result<(), FlacBlockReaderError> { - self.reader.finish() - } - - /// If true, we have received all the data we need - pub fn is_done(&mut self) -> bool { - self.reader.is_done() - } - - /// If false, this reader has sent all its data. - /// - /// Note that `read_data` may write zero bytes if this method returns `true`. - /// If `has_data` is false, we don't AND WON'T have data. If we're waiting - /// for data, this is `true`. - pub fn has_data(&self) -> bool { - self.last_block.is_some() || !self.reader.is_done() || self.reader.has_block() - } - - /// Write available data from this struct into `target` - pub fn read_data(&mut self, target: &mut impl Write) -> Result<(), FlacEncodeError> { - if self.first_write { - target.write_all(&[0x66, 0x4C, 0x61, 0x43])?; - self.first_write = false; - } - - while let Some(block) = self.reader.pop_block() { - if let Some(last_block) = self.last_block.take() { - last_block.encode( - // The last metadata block is the only one followed by an audio frame - !matches!(last_block, FlacBlock::AudioFrame(_)) - && matches!(block, FlacBlock::AudioFrame(_)), - true, - target, - )?; - } - self.last_block = Some(block); - } - - // We don't need to store audioframes in our last_block buffer, - // since they do not have an `is_last` flag. - if matches!(self.last_block, Some(FlacBlock::AudioFrame(_))) { - #[expect(clippy::expect_used)] - let x = self - .last_block - .take() - .expect("last_block is Some(AudioFrame), just matched"); - x.encode(false, true, target)?; - } - - return Ok(()); - } -} - -#[cfg(test)] -mod tests { - use paste::paste; - use rand::Rng; - use sha2::{Digest, Sha256}; - - use crate::flac::{ - blockread::FlacBlockReaderError, proc::metastrip::FlacMetaStrip, tests::FlacTestCase, - tests::manifest, - }; - - #[expect(clippy::unwrap_used)] - fn test_strip( - test_case: &FlacTestCase, - fragment_size_range: Option>, - ) -> Result<(), FlacBlockReaderError> { - let file_data = std::fs::read(test_case.get_path()).unwrap(); - - // Make sure input file is correct - let mut hasher = Sha256::new(); - hasher.update(&file_data); - assert_eq!( - test_case.get_in_hash(), - hasher.finalize().map(|x| format!("{x:02x}")).join("") - ); - - let mut strip = FlacMetaStrip::new(); - - // Push file data to the reader, in parts or as a whole. - if let Some(fragment_size_range) = fragment_size_range { - let mut head = 0; - while head < file_data.len() { - let mut frag_size = rand::rng().random_range(fragment_size_range.clone()); - if head + frag_size > file_data.len() { - frag_size = file_data.len() - head; - } - strip.push_data(&file_data[head..head + frag_size])?; - head += frag_size; - } - } else { - strip.push_data(&file_data)?; - } - - strip.finish()?; - - let mut out_data = Vec::new(); - strip.read_data(&mut out_data).unwrap(); - assert!(!strip.has_data()); - - let mut hasher = Sha256::new(); - hasher.update(&out_data); - let result = hasher.finalize().map(|x| format!("{x:02x}")).join(""); - assert_eq!( - result, - test_case.get_stripped_hash().unwrap(), - "Stripped FLAC hash doesn't match" - ); - - return Ok(()); - } - - macro_rules! gen_tests { - ( $test_name:ident ) => { - paste! { - #[test] - pub fn []() { - let manifest =manifest(); - let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap(); - match test_case { - FlacTestCase::Error { stripped_hash: Some(_), .. } | - FlacTestCase::Success { .. } => { - for _ in 0..5 { - test_strip( - test_case, - Some(5_000..100_000), - ).unwrap() - } - }, - - FlacTestCase::Error { check_error, .. } => { - let e = test_strip(test_case, Some(5_000..100_000)).unwrap_err(); - match e { - FlacBlockReaderError::DecodeError(e) => assert!(check_error(&e), "Unexpected error {e:?}"), - _ => panic!("Unexpected error {e:?}") - } - } - } - } - } - }; - - - } - - gen_tests!(custom_01); - gen_tests!(custom_02); - gen_tests!(custom_03); - - gen_tests!(uncommon_10); - - gen_tests!(faulty_06); - gen_tests!(faulty_07); - gen_tests!(faulty_10); - gen_tests!(faulty_11); - - gen_tests!(subset_45); - gen_tests!(subset_47); - gen_tests!(subset_54); - gen_tests!(subset_55); - gen_tests!(subset_57); -} diff --git a/crates/pile-audio/src/flac/proc/mod.rs b/crates/pile-audio/src/flac/proc/mod.rs deleted file mode 100644 index 24bf102..0000000 --- a/crates/pile-audio/src/flac/proc/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Flac processors. These are well-tested wrappers around [`crate::flac::blockread::FlacBlockReader`] -//! that are specialized for specific tasks. - -pub mod metastrip; -pub mod pictures; diff --git a/crates/pile-audio/src/flac/proc/pictures.rs b/crates/pile-audio/src/flac/proc/pictures.rs deleted file mode 100644 index b77d957..0000000 --- a/crates/pile-audio/src/flac/proc/pictures.rs +++ /dev/null @@ -1,229 +0,0 @@ -//! A flac processor that finds all images inside a flac file - -use std::collections::VecDeque; - -use super::super::{ - blockread::{FlacBlock, FlacBlockReader, FlacBlockReaderError, FlacBlockSelector}, - blocks::FlacPictureBlock, -}; - -/// Find all pictures in a flac file, -/// in both picture metablocks and vorbis comments. -pub struct FlacPictureReader { - reader: FlacBlockReader, - pictures: VecDeque, -} - -impl FlacPictureReader { - /// Make a new [`FlacMetaStrip`] - pub fn new() -> Self { - Self { - pictures: VecDeque::new(), - reader: FlacBlockReader::new(FlacBlockSelector { - pick_streaminfo: false, - pick_padding: false, - pick_application: false, - pick_seektable: false, - pick_vorbiscomment: true, - pick_cuesheet: false, - pick_picture: true, - pick_audio: false, - }), - } - } - - /// Push some data to this flac processor - pub fn push_data(&mut self, buf: &[u8]) -> Result<(), FlacBlockReaderError> { - self.reader.push_data(buf)?; - - while let Some(b) = self.reader.pop_block() { - match b { - FlacBlock::Picture(p) => self.pictures.push_back(p), - - FlacBlock::VorbisComment(c) => { - for p in c.comment.pictures { - self.pictures.push_back(p) - } - } - - _ => unreachable!(), - } - } - - return Ok(()); - } - - /// Call after sending the entire flac file to this reader - pub fn finish(&mut self) -> Result<(), FlacBlockReaderError> { - self.reader.finish() - } - - /// If true, we have received all the data we need - pub fn is_done(&mut self) -> bool { - self.reader.is_done() - } - - /// If false, this reader has sent all its data. - /// - /// Note that `read_data` may write zero bytes if this method returns `true`. - /// If `has_data` is false, we don't AND WON'T have data. If we're waiting - /// for data, this is `true`. - pub fn has_data(&self) -> bool { - !self.reader.is_done() || self.reader.has_block() || !self.pictures.is_empty() - } - - /// Pop the next picture we read from this file, if any. - pub fn pop_picture(&mut self) -> Option { - self.pictures.pop_front() - } -} - -#[cfg(test)] -mod tests { - use paste::paste; - use rand::Rng; - use sha2::{Digest, Sha256}; - - use crate::flac::{ - blockread::FlacBlockReaderError, - proc::pictures::FlacPictureReader, - tests::{FlacBlockOutput, FlacTestCase, manifest}, - }; - - #[expect(clippy::unwrap_used)] - fn test_pictures( - test_case: &FlacTestCase, - fragment_size_range: Option>, - ) -> Result<(), FlacBlockReaderError> { - let file_data = std::fs::read(test_case.get_path()).unwrap(); - - // Make sure input file is correct - let mut hasher = Sha256::new(); - hasher.update(&file_data); - assert_eq!( - test_case.get_in_hash(), - hasher.finalize().map(|x| format!("{x:02x}")).join("") - ); - - let mut pic = FlacPictureReader::new(); - - // Push file data to the reader, in parts or as a whole. - if let Some(fragment_size_range) = fragment_size_range { - let mut head = 0; - while head < file_data.len() { - let mut frag_size = rand::rng().random_range(fragment_size_range.clone()); - if head + frag_size > file_data.len() { - frag_size = file_data.len() - head; - } - pic.push_data(&file_data[head..head + frag_size])?; - head += frag_size; - } - } else { - pic.push_data(&file_data)?; - } - - pic.finish()?; - - let mut out = Vec::new(); - while let Some(p) = pic.pop_picture() { - out.push(p); - } - - let out_pictures = test_case.get_pictures().unwrap(); - - assert_eq!( - out.len(), - out_pictures.len(), - "Unexpected number of pictures" - ); - - for (got, expected) in out.iter().zip(out_pictures) { - let (picture_type, mime, description, width, height, bit_depth, color_count, img_data) = - match expected { - FlacBlockOutput::Picture { - picture_type, - mime, - description, - width, - height, - bit_depth, - color_count, - img_data, - } => ( - picture_type, - mime, - description, - width, - height, - bit_depth, - color_count, - img_data, - ), - _ => unreachable!(), - }; - - assert_eq!(picture_type, got.picture_type, "{}", test_case.get_name()); - assert_eq!(mime, got.mime, "{}", test_case.get_name()); - assert_eq!(*description, got.description, "{}", test_case.get_name()); - assert_eq!(width, got.width, "{}", test_case.get_name()); - assert_eq!(height, got.height, "{}", test_case.get_name()); - assert_eq!(bit_depth, got.bit_depth, "{}", test_case.get_name()); - assert_eq!(color_count, got.color_count, "{}", test_case.get_name()); - assert_eq!( - *img_data, - { - let mut hasher = Sha256::new(); - hasher.update(&got.img_data); - hasher.finalize().map(|x| format!("{x:02x}")).join("") - }, - "{}", - test_case.get_name() - ); - } - - return Ok(()); - } - - macro_rules! gen_tests { - ( $test_name:ident ) => { - paste! { - #[test] - pub fn []() { - let manifest = manifest(); - let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap(); - match test_case { - FlacTestCase::Error { pictures: Some(_), .. } | - FlacTestCase::Success { .. } => { - for _ in 0..5 { - test_pictures( - test_case, - Some(5_000..100_000), - ).unwrap() - } - }, - - FlacTestCase::Error { check_error, .. } => { - let e = test_pictures(test_case, Some(5_000..100_000)).unwrap_err(); - match e { - FlacBlockReaderError::DecodeError(e) => assert!(check_error(&e), "Unexpected error {e:?}"), - _ => panic!("Unexpected error {e:?}") - } - } - } - } - } - }; - } - - gen_tests!(custom_01); - gen_tests!(custom_02); - gen_tests!(custom_03); - - gen_tests!(subset_47); - gen_tests!(subset_50); - gen_tests!(subset_55); - gen_tests!(subset_56); - gen_tests!(subset_57); - gen_tests!(subset_58); - gen_tests!(subset_59); -} diff --git a/crates/pile-audio/src/lib.rs b/crates/pile-audio/src/lib.rs deleted file mode 100644 index 7f10084..0000000 --- a/crates/pile-audio/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Read and write audio file metadata. -pub mod common; -pub mod flac; diff --git a/crates/pile-audio/src/nodes/extractcovers.rs b/crates/pile-audio/src/nodes/extractcovers.rs deleted file mode 100644 index 2a76d4c..0000000 --- a/crates/pile-audio/src/nodes/extractcovers.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::flac::proc::pictures::FlacPictureReader; -use std::{collections::BTreeMap, sync::Arc}; -use tracing::{debug, trace}; - -pub struct ExtractCovers {} - -impl NodeBuilder for ExtractCovers { - fn build<'ctx>(&self) -> Box> { - Box::new(Self {}) - } -} - -// Inputs: "data" - Bytes -#[async_trait] -impl<'ctx> Node<'ctx> for ExtractCovers { - async fn run( - &self, - ctx: &CopperContext<'ctx>, - this_node: ThisNodeInfo, - params: NodeParameters, - mut input: BTreeMap>, - ) -> Result, RunNodeError> { - // - // Extract parameters - // - params.err_if_not_empty()?; - - // - // Extract arguments - // - let data = input.remove(&PortName::new("data")); - if data.is_none() { - return Err(RunNodeError::MissingInput { - port: PortName::new("data"), - }); - } - if let Some((port, _)) = input.pop_first() { - return Err(RunNodeError::UnrecognizedInput { port }); - } - - trace!( - message = "Inputs ready, preparing reader", - node_id = ?this_node.id - ); - - let mut reader = match data.unwrap() { - None => { - return Err(RunNodeError::RequiredInputNull { - port: PortName::new("data"), - }); - } - - Some(PipeData::Blob { source, .. }) => source.build(ctx).await?, - - _ => { - return Err(RunNodeError::BadInputType { - port: PortName::new("data"), - }); - } - }; - - // - // Setup is done, extract covers - // - debug!( - message = "Extracting covers", - node_id = ?this_node.id - ); - let mut picreader = FlacPictureReader::new(); - - while let Some(data) = reader.next_fragment().await? { - picreader - .push_data(&data) - .map_err(|e| RunNodeError::Other(Arc::new(e)))?; - } - - picreader - .finish() - .map_err(|e| RunNodeError::Other(Arc::new(e)))?; - - // - // Send the first cover we find - // - - let mut output = BTreeMap::new(); - - if let Some(picture) = picreader.pop_picture() { - debug!( - message = "Found a cover, sending", - node_id = ?this_node.id, - picture = ?picture - ); - - output.insert( - PortName::new("cover_data"), - PipeData::Blob { - source: BytesProcessorBuilder::new(RawBytesSource::Array { - mime: picture.mime.clone(), - data: Arc::new(picture.img_data), - }), - }, - ); - } else { - debug!( - message = "Did not find a cover, sending None", - node_id = ?this_node.id - ); - } - - return Ok(output); - } -} diff --git a/crates/pile-audio/src/nodes/extracttags.rs b/crates/pile-audio/src/nodes/extracttags.rs deleted file mode 100644 index 370c17a..0000000 --- a/crates/pile-audio/src/nodes/extracttags.rs +++ /dev/null @@ -1,164 +0,0 @@ -use crate::{ - common::tagtype::TagType, - flac::blockread::{FlacBlock, FlacBlockReader, FlacBlockSelector}, -}; -use async_trait::async_trait; -use copper_piper::{ - base::{Node, NodeBuilder, NodeParameterValue, PortName, RunNodeError, ThisNodeInfo}, - data::PipeData, - helpers::NodeParameters, - CopperContext, -}; -use std::{collections::BTreeMap, sync::Arc}; -use tracing::{debug, trace}; - -/// Extract tags from audio metadata -pub struct ExtractTags {} - -impl NodeBuilder for ExtractTags { - fn build<'ctx>(&self) -> Box> { - Box::new(Self {}) - } -} - -// Inputs: "data" - Bytes -// Outputs: variable, depends on tags -#[async_trait] -impl<'ctx> Node<'ctx> for ExtractTags { - async fn run( - &self, - ctx: &CopperContext<'ctx>, - this_node: ThisNodeInfo, - mut params: NodeParameters, - mut input: BTreeMap>, - ) -> Result, RunNodeError> { - // - // Extract parameters - // - - let tags = { - let mut tags: BTreeMap = BTreeMap::new(); - let val = params.pop_val("tags")?; - - match val { - NodeParameterValue::List(list) => { - for t in list { - match t { - NodeParameterValue::String(s) => { - tags.insert(PortName::new(s.as_str()), s.as_str().into()); - } - _ => { - return Err(RunNodeError::BadParameterType { - parameter: "tags".into(), - }) - } - } - } - } - _ => { - return Err(RunNodeError::BadParameterType { - parameter: "tags".into(), - }) - } - }; - - tags - }; - - params.err_if_not_empty()?; - - // - // Extract arguments - // - let data = input.remove(&PortName::new("data")); - if data.is_none() { - return Err(RunNodeError::MissingInput { - port: PortName::new("data"), - }); - } - if let Some((port, _)) = input.pop_first() { - return Err(RunNodeError::UnrecognizedInput { port }); - } - - trace!( - message = "Inputs ready, preparing reader", - node_id = ?this_node.id - ); - - let mut reader = match data.unwrap() { - None => { - return Err(RunNodeError::RequiredInputNull { - port: PortName::new("data"), - }) - } - - Some(PipeData::Blob { source, .. }) => source.build(ctx).await?, - - _ => { - return Err(RunNodeError::BadInputType { - port: PortName::new("data"), - }) - } - }; - - // - // Setup is done, extract tags - // - debug!( - message = "Extracting tags", - node_id = ?this_node.id - ); - - let mut block_reader = FlacBlockReader::new(FlacBlockSelector { - pick_vorbiscomment: true, - ..Default::default() - }); - - while let Some(data) = reader.next_fragment().await? { - block_reader - .push_data(&data) - .map_err(|e| RunNodeError::Other(Arc::new(e)))?; - } - - block_reader - .finish() - .map_err(|e| RunNodeError::Other(Arc::new(e)))?; - - // - // Return tags - // - - let mut output = BTreeMap::new(); - - while block_reader.has_block() { - let b = block_reader.pop_block().unwrap(); - match b { - FlacBlock::VorbisComment(comment) => { - for (port, tag_type) in tags.iter() { - if let Some((_, tag_value)) = - comment.comment.comments.iter().find(|(t, _)| t == tag_type) - { - let x = output.insert( - port.clone(), - PipeData::Text { - value: tag_value.clone(), - }, - ); - - // Each insertion should be new - assert!(x.is_none()); - } - } - } - - // `reader` filters blocks for us - _ => unreachable!(), - } - - // We should only have one comment block - assert!(!block_reader.has_block()); - } - - return Ok(output); - } -} diff --git a/crates/pile-audio/src/nodes/mod.rs b/crates/pile-audio/src/nodes/mod.rs deleted file mode 100644 index 3007874..0000000 --- a/crates/pile-audio/src/nodes/mod.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Pipeline nodes for processing audio files -use copper_piper::base::{NodeDispatcher, RegisterNodeError}; -use std::collections::BTreeMap; - -mod extractcovers; -mod extracttags; -mod striptags; - -/// Register all nodes in this module into the given dispatcher -pub fn register(dispatcher: &mut NodeDispatcher) -> Result<(), RegisterNodeError> { - dispatcher - .register_node( - "StripTags", - BTreeMap::new(), - Box::new(striptags::StripTags {}), - ) - .unwrap(); - - dispatcher - .register_node( - "ExtractCovers", - BTreeMap::new(), - Box::new(extractcovers::ExtractCovers {}), - ) - .unwrap(); - - dispatcher - .register_node( - "ExtractTags", - BTreeMap::new(), - Box::new(extracttags::ExtractTags {}), - ) - .unwrap(); - - return Ok(()); -} diff --git a/crates/pile-audio/src/nodes/striptags.rs b/crates/pile-audio/src/nodes/striptags.rs deleted file mode 100644 index 24ec31f..0000000 --- a/crates/pile-audio/src/nodes/striptags.rs +++ /dev/null @@ -1,181 +0,0 @@ -//! Strip all tags from an audio file - -use crate::flac::proc::metastrip::FlacMetaStrip; -use async_trait::async_trait; -use copper_piper::{ - base::{Node, NodeBuilder, NodeId, PortName, RunNodeError, ThisNodeInfo}, - data::PipeData, - helpers::{ - processor::{StreamProcessor, StreamProcessorBuilder}, - NodeParameters, - }, - CopperContext, -}; -use copper_util::MimeType; -use smartstring::{LazyCompact, SmartString}; -use std::{collections::BTreeMap, sync::Arc}; -use tokio::sync::mpsc::{Receiver, Sender}; -use tracing::debug; - -/// Strip all metadata from an audio file -pub struct StripTags {} - -impl NodeBuilder for StripTags { - fn build<'ctx>(&self) -> Box> { - Box::new(Self {}) - } -} - -// Input: "data" - Blob -// Output: "out" - Blob -#[async_trait] -impl<'ctx> Node<'ctx> for StripTags { - async fn run( - &self, - _ctx: &CopperContext<'ctx>, - this_node: ThisNodeInfo, - params: NodeParameters, - mut input: BTreeMap>, - ) -> Result, RunNodeError> { - // - // Extract parameters - // - params.err_if_not_empty()?; - - // - // Extract arguments - // - let data = input.remove(&PortName::new("data")); - if data.is_none() { - return Err(RunNodeError::MissingInput { - port: PortName::new("data"), - }); - } - if let Some((port, _)) = input.pop_first() { - return Err(RunNodeError::UnrecognizedInput { port }); - } - - let source = match data.unwrap() { - None => { - return Err(RunNodeError::RequiredInputNull { - port: PortName::new("data"), - }) - } - - Some(PipeData::Blob { source, .. }) => source, - - _ => { - return Err(RunNodeError::BadInputType { - port: PortName::new("data"), - }) - } - }; - - debug!( - message = "Setup done, stripping tags", - node_id = ?this_node.id - ); - - let mut output = BTreeMap::new(); - - output.insert( - PortName::new("out"), - PipeData::Blob { - source: source.add_processor(Arc::new(TagStripProcessor { - node_id: this_node.id.clone(), - node_type: this_node.node_type.clone(), - })), - }, - ); - - return Ok(output); - } -} - -#[derive(Debug, Clone)] -struct TagStripProcessor { - node_id: NodeId, - node_type: SmartString, -} - -impl StreamProcessorBuilder for TagStripProcessor { - fn build(&self) -> Box { - Box::new(self.clone()) - } -} - -#[async_trait] -impl StreamProcessor for TagStripProcessor { - fn mime(&self) -> &MimeType { - return &MimeType::Flac; - } - - fn name(&self) -> &'static str { - "TagStripProcessor" - } - - fn source_node_id(&self) -> &NodeId { - &self.node_id - } - - /// Return the type of the node that created this processor - fn source_node_type(&self) -> &str { - &self.node_type - } - - async fn run( - &self, - mut source: Receiver>>, - sink: Sender>>, - max_buffer_size: usize, - ) -> Result<(), RunNodeError> { - // - // Strip tags - // - - let mut strip = FlacMetaStrip::new(); - let mut out_bytes = Vec::new(); - - while let Some(data) = source.recv().await { - strip - .push_data(&data) - .map_err(|e| RunNodeError::Other(Arc::new(e)))?; - - strip - .read_data(&mut out_bytes) - .map_err(|e| RunNodeError::Other(Arc::new(e)))?; - - if out_bytes.len() >= max_buffer_size { - let x = std::mem::take(&mut out_bytes); - - match sink.send(Arc::new(x)).await { - Ok(()) => {} - - // Not an error, our receiver was dropped. - // Exit early if that happens! - Err(_) => return Ok(()), - }; - } - } - - strip - .finish() - .map_err(|e| RunNodeError::Other(Arc::new(e)))?; - - while strip.has_data() { - strip - .read_data(&mut out_bytes) - .map_err(|e| RunNodeError::Other(Arc::new(e)))?; - } - - match sink.send(Arc::new(out_bytes)).await { - Ok(()) => {} - - // Not an error, our receiver was dropped. - // Exit early if that happens! - Err(_) => return Ok(()), - }; - - return Ok(()); - } -} diff --git a/crates/pile-dataset/Cargo.toml b/crates/pile-dataset/Cargo.toml index 43399af..4959e02 100644 --- a/crates/pile-dataset/Cargo.toml +++ b/crates/pile-dataset/Cargo.toml @@ -9,8 +9,9 @@ workspace = true [dependencies] pile-config = { workspace = true } -pile-audio = { workspace = true } pile-toolbox = { workspace = true } +pile-flac = { workspace = true } + serde_json = { workspace = true } itertools = { workspace = true } diff --git a/crates/pile-dataset/src/item/flac.rs b/crates/pile-dataset/src/item/flac.rs index f73ee36..475c836 100644 --- a/crates/pile-dataset/src/item/flac.rs +++ b/crates/pile-dataset/src/item/flac.rs @@ -1,12 +1,7 @@ -use std::{ - fmt::Debug, - fs::File, - io::{Read, Seek}, - path::PathBuf, -}; +use std::{fmt::Debug, fs::File, io::BufReader, path::PathBuf}; -use pile_audio::flac::blockread::{FlacBlock, FlacBlockReader, FlacBlockSelector}; use pile_config::Label; +use pile_flac::{FlacBlock, FlacReader}; use serde_json::{Map, Value}; use crate::Item; @@ -36,56 +31,34 @@ impl Item for FlacItem { } fn json(&self) -> Result { - let mut block_reader = FlacBlockReader::new(FlacBlockSelector { - pick_vorbiscomment: true, - ..Default::default() - }); - - let mut file = File::open(&self.path)?; - - // TODO: do not read the whole file - file.rewind()?; - let mut data = Vec::new(); - file.read_to_end(&mut data)?; - - block_reader - .push_data(&data) - .map_err(std::io::Error::other)?; - block_reader.finish().map_err(std::io::Error::other)?; - - // - // Return tags - // + let file = File::open(&self.path)?; + let reader = FlacReader::new(BufReader::new(file)); let mut output = Map::new(); - while let Some(block) = block_reader.pop_block() { - match block { - FlacBlock::VorbisComment(comment) => { - for (k, v) in comment.comment.comments { - let k = k.to_string(); - let v = Value::String(v.into()); - let e = output.get_mut(&k); + for block in reader { + if let FlacBlock::VorbisComment(comment) = block.unwrap() { + for (k, v) in comment.comment.comments { + let k = k.to_string(); + let v = Value::String(v.into()); + let e = output.get_mut(&k); - match e { - None => { - output.insert(k.clone(), Value::Array(vec![v])); - } - Some(e) => { - // We always insert an array - #[expect(clippy::unwrap_used)] - e.as_array_mut().unwrap().push(v); - } + match e { + None => { + output.insert(k.clone(), Value::Array(vec![v])); + } + Some(e) => { + // We always insert an array + #[expect(clippy::unwrap_used)] + e.as_array_mut().unwrap().push(v); } } } - // `reader` filters blocks for us - _ => unreachable!(), + // We should only have one comment block, + // stop reading when we find it + break; } - - // We should only have one comment block - assert!(!block_reader.has_block()); } return Ok(serde_json::Value::Object(output)); diff --git a/crates/pile-audio/Cargo.toml b/crates/pile-flac/Cargo.toml similarity index 89% rename from crates/pile-audio/Cargo.toml rename to crates/pile-flac/Cargo.toml index 738bc6c..5cdffb1 100644 --- a/crates/pile-audio/Cargo.toml +++ b/crates/pile-flac/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "pile-audio" +name = "pile-flac" version = { workspace = true } rust-version = { workspace = true } edition = { workspace = true } @@ -16,6 +16,5 @@ smartstring = { workspace = true } [dev-dependencies] paste = { workspace = true } -rand = { workspace = true } sha2 = { workspace = true } itertools = { workspace = true } diff --git a/crates/pile-audio/src/flac/blocks/application.rs b/crates/pile-flac/src/blocks/application.rs similarity index 96% rename from crates/pile-audio/src/flac/blocks/application.rs rename to crates/pile-flac/src/blocks/application.rs index b5c0ecc..d312c07 100644 --- a/crates/pile-audio/src/flac/blocks/application.rs +++ b/crates/pile-flac/src/blocks/application.rs @@ -1,10 +1,10 @@ -use crate::flac::errors::{FlacDecodeError, FlacEncodeError}; use std::{ fmt::Debug, io::{Cursor, Read}, }; use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType}; +use crate::{FlacDecodeError, FlacEncodeError}; /// An application block in a flac file pub struct FlacApplicationBlock { diff --git a/crates/pile-audio/src/flac/blocks/audiodata.rs b/crates/pile-flac/src/blocks/audiodata.rs similarity index 93% rename from crates/pile-audio/src/flac/blocks/audiodata.rs rename to crates/pile-flac/src/blocks/audiodata.rs index 6d29eae..ba346d8 100644 --- a/crates/pile-audio/src/flac/blocks/audiodata.rs +++ b/crates/pile-flac/src/blocks/audiodata.rs @@ -1,7 +1,6 @@ +use crate::{FlacDecodeError, FlacEncodeError}; use std::fmt::Debug; -use crate::flac::errors::{FlacDecodeError, FlacEncodeError}; - /// An audio frame in a flac file pub struct FlacAudioFrame { /// The audio frame diff --git a/crates/pile-audio/src/flac/blocks/comment.rs b/crates/pile-flac/src/blocks/comment.rs similarity index 91% rename from crates/pile-audio/src/flac/blocks/comment.rs rename to crates/pile-flac/src/blocks/comment.rs index 91f9fad..79d13e3 100644 --- a/crates/pile-audio/src/flac/blocks/comment.rs +++ b/crates/pile-flac/src/blocks/comment.rs @@ -1,11 +1,7 @@ use std::fmt::Debug; -use crate::{ - common::vorbiscomment::VorbisComment, - flac::errors::{FlacDecodeError, FlacEncodeError}, -}; - use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType}; +use crate::{FlacDecodeError, FlacEncodeError, VorbisComment}; /// A vorbis comment metablock in a flac file pub struct FlacCommentBlock { diff --git a/crates/pile-audio/src/flac/blocks/cuesheet.rs b/crates/pile-flac/src/blocks/cuesheet.rs similarity index 94% rename from crates/pile-audio/src/flac/blocks/cuesheet.rs rename to crates/pile-flac/src/blocks/cuesheet.rs index 107a964..c199118 100644 --- a/crates/pile-audio/src/flac/blocks/cuesheet.rs +++ b/crates/pile-flac/src/blocks/cuesheet.rs @@ -1,8 +1,7 @@ use std::fmt::Debug; -use crate::flac::errors::{FlacDecodeError, FlacEncodeError}; - use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType}; +use crate::{FlacDecodeError, FlacEncodeError}; /// A cuesheet meta in a flac file pub struct FlacCuesheetBlock { diff --git a/crates/pile-audio/src/flac/blocks/header.rs b/crates/pile-flac/src/blocks/header.rs similarity index 97% rename from crates/pile-audio/src/flac/blocks/header.rs rename to crates/pile-flac/src/blocks/header.rs index 48e3a36..162ac44 100644 --- a/crates/pile-audio/src/flac/blocks/header.rs +++ b/crates/pile-flac/src/blocks/header.rs @@ -1,7 +1,7 @@ //! FLAC metablock headers. See spec. use std::fmt::Debug; -use crate::flac::errors::{FlacDecodeError, FlacEncodeError}; +use crate::{FlacDecodeError, FlacEncodeError}; /// A type of flac metadata block #[expect(missing_docs)] diff --git a/crates/pile-audio/src/flac/blocks/mod.rs b/crates/pile-flac/src/blocks/mod.rs similarity index 81% rename from crates/pile-audio/src/flac/blocks/mod.rs rename to crates/pile-flac/src/blocks/mod.rs index 1287f77..e0298d7 100644 --- a/crates/pile-audio/src/flac/blocks/mod.rs +++ b/crates/pile-flac/src/blocks/mod.rs @@ -1,14 +1,11 @@ //! Read and write implementations for all flac block types -// Not metadata blocks mod header; pub use header::{FlacMetablockHeader, FlacMetablockType}; mod audiodata; pub use audiodata::FlacAudioFrame; -// Metadata blocks - mod streaminfo; pub use streaminfo::FlacStreaminfoBlock; @@ -30,15 +27,12 @@ pub use cuesheet::FlacCuesheetBlock; mod comment; pub use comment::FlacCommentBlock; -use super::errors::{FlacDecodeError, FlacEncodeError}; -use std::io::Write; - /// A decode implementation for a /// flac metadata block pub trait FlacMetablockDecode: Sized { /// Try to decode this block from bytes. /// `data` should NOT include the metablock header. - fn decode(data: &[u8]) -> Result; + fn decode(data: &[u8]) -> Result; } /// A encode implementation for a @@ -53,6 +47,6 @@ pub trait FlacMetablockEncode: Sized { &self, is_last: bool, with_header: bool, - target: &mut impl Write, - ) -> Result<(), FlacEncodeError>; + target: &mut impl std::io::Write, + ) -> Result<(), crate::FlacEncodeError>; } diff --git a/crates/pile-audio/src/flac/blocks/padding.rs b/crates/pile-flac/src/blocks/padding.rs similarity index 94% rename from crates/pile-audio/src/flac/blocks/padding.rs rename to crates/pile-flac/src/blocks/padding.rs index 1315ea7..d63bf27 100644 --- a/crates/pile-audio/src/flac/blocks/padding.rs +++ b/crates/pile-flac/src/blocks/padding.rs @@ -1,8 +1,7 @@ use std::{fmt::Debug, io::Read}; -use crate::flac::errors::{FlacDecodeError, FlacEncodeError}; - use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType}; +use crate::{FlacDecodeError, FlacEncodeError}; /// A padding block in a FLAC file. #[derive(Debug)] diff --git a/crates/pile-audio/src/flac/blocks/picture.rs b/crates/pile-flac/src/blocks/picture.rs similarity index 95% rename from crates/pile-audio/src/flac/blocks/picture.rs rename to crates/pile-flac/src/blocks/picture.rs index af4750e..244aebe 100644 --- a/crates/pile-audio/src/flac/blocks/picture.rs +++ b/crates/pile-flac/src/blocks/picture.rs @@ -1,16 +1,11 @@ +use mime::Mime; use std::{ fmt::Debug, io::{Cursor, Read}, }; -use mime::Mime; - -use crate::{ - common::picturetype::PictureType, - flac::errors::{FlacDecodeError, FlacEncodeError}, -}; - use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType}; +use crate::{FlacDecodeError, FlacEncodeError, PictureType}; /// A picture metablock in a flac file pub struct FlacPictureBlock { @@ -59,7 +54,9 @@ impl FlacMetablockDecode for FlacPictureBlock { #[expect(clippy::map_err_ignore)] d.read_exact(&mut block) .map_err(|_| FlacDecodeError::MalformedBlock)?; - let picture_type = PictureType::from_idx(u32::from_be_bytes(block))?; + let picture_type = u32::from_be_bytes(block); + let picture_type = PictureType::from_idx(picture_type) + .ok_or(FlacDecodeError::InvalidPictureType(picture_type))?; // Image format let mime = { diff --git a/crates/pile-audio/src/flac/blocks/seektable.rs b/crates/pile-flac/src/blocks/seektable.rs similarity index 94% rename from crates/pile-audio/src/flac/blocks/seektable.rs rename to crates/pile-flac/src/blocks/seektable.rs index 67065ef..d2120c4 100644 --- a/crates/pile-audio/src/flac/blocks/seektable.rs +++ b/crates/pile-flac/src/blocks/seektable.rs @@ -1,8 +1,7 @@ use std::fmt::Debug; -use crate::flac::errors::{FlacDecodeError, FlacEncodeError}; - use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType}; +use crate::{FlacDecodeError, FlacEncodeError}; /// A seektable block in a flac file pub struct FlacSeektableBlock { diff --git a/crates/pile-audio/src/flac/blocks/streaminfo.rs b/crates/pile-flac/src/blocks/streaminfo.rs similarity index 98% rename from crates/pile-audio/src/flac/blocks/streaminfo.rs rename to crates/pile-flac/src/blocks/streaminfo.rs index ddaac9b..3356d25 100644 --- a/crates/pile-audio/src/flac/blocks/streaminfo.rs +++ b/crates/pile-flac/src/blocks/streaminfo.rs @@ -1,8 +1,7 @@ use std::io::{Cursor, Read}; -use crate::flac::errors::{FlacDecodeError, FlacEncodeError}; - use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType}; +use crate::{FlacDecodeError, FlacEncodeError}; /// A streaminfo block in a flac file #[derive(Debug)] diff --git a/crates/pile-audio/src/flac/errors.rs b/crates/pile-flac/src/errors.rs similarity index 53% rename from crates/pile-audio/src/flac/errors.rs rename to crates/pile-flac/src/errors.rs index 8da733c..938c239 100644 --- a/crates/pile-audio/src/flac/errors.rs +++ b/crates/pile-flac/src/errors.rs @@ -1,8 +1,4 @@ //! FLAC errors -use crate::common::{ - picturetype::PictureTypeError, - vorbiscomment::{VorbisCommentDecodeError, VorbisCommentEncodeError}, -}; use std::string::FromUtf8Error; use thiserror::Error; @@ -22,47 +18,37 @@ pub enum FlacDecodeError { BadMetablockType(u8), /// We encountered an i/o error while processing - #[error("io error while reading flac")] + #[error("i/o error")] IoError(#[from] std::io::Error), - /// We could not parse a vorbis comment - #[error("error while decoding vorbis comment")] - VorbisComment(#[from] VorbisCommentDecodeError), - /// We tried to decode a string, but found invalid UTF-8 - #[error("error while decoding string")] + #[error("could not decode string")] FailedStringDecode(#[from] FromUtf8Error), /// We tried to read a block, but it was out of spec. #[error("malformed flac block")] MalformedBlock, + /// We could not parse a vorbis comment string + #[error("malformed vorbis comment string: {0:?}")] + MalformedCommentString(String), + + /// We could not parse a vorbis comment string + #[error("malformed vorbis picture")] + MalformedPicture(base64::DecodeError), + /// We didn't find frame sync bytes where we expected them #[error("bad frame sync bytes")] BadSyncBytes, - /// We tried to decode a bad picture type - #[error("bad picture type")] - PictureTypeError(#[from] PictureTypeError), + #[error("invalid picture type {0}")] + InvalidPictureType(u32), } #[expect(missing_docs)] #[derive(Debug, Error)] pub enum FlacEncodeError { /// We encountered an i/o error while processing - #[error("io error while encoding block")] + #[error("i/o error")] IoError(#[from] std::io::Error), - - /// We could not encode a picture inside a vorbis comment - #[error("could not encode picture in vorbis comment")] - VorbisPictureEncodeError, -} - -impl From for FlacEncodeError { - fn from(value: VorbisCommentEncodeError) -> Self { - match value { - VorbisCommentEncodeError::IoError(e) => e.into(), - VorbisCommentEncodeError::PictureEncodeError => Self::VorbisPictureEncodeError, - } - } } diff --git a/crates/pile-flac/src/lib.rs b/crates/pile-flac/src/lib.rs new file mode 100644 index 0000000..2790eca --- /dev/null +++ b/crates/pile-flac/src/lib.rs @@ -0,0 +1,19 @@ +pub mod blocks; + +mod tagtype; +pub use tagtype::*; + +mod picturetype; +pub use picturetype::*; + +mod errors; +pub use errors::*; + +mod reader; +pub use reader::*; + +mod vorbiscomment; +pub use vorbiscomment::*; + +#[cfg(test)] +mod tests; diff --git a/crates/pile-audio/src/common/picturetype.rs b/crates/pile-flac/src/picturetype.rs similarity index 82% rename from crates/pile-audio/src/common/picturetype.rs rename to crates/pile-flac/src/picturetype.rs index 5f8783b..1ccd0bc 100644 --- a/crates/pile-audio/src/common/picturetype.rs +++ b/crates/pile-flac/src/picturetype.rs @@ -1,21 +1,5 @@ //! An audio picture type, according to the ID3v2 APIC frame -use std::fmt::Display; - -/// We failed to decode a picture type -#[derive(Debug)] -pub struct PictureTypeError { - idx: u32, -} - -impl Display for PictureTypeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Bad picture type `{}`", self.idx) - } -} - -impl std::error::Error for PictureTypeError {} - /// A picture type according to the ID3v2 APIC frame #[expect(missing_docs)] #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -46,8 +30,8 @@ pub enum PictureType { impl PictureType { /// Try to decode a picture type from the given integer. /// Returns an error if `idx` is invalid. - pub fn from_idx(idx: u32) -> Result { - Ok(match idx { + pub fn from_idx(idx: u32) -> Option { + Some(match idx { 0 => PictureType::Other, 1 => PictureType::PngFileIcon, 2 => PictureType::OtherFileIcon, @@ -69,7 +53,7 @@ impl PictureType { 18 => PictureType::Illustration, 19 => PictureType::ArtistLogotype, 20 => PictureType::PublisherLogotype, - _ => return Err(PictureTypeError { idx }), + _ => return None, }) } diff --git a/crates/pile-flac/src/reader/block.rs b/crates/pile-flac/src/reader/block.rs new file mode 100644 index 0000000..4a8e4a4 --- /dev/null +++ b/crates/pile-flac/src/reader/block.rs @@ -0,0 +1,63 @@ +use std::io::Write; + +use crate::{ + FlacDecodeError, FlacEncodeError, + blocks::{ + FlacApplicationBlock, FlacAudioFrame, FlacCommentBlock, FlacCuesheetBlock, + FlacMetablockDecode, FlacMetablockEncode, FlacMetablockType, FlacPaddingBlock, + FlacPictureBlock, FlacSeektableBlock, FlacStreaminfoBlock, + }, +}; + +#[derive(Debug)] +#[expect(missing_docs)] +pub enum FlacBlock { + Streaminfo(FlacStreaminfoBlock), + Picture(FlacPictureBlock), + Padding(FlacPaddingBlock), + Application(FlacApplicationBlock), + SeekTable(FlacSeektableBlock), + VorbisComment(FlacCommentBlock), + CueSheet(FlacCuesheetBlock), + AudioFrame(FlacAudioFrame), +} + +impl FlacBlock { + /// Encode this block + pub fn encode( + &self, + is_last: bool, + with_header: bool, + target: &mut impl Write, + ) -> Result<(), FlacEncodeError> { + match self { + Self::Streaminfo(b) => b.encode(is_last, with_header, target), + Self::SeekTable(b) => b.encode(is_last, with_header, target), + Self::Picture(b) => b.encode(is_last, with_header, target), + Self::Padding(b) => b.encode(is_last, with_header, target), + Self::Application(b) => b.encode(is_last, with_header, target), + Self::VorbisComment(b) => b.encode(is_last, with_header, target), + Self::CueSheet(b) => b.encode(is_last, with_header, target), + Self::AudioFrame(b) => b.encode(target), + } + } + + /// Try to decode the given data as a block + pub fn decode(block_type: FlacMetablockType, data: &[u8]) -> Result { + Ok(match block_type { + FlacMetablockType::Streaminfo => { + FlacBlock::Streaminfo(FlacStreaminfoBlock::decode(data)?) + } + FlacMetablockType::Application => { + FlacBlock::Application(FlacApplicationBlock::decode(data)?) + } + FlacMetablockType::Cuesheet => FlacBlock::CueSheet(FlacCuesheetBlock::decode(data)?), + FlacMetablockType::Padding => FlacBlock::Padding(FlacPaddingBlock::decode(data)?), + FlacMetablockType::Picture => FlacBlock::Picture(FlacPictureBlock::decode(data)?), + FlacMetablockType::Seektable => FlacBlock::SeekTable(FlacSeektableBlock::decode(data)?), + FlacMetablockType::VorbisComment => { + FlacBlock::VorbisComment(FlacCommentBlock::decode(data)?) + } + }) + } +} diff --git a/crates/pile-flac/src/reader/mod.rs b/crates/pile-flac/src/reader/mod.rs new file mode 100644 index 0000000..77a68e1 --- /dev/null +++ b/crates/pile-flac/src/reader/mod.rs @@ -0,0 +1,9 @@ +mod block; +pub use block::*; + +#[expect(clippy::module_inception)] +mod reader; +pub use reader::*; + +#[cfg(test)] +mod tests; diff --git a/crates/pile-flac/src/reader/reader.rs b/crates/pile-flac/src/reader/reader.rs new file mode 100644 index 0000000..46cd4e0 --- /dev/null +++ b/crates/pile-flac/src/reader/reader.rs @@ -0,0 +1,184 @@ +use std::io::{Read, Seek, SeekFrom}; + +use crate::{ + FlacBlock, FlacDecodeError, + blocks::{FlacAudioFrame, FlacMetablockHeader, FlacMetablockType}, +}; + +// TODO: quickly skip blocks we do not need + +/// The next block we expect to read +enum ReaderState { + MagicBits, + MetablockHeader { is_first: bool }, + MetaBlock { header: FlacMetablockHeader }, + AudioData, + Done, +} + +pub struct FlacReader { + inner: R, + state: ReaderState, +} + +impl FlacReader { + const MIN_AUDIO_FRAME_LEN: usize = 5000; + + pub fn new(inner: R) -> Self { + Self { + inner, + state: ReaderState::MagicBits, + } + } +} + +impl Iterator for FlacReader { + type Item = Result; + + fn next(&mut self) -> Option { + loop { + match &mut self.state { + ReaderState::Done => return None, + + ReaderState::MagicBits => { + let mut data = [0u8; 4]; + if let Err(e) = self.inner.read_exact(&mut data[..4]) { + self.state = ReaderState::Done; + return Some(Err(e.into())); + } + + if data != [0x66, 0x4C, 0x61, 0x43] { + self.state = ReaderState::Done; + return Some(Err(FlacDecodeError::BadMagicBytes)); + } + + self.state = ReaderState::MetablockHeader { is_first: true }; + } + + ReaderState::MetablockHeader { is_first } => { + let mut data = [0u8; 4]; + if let Err(e) = self.inner.read_exact(&mut data[..]) { + self.state = ReaderState::Done; + return Some(Err(e.into())); + } + + let header = match FlacMetablockHeader::decode(&data) { + Ok(h) => h, + Err(e) => { + self.state = ReaderState::Done; + return Some(Err(e)); + } + }; + + if *is_first && !matches!(header.block_type, FlacMetablockType::Streaminfo) { + self.state = ReaderState::Done; + return Some(Err(FlacDecodeError::BadFirstBlock)); + } + + self.state = ReaderState::MetaBlock { header }; + } + + ReaderState::MetaBlock { header } => { + let mut data = vec![0u8; header.length as usize]; + if let Err(e) = self.inner.read_exact(&mut data) { + self.state = ReaderState::Done; + return Some(Err(e.into())); + } + + let block = match FlacBlock::decode(header.block_type, &data) { + Ok(b) => b, + Err(e) => { + self.state = ReaderState::Done; + return Some(Err(e)); + } + }; + + if header.is_last { + self.state = ReaderState::AudioData; + } else { + self.state = ReaderState::MetablockHeader { is_first: false }; + } + + return Some(Ok(block)); + } + + ReaderState::AudioData => { + let mut data = Vec::new(); + loop { + let mut byte = [0u8; 1]; + match self.inner.read_exact(&mut byte) { + Ok(_) => { + data.push(byte[0]); + + if data.len() >= Self::MIN_AUDIO_FRAME_LEN + 2 { + let len = data.len(); + if data[len - 2] == 0b1111_1111 + && data[len - 1] & 0b1111_1100 == 0b1111_1000 + { + let frame_data = data[..len - 2].to_vec(); + + if frame_data.len() < 2 + || frame_data[0] != 0b1111_1111 || frame_data[1] + & 0b1111_1100 + != 0b1111_1000 + { + self.state = ReaderState::Done; + return Some(Err(FlacDecodeError::BadSyncBytes)); + } + + let audio_frame = match FlacAudioFrame::decode(&frame_data) + { + Ok(f) => f, + Err(e) => { + self.state = ReaderState::Done; + return Some(Err(e)); + } + }; + + // Seek back 2 bytes so the next frame starts with the sync bytes + if let Err(e) = self.inner.seek(SeekFrom::Current(-2)) { + self.state = ReaderState::Done; + return Some(Err(e.into())); + } + + self.state = ReaderState::AudioData; + + return Some(Ok(FlacBlock::AudioFrame(audio_frame))); + } + } + } + Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => { + if data.len() > 2 { + if data[0] != 0b1111_1111 + || data[1] & 0b1111_1100 != 0b1111_1000 + { + self.state = ReaderState::Done; + return Some(Err(FlacDecodeError::BadSyncBytes)); + } + + let audio_frame = match FlacAudioFrame::decode(&data) { + Ok(f) => f, + Err(e) => { + self.state = ReaderState::Done; + return Some(Err(e)); + } + }; + + self.state = ReaderState::Done; + return Some(Ok(FlacBlock::AudioFrame(audio_frame))); + } else { + self.state = ReaderState::Done; + return None; + } + } + Err(e) => { + self.state = ReaderState::Done; + return Some(Err(e.into())); + } + } + } + } + } + } + } +} diff --git a/crates/pile-flac/src/reader/tests.rs b/crates/pile-flac/src/reader/tests.rs new file mode 100644 index 0000000..94bf740 --- /dev/null +++ b/crates/pile-flac/src/reader/tests.rs @@ -0,0 +1,393 @@ +use itertools::Itertools; +use paste::paste; +use sha2::{Digest, Sha256}; +use std::{fs::File, io::Write, str::FromStr}; + +use crate::{ + FlacDecodeError, TagType, + tests::{FlacBlockOutput, FlacTestCase, VorbisCommentTestValue}, +}; + +use super::*; + +#[expect(clippy::unwrap_used)] +fn read_file(test_case: &FlacTestCase) -> Result, FlacDecodeError> { + let file_data = std::fs::read(test_case.get_path()).unwrap(); + + // Make sure input file is correct + let mut hasher = Sha256::new(); + hasher.update(&file_data); + assert_eq!( + test_case.get_in_hash(), + hasher.finalize().map(|x| format!("{x:02x}")).join("") + ); + + let file = File::open(test_case.get_path()).unwrap(); + + let mut reader = FlacReader::new(file); + let mut out_blocks = Vec::new(); + + while let Some(b) = reader.next() { + out_blocks.push(b?) + } + + return Ok(out_blocks); +} + +#[expect(clippy::unwrap_used)] +fn test_identical(test_case: &FlacTestCase) -> Result<(), FlacDecodeError> { + let out_blocks = read_file(test_case)?; + + let mut out = Vec::new(); + out.write_all(&[0x66, 0x4C, 0x61, 0x43]).unwrap(); + + for i in 0..out_blocks.len() { + let b = &out_blocks[i]; + let is_last = if i == out_blocks.len() - 1 { + false + } else { + !matches!(b, FlacBlock::AudioFrame(_)) + && matches!(&out_blocks[i + 1], FlacBlock::AudioFrame(_)) + }; + + b.encode(is_last, true, &mut out).unwrap(); + } + + let mut hasher = Sha256::new(); + hasher.update(&out); + let result = hasher.finalize().map(|x| format!("{x:02x}")).join(""); + assert_eq!(result, test_case.get_in_hash(), "Output hash doesn't match"); + return Ok(()); +} + +fn test_blockread(test_case: &FlacTestCase) -> Result<(), FlacDecodeError> { + let out_blocks = read_file(test_case)?; + + assert_eq!( + test_case.get_blocks().unwrap().len(), + out_blocks + .iter() + .filter(|x| !matches!(*x, FlacBlock::AudioFrame(_))) + .count(), + "Number of blocks didn't match" + ); + + let mut audio_data_hasher = Sha256::new(); + let mut result_i = 0; + + for b in out_blocks { + match b { + FlacBlock::Streaminfo(s) => match &test_case.get_blocks().unwrap()[result_i] { + FlacBlockOutput::Streaminfo { + min_block_size, + max_block_size, + min_frame_size, + max_frame_size, + sample_rate, + channels, + bits_per_sample, + total_samples, + md5_signature, + } => { + assert_eq!(*min_block_size, s.min_block_size,); + assert_eq!(*max_block_size, s.max_block_size); + assert_eq!(*min_frame_size, s.min_frame_size); + assert_eq!(*max_frame_size, s.max_frame_size); + assert_eq!(*sample_rate, s.sample_rate); + assert_eq!(*channels, s.channels); + assert_eq!(*bits_per_sample, s.bits_per_sample); + assert_eq!(*total_samples, s.total_samples); + assert_eq!( + *md5_signature, + s.md5_signature.iter().map(|x| format!("{x:02x}")).join("") + ); + } + _ => panic!("Unexpected block type"), + }, + + FlacBlock::Application(a) => match &test_case.get_blocks().unwrap()[result_i] { + FlacBlockOutput::Application { + application_id, + hash, + } => { + assert_eq!( + *application_id, a.application_id, + "Application id doesn't match" + ); + assert_eq!( + *hash, + { + let mut hasher = Sha256::new(); + + hasher.update(&a.data); + hasher.finalize().map(|x| format!("{x:02x}")).join("") + }, + "Application content hash doesn't match" + ); + } + _ => panic!("Unexpected block type"), + }, + + FlacBlock::CueSheet(c) => match &test_case.get_blocks().unwrap()[result_i] { + FlacBlockOutput::CueSheet { hash } => { + assert_eq!(*hash, { + let mut hasher = Sha256::new(); + + hasher.update(&c.data); + hasher.finalize().map(|x| format!("{x:02x}")).join("") + }); + } + _ => panic!("Unexpected block type"), + }, + + FlacBlock::Padding(p) => match &test_case.get_blocks().unwrap()[result_i] { + FlacBlockOutput::Padding { size } => { + assert_eq!(p.size, *size); + } + _ => panic!("Unexpected block type"), + }, + + FlacBlock::SeekTable(t) => match &test_case.get_blocks().unwrap()[result_i] { + FlacBlockOutput::Seektable { hash } => { + assert_eq!(*hash, { + let mut hasher = Sha256::new(); + + hasher.update(&t.data); + hasher.finalize().map(|x| format!("{x:02x}")).join("") + }); + } + _ => panic!("Unexpected block type"), + }, + + FlacBlock::Picture(p) => match &test_case.get_blocks().unwrap()[result_i] { + FlacBlockOutput::Picture { + picture_type, + mime, + description, + width, + height, + bit_depth, + color_count, + img_data, + } => { + assert_eq!(*picture_type, p.picture_type, "{}", test_case.get_name()); + assert_eq!(*mime, p.mime, "{}", test_case.get_name()); + assert_eq!(*description, p.description, "{}", test_case.get_name()); + assert_eq!(*width, p.width, "{}", test_case.get_name()); + assert_eq!(*height, p.height, "{}", test_case.get_name()); + assert_eq!(*bit_depth, p.bit_depth, "{}", test_case.get_name()); + assert_eq!(*color_count, p.color_count, "{}", test_case.get_name()); + assert_eq!( + *img_data, + { + let mut hasher = Sha256::new(); + + hasher.update(&p.img_data); + hasher.finalize().map(|x| format!("{x:02x}")).join("") + }, + "{}", + test_case.get_name() + ); + } + _ => panic!("Unexpected block type"), + }, + + FlacBlock::VorbisComment(v) => match &test_case.get_blocks().unwrap()[result_i] { + FlacBlockOutput::VorbisComment { + vendor, + comments, + pictures, + } => { + assert_eq!(*vendor, v.comment.vendor, "Comment vendor doesn't match"); + + assert_eq!( + v.comment.pictures.len(), + pictures.len(), + "Number of pictures doesn't match" + ); + + for (p, e) in v.comment.pictures.iter().zip(*pictures) { + match e { + FlacBlockOutput::Picture { + picture_type, + mime, + description, + width, + height, + bit_depth, + color_count, + img_data, + } => { + assert_eq!(*picture_type, p.picture_type); + assert_eq!(*mime, p.mime); + assert_eq!(*description, p.description); + assert_eq!(*width, p.width); + assert_eq!(*height, p.height); + assert_eq!(*bit_depth, p.bit_depth); + assert_eq!(*color_count, p.color_count); + assert_eq!(*img_data, { + let mut hasher = Sha256::new(); + hasher.update(&p.img_data); + hasher.finalize().map(|x| format!("{x:02x}")).join("") + }); + } + _ => panic!("Bad test data: expected only Picture blocks."), + } + } + + match comments { + VorbisCommentTestValue::Raw { tags } => { + assert_eq!( + v.comment.comments.len(), + tags.len(), + "Number of comments doesn't match" + ); + + for ((got_tag, got_val), (exp_tag, exp_val)) in + v.comment.comments.iter().zip(*tags) + { + assert_eq!( + *got_tag, + TagType::from_str(exp_tag).unwrap(), + "Tag key doesn't match" + ); + assert_eq!( + got_val, exp_val, + "Tag value of {exp_tag} doesn't match" + ); + } + } + + VorbisCommentTestValue::Hash { n_comments, hash } => { + assert_eq!( + v.comment.comments.len(), + *n_comments, + "Number of comments doesn't match" + ); + + let mut hasher = Sha256::new(); + + for (got_tag, got_val) in &v.comment.comments { + hasher.update(format!("{}={got_val};", got_tag.to_vorbis_string()).as_bytes()); + } + assert_eq!( + &hasher.finalize().map(|x| format!("{x:02x}")).join(""), + hash, + "Comment hash doesn't match" + ); + } + } + } + _ => panic!("Unexpected block type"), + }, + + FlacBlock::AudioFrame(data) => { + let mut vec = Vec::new(); + data.encode(&mut vec).unwrap(); + audio_data_hasher.update(&vec); + + if result_i != test_case.get_blocks().unwrap().len() { + panic!("There are metadata blocks between audio frames!") + } + + // Don't increment result_i + continue; + } + } + + result_i += 1; + } + + // Check audio data hash + assert_eq!( + test_case.get_audio_hash().unwrap(), + audio_data_hasher + .finalize() + .map(|x| format!("{x:02x}")) + .join("") + ); + + return Ok(()); +} + +// Helper macros to generate tests +macro_rules! gen_tests { + ( $test_name:ident ) => { + paste! { + #[test] + pub fn []() { + let manifest = crate::tests::manifest(); + let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap(); + + match test_case { + FlacTestCase::Success { .. } => { + for _ in 0..5 { + test_blockread( + test_case, + ).unwrap() + } + }, + + FlacTestCase::Error { check_error, .. } => { + let e = test_blockread(test_case); + match e { + Err(e) => assert!(check_error(&e), "Unexpected error {e:?}"), + _ => panic!("Unexpected error {e:?}") + } + } + } + } + + #[test] + pub fn []() { + let manifest = crate::tests::manifest(); + let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap(); + + match test_case { + FlacTestCase::Success { .. } => { + for _ in 0..5 { + test_identical( + test_case, + ).unwrap() + } + }, + + FlacTestCase::Error { check_error, .. } => { + let e = test_identical(test_case); + match e { + Err(e) => assert!(check_error(&e), "Unexpected error {e:?}"), + _ => panic!("Unexpected error {e:?}") + } + } + } + } + } + }; +} + +gen_tests!(custom_01); +gen_tests!(custom_02); +gen_tests!(custom_03); + +gen_tests!(uncommon_10); + +gen_tests!(faulty_06); +gen_tests!(faulty_07); +gen_tests!(faulty_10); +gen_tests!(faulty_11); + +gen_tests!(subset_45); +gen_tests!(subset_46); +gen_tests!(subset_47); +gen_tests!(subset_48); +gen_tests!(subset_49); +gen_tests!(subset_50); +gen_tests!(subset_51); +gen_tests!(subset_52); +gen_tests!(subset_53); +gen_tests!(subset_54); +gen_tests!(subset_55); +gen_tests!(subset_56); +gen_tests!(subset_57); +gen_tests!(subset_58); +gen_tests!(subset_59); diff --git a/crates/pile-audio/src/common/tagtype.rs b/crates/pile-flac/src/tagtype.rs similarity index 56% rename from crates/pile-audio/src/common/tagtype.rs rename to crates/pile-flac/src/tagtype.rs index 2ce04ea..6e6cc20 100644 --- a/crates/pile-audio/src/common/tagtype.rs +++ b/crates/pile-flac/src/tagtype.rs @@ -4,37 +4,75 @@ use strum::{Display, EnumString}; /// A universal tag type #[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, EnumString, Display)] +#[strum(ascii_case_insensitive)] pub enum TagType { /// A tag we didn't recognize + #[strum(default)] Other(SmartString), /// Album name Album, + /// Album artist AlbumArtist, + /// Comment Comment, + /// Release date ReleaseDate, + /// Disk number DiskNumber, + /// Total disks in album DiskTotal, + /// Genre Genre, + /// International standard recording code Isrc, + /// Track lyrics, possibly time-coded Lyrics, + /// This track's number in its album TrackNumber, + /// The total number of tracks in this track's album TrackTotal, + /// The title of this track TrackTitle, + /// This track's artist (the usual `Artist`, /// compare to `AlbumArtist`) TrackArtist, + /// The year this track was released Year, } + +impl TagType { + pub fn to_vorbis_string(&self) -> String { + match self { + TagType::TrackTitle => "TITLE", + TagType::Album => "ALBUM", + TagType::TrackNumber => "TRACKNUMBER", + TagType::TrackArtist => "ARTIST", + TagType::AlbumArtist => "ALBUMARTIST", + TagType::Genre => "GENRE", + TagType::Isrc => "ISRC", + TagType::ReleaseDate => "DATE", + TagType::TrackTotal => "TOTALTRACKS", + TagType::Lyrics => "LYRICS", + TagType::Comment => "COMMENT", + TagType::DiskNumber => "DISKNUMBER", + TagType::DiskTotal => "DISKTOTAL", + TagType::Year => "YEAR", + TagType::Other(x) => x, + } + .to_uppercase() + } +} diff --git a/crates/pile-flac/src/tests.rs b/crates/pile-flac/src/tests.rs new file mode 100644 index 0000000..2bd877e --- /dev/null +++ b/crates/pile-flac/src/tests.rs @@ -0,0 +1,919 @@ +use std::str::FromStr; + +use itertools::Itertools; +use mime::Mime; + +use crate::PictureType; + +use super::errors::FlacDecodeError; + +/// The value of a vorbis comment. +/// +/// Some files have VERY large comments, and providing them +/// explicitly here doesn't make sense. +#[derive(Clone)] +pub enum VorbisCommentTestValue { + /// The comments, in order + Raw { + tags: &'static [(&'static str, &'static str)], + }, + /// The hash of all comments concatenated together, + /// stringified as `{key}={value};` + Hash { + n_comments: usize, + hash: &'static str, + }, +} + +#[derive(Clone)] +pub enum FlacBlockOutput { + Application { + application_id: u32, + hash: &'static str, + }, + Streaminfo { + min_block_size: u32, + max_block_size: u32, + min_frame_size: u32, + max_frame_size: u32, + sample_rate: u32, + channels: u8, + bits_per_sample: u8, + total_samples: u128, + md5_signature: &'static str, + }, + CueSheet { + // Hash of this block's data, without the header. + // This is easy to get with + // + // ```notrust + // metaflac \ + // --list \ + // --block-number= \ + // --data-format=binary-headerless \ + // \ + // | sha256sum + //``` + hash: &'static str, + }, + Seektable { + hash: &'static str, + }, + Padding { + size: u32, + }, + Picture { + picture_type: PictureType, + mime: Mime, + description: &'static str, + width: u32, + height: u32, + bit_depth: u32, + color_count: u32, + img_data: &'static str, + }, + VorbisComment { + vendor: &'static str, + comments: VorbisCommentTestValue, + pictures: &'static [FlacBlockOutput], + }, +} + +pub enum FlacTestCase { + Success { + /// This test's name + test_name: &'static str, + + /// The file to use for this test + file_path: &'static str, + + /// The hash of the input files + in_hash: &'static str, + + /// The flac metablocks we expect to find in this file, in order. + blocks: Vec, + + /// The hash of the audio frames in this file + /// + /// Get this hash by running `metaflac --remove-all --dont-use-padding`, + /// then by manually deleting remaining headers in a hex editor + /// (Remember that the sync sequence is 0xFF 0xF8) + audio_hash: &'static str, + + /// The hash we should get when we strip this file's tags. + /// + /// A stripped flac file has unmodified STREAMINFO, SEEKTABLE, + /// CUESHEET, and audio data blocks; and nothing else (not even padding). + /// + /// Reference implementation: + /// ```notrust + /// metaflac \ + /// --remove \ + /// --block-type=PADDING,APPLICATION,VORBIS_COMMENT,PICTURE \ + /// --dont-use-padding \ + /// + /// ``` + stripped_hash: &'static str, + }, + Error { + /// This test's name + test_name: &'static str, + + /// The file to use for this test + file_path: &'static str, + + /// The hash of the input files + in_hash: &'static str, + + /// The error we should encounter while reading this file + check_error: &'static dyn Fn(&FlacDecodeError) -> bool, + + /// If some, stripping this file's metadata should produce the given hash. + /// If none, trying to strip metadata should produce `check_error` + stripped_hash: Option<&'static str>, + + /// If some, the following images should be extracted from this file + /// If none, trying to strip images should produce `check_error` + pictures: Option>, + }, +} + +#[expect(dead_code)] +impl FlacTestCase { + pub fn get_name(&self) -> &str { + match self { + Self::Error { test_name, .. } | Self::Success { test_name, .. } => test_name, + } + } + + pub fn get_path(&self) -> &str { + match self { + Self::Success { file_path, .. } | Self::Error { file_path, .. } => file_path, + } + } + + pub fn get_in_hash(&self) -> &str { + match self { + Self::Success { in_hash, .. } | Self::Error { in_hash, .. } => in_hash, + } + } + + pub fn get_stripped_hash(&self) -> Option<&str> { + match self { + Self::Success { stripped_hash, .. } => Some(stripped_hash), + Self::Error { stripped_hash, .. } => *stripped_hash, + } + } + + pub fn get_audio_hash(&self) -> Option<&str> { + match self { + Self::Success { audio_hash, .. } => Some(audio_hash), + _ => None, + } + } + + pub fn get_blocks(&self) -> Option<&[FlacBlockOutput]> { + match self { + Self::Success { blocks, .. } => Some(blocks), + _ => None, + } + } + + pub fn get_pictures(&self) -> Option> { + match self { + Self::Success { blocks, .. } => { + let mut out = Vec::new(); + for b in blocks { + match b { + FlacBlockOutput::Picture { .. } => out.push(b.clone()), + FlacBlockOutput::VorbisComment { pictures, .. } => { + for p in *pictures { + out.push(p.clone()) + } + } + _ => {} + } + } + + return Some(out); + } + + Self::Error { pictures, .. } => pictures.as_ref().map(|x| x.iter().cloned().collect()), + } + } +} + +/// A list of test files and their expected output +#[expect(clippy::unwrap_used)] +pub fn manifest() -> [FlacTestCase; 23] { + [ + FlacTestCase::Error { + test_name: "uncommon_10", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_uncommon/10 - file starting at frame header.flac" + ), + in_hash: "d95f63e8101320f5ac7ffe249bc429a209eb0e10996a987301eaa63386a8faa1", + check_error: &|x| matches!(x, FlacDecodeError::BadMagicBytes), + stripped_hash: None, + pictures: None, + }, + FlacTestCase::Error { + test_name: "faulty_06", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_faulty/06 - missing streaminfo metadata block.flac" + ), + in_hash: "53aed5e7fde7a652b82ba06a8382b2612b02ebbde7b0d2016276644d17cc76cd", + check_error: &|x| matches!(x, FlacDecodeError::BadFirstBlock), + stripped_hash: None, + pictures: None, + }, + FlacTestCase::Error { + test_name: "faulty_07", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_faulty/07 - other metadata blocks preceding streaminfo metadata block.flac" + ), + in_hash: "6d46725991ba5da477187fde7709ea201c399d00027257c365d7301226d851ea", + check_error: &|x| matches!(x, FlacDecodeError::BadFirstBlock), + stripped_hash: None, + pictures: None, + }, + FlacTestCase::Error { + test_name: "faulty_10", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_faulty/10 - invalid vorbis comment metadata block.flac" + ), + in_hash: "c79b0514a61634035a5653c5493797bbd1fcc78982116e4d429630e9e462d29b", + check_error: &|x| matches!(x, FlacDecodeError::IoError(_)), + // This file's vorbis comment is invalid, but that shouldn't stop us from removing it. + // As a general rule, we should NOT encounter an error when stripping invalid blocks. + // + // We should, however, get errors when we try to strip flac files with invalid structure. + // (For example, the out-of-order streaminfo test in faulty_07). + stripped_hash: Some("4b994f82dc1699a58e2b127058b37374220ee41dc294d4887ac14f056291a1b0"), + pictures: None, + }, + FlacTestCase::Error { + test_name: "faulty_11", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_faulty/11 - incorrect metadata block length.flac" + ), + in_hash: "3732151ba8c4e66a785165aa75a444aad814c16807ddc97b793811376acacfd6", + check_error: &|x| matches!(x, FlacDecodeError::BadMetablockType(127)), + stripped_hash: None, + pictures: None, + }, + FlacTestCase::Success { + test_name: "subset_45", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/45 - no total number of samples set.flac" + ), + in_hash: "336a18eb7a78f7fc0ab34980348e2895bc3f82db440a2430d9f92e996f889f9a", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 907, + max_frame_size: 8053, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 0, + md5_signature: "c41ae3b82c35d8f5c3dab1729f948fde", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + ], + audio_hash: "3fb3482ebc1724559bdd57f34de472458563d78a676029614e76e32b5d2b8816", + stripped_hash: "31631ac227ebe2689bac7caa1fa964b47e71a9f1c9c583a04ea8ebd9371508d0", + }, + FlacTestCase::Success { + test_name: "subset_46", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/46 - no min-max framesize set.flac" + ), + in_hash: "9dc39732ce17815832790901b768bb50cd5ff0cd21b28a123c1cabc16ed776cc", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 0, + max_frame_size: 0, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 282866, + md5_signature: "fd131e6ebc75251ed83f8f4c07df36a4", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + ], + audio_hash: "a1eed422462b386a932b9eb3dff3aea3687b41eca919624fb574aadb7eb50040", + stripped_hash: "9e57cd77f285fc31f87fa4e3a31ab8395d68d5482e174c8e0d0bba9a0c20ba27", + }, + FlacTestCase::Success { + test_name: "subset_47", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/47 - only STREAMINFO.flac" + ), + in_hash: "9a62c79f634849e74cb2183f9e3a9bd284f51e2591c553008d3e6449967eef85", + blocks: vec![FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 4747, + max_frame_size: 7034, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 232608, + md5_signature: "bba30c5f70789910e404b7ac727c3853", + }], + audio_hash: "5ee1450058254087f58c91baf0f70d14bde8782cf2dc23c741272177fe0fce6e", + stripped_hash: "9a62c79f634849e74cb2183f9e3a9bd284f51e2591c553008d3e6449967eef85", + }, + FlacTestCase::Success { + test_name: "subset_48", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/48 - Extremely large SEEKTABLE.flac" + ), + in_hash: "4417aca6b5f90971c50c28766d2f32b3acaa7f9f9667bd313336242dae8b2531", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 2445, + max_frame_size: 7364, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 259884, + md5_signature: "97a0574290237563fbaa788ad77d2cdf", + }, + FlacBlockOutput::Seektable { + hash: "21ca2184ae22fe26b690fd7cbd8d25fcde1d830ff6e5796ced4107bab219d7c0", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + ], + audio_hash: "c2d691f2c4c986fe3cd5fd7864d9ba9ce6dd68a4ffc670447f008434b13102c2", + stripped_hash: "abc9a0c40a29c896bc6e1cc0b374db1c8e157af716a5a3c43b7db1591a74c4e8", + }, + FlacTestCase::Success { + test_name: "subset_49", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/49 - Extremely large PADDING.flac", + ), + in_hash: "7bc44fa2754536279fde4f8fb31d824f43b8d0b3f93d27d055d209682914f20e", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 1353, + max_frame_size: 7117, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 258939, + md5_signature: "6e78f221caaaa5d570a53f1714d84ded", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + FlacBlockOutput::Padding { size: 16777215 }, + ], + audio_hash: "5007be7109b28b0149d1b929d2a0e93a087381bd3e68cf2a3ef78ea265ea20c3", + stripped_hash: "a2283bbacbc4905ad3df1bf9f43a0ea7aa65cf69523d84a7dd8eb54553cc437e", + }, + FlacTestCase::Success { + test_name: "subset_50", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/50 - Extremely large PICTURE.flac" + ), + in_hash: "1f04f237d74836104993a8072d4223e84a5d3bd76fbc44555c221c7e69a23594", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 5099, + max_frame_size: 7126, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 265617, + md5_signature: "82164e4da30ed43b47e6027cef050648", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + FlacBlockOutput::Picture { + picture_type: PictureType::FrontCover, + mime: mime::IMAGE_JPEG, + description: "", + width: 3200, + height: 2252, + bit_depth: 24, + color_count: 0, + img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563", + }, + ], + audio_hash: "9778b25c5d1f56cfcd418e550baed14f9d6a4baf29489a83ed450fbebb28de8c", + stripped_hash: "20df129287d94f9ae5951b296d7f65fcbed92db423ba7db4f0d765f1f0a7e18c", + }, + FlacTestCase::Success { + test_name: "subset_51", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/51 - Extremely large VORBISCOMMENT.flac" + ), + in_hash: "033160e8124ed287b0b5d615c94ac4139477e47d6e4059b1c19b7141566f5ef9", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 4531, + max_frame_size: 7528, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 289972, + md5_signature: "5ff622c88f8dd9bc201a6a541f3890d3", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Hash { + n_comments: 39, + hash: "01984e9ec0cfad41f27b3b4e84184966f6725ead84b7815bd0b3313549ee4229", + }, + pictures: &[], + }, + ], + audio_hash: "76419865d10eb22a74f020423a4e515e800f0177441676afd0418557c2d76c36", + stripped_hash: "c0ca6c6099b5d9ec53d6bb370f339b2b1570055813a6cd3616fac2db83a2185e", + }, + FlacTestCase::Success { + test_name: "subset_52", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/52 - Extremely large APPLICATION.flac" + ), + in_hash: "0e45a4f8dbef15cbebdd8dfe690d8ae60e0c6abb596db1270a9161b62a7a3f1c", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 3711, + max_frame_size: 7056, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 317876, + md5_signature: "eb7140266bc194527488c21ab49bc47b", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + FlacBlockOutput::Application { + application_id: 0x74657374, + hash: "cfc0b8969e4ba6bd507999ba89dea2d274df69d94749d6ae3cf117a7780bba09", + }, + ], + audio_hash: "89ad1a5c86a9ef35d33189c81c8a90285a23964a13f8325bf2c02043e8c83d63", + stripped_hash: "cc4a0afb95ec9bcde8ee33f13951e494dc4126a9a3a668d79c80ce3c14a3acd9", + }, + FlacTestCase::Success { + test_name: "subset_53", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/53 - CUESHEET with very many indexes.flac" + ), + in_hash: "513fad18578f3225fae5de1bda8f700415be6fd8aa1e7af533b5eb796ed2d461", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 2798, + max_frame_size: 7408, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 2910025, + md5_signature: "d11f3717d628cfe6a90a10facc478340", + }, + FlacBlockOutput::Seektable { + hash: "18629e1b874cb27e4364da72fb3fec2141eb0618baae4a1cee6ed09562aa00a8", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + FlacBlockOutput::CueSheet { + hash: "70638a241ca06881a52c0a18258ea2d8946a830137a70479c49746d2a1344bdd", + }, + ], + audio_hash: "e993070f2080f2c598be1d61d208e9187a55ddea4be1d2ed1f8043e7c03e97a5", + stripped_hash: "57c5b945e14c6fcd06916d6a57e5b036d67ff35757893c24ed872007aabbcf4b", + }, + FlacTestCase::Success { + test_name: "subset_54", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/54 - 1000x repeating VORBISCOMMENT.flac" + ), + in_hash: "b68dc6644784fac35aa07581be8603a360d1697e07a2265d7eb24001936fd247", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 1694, + max_frame_size: 7145, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 433151, + md5_signature: "1d950e92b357dedbc5290a7f2210a2ef", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Hash { + n_comments: 20000, + hash: "0371b6f158411f35121f1d62ccbc18c90c9b1b0263e51bfc1b8fc942892eaf12", + }, + pictures: &[], + }, + ], + audio_hash: "4721b784058410c6263f73680079e9a71aee914c499afcf5580c121fce00e874", + stripped_hash: "5c8b92b83c0fa17821add38263fa323d1c66cfd2ee57aca054b50bd05b9df5c2", + }, + FlacTestCase::Success { + test_name: "subset_55", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/55 - file 48-53 combined.flac" + ), + in_hash: "a756b460df79b7cc492223f80cda570e4511f2024e5fa0c4d505ba51b86191f6", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 3103, + max_frame_size: 11306, + sample_rate: 44100, + channels: 2, + bits_per_sample: 16, + total_samples: 2646000, + md5_signature: "2c78978cbbff11daac296fee97c3e061", + }, + FlacBlockOutput::Seektable { + hash: "58dfa7bac4974edf1956b068f5aa72d1fbd9301c36a3085a8a57b9db11a2dbf0", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.3 20190804", + comments: VorbisCommentTestValue::Hash { + n_comments: 40036, + hash: "8d8f21954b4aaee2c7ec92389125a9b28b7de5a8153c62abdd80330f445214df", + }, + pictures: &[], + }, + FlacBlockOutput::CueSheet { + hash: "db11916c8f5f39648256f93f202e00ff8d73d7d96b62f749b4c77cf3ea744f90", + }, + FlacBlockOutput::Application { + application_id: 0x74657374, + hash: "6088a557a1bad7bfa5ebf79a324669fbf4fa2f8e708f5487305dfc5b2ff2249a", + }, + FlacBlockOutput::Picture { + picture_type: PictureType::FrontCover, + mime: mime::IMAGE_JPEG, + description: "", + width: 3200, + height: 2252, + bit_depth: 24, + color_count: 0, + img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563", + }, + FlacBlockOutput::Padding { size: 16777215 }, + ], + audio_hash: "f1285b77cec7fa9a0979033244489a9d06b8515b2158e9270087a65a4007084d", + stripped_hash: "401038fce06aff5ebdc7a5f2fc01fa491cbf32d5da9ec99086e414b2da3f8449", + }, + FlacTestCase::Success { + test_name: "subset_56", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/56 - JPG PICTURE.flac" + ), + in_hash: "5cebe7a3710cf8924bd2913854e9ca60b4cd53cfee5a3af0c3c73fddc1888963", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 3014, + max_frame_size: 7219, + sample_rate: 44100, + channels: 2, + bits_per_sample: 16, + total_samples: 220026, + md5_signature: "5b0e898d9c2626d0c28684f5a586813f", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + FlacBlockOutput::Picture { + picture_type: PictureType::FrontCover, + mime: mime::IMAGE_JPEG, + description: "", + width: 1920, + height: 1080, + bit_depth: 24, + color_count: 0, + img_data: "7a3ed658f80f433eee3914fff451ea0312807de0af709e37cc6a4f3f6e8a47c6", + }, + ], + audio_hash: "ccfe90b0f15cd9662f7a18f40cd4c347538cf8897a08228e75351206f7804573", + stripped_hash: "31a38d59db2010790b7abf65ec0cc03f2bbe1fed5952bc72bee4ca4d0c92e79f", + }, + FlacTestCase::Success { + test_name: "subset_57", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/57 - PNG PICTURE.flac" + ), + in_hash: "c6abff7f8bb63c2821bd21dd9052c543f10ba0be878e83cb419c248f14f72697", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 463, + max_frame_size: 6770, + sample_rate: 44100, + channels: 2, + bits_per_sample: 16, + total_samples: 221623, + md5_signature: "ad16957bcf8d5a3ec8caf261e43d5ff7", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + FlacBlockOutput::Picture { + picture_type: PictureType::FrontCover, + mime: mime::IMAGE_PNG, + description: "", + width: 960, + height: 540, + bit_depth: 24, + color_count: 0, + img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2", + }, + ], + audio_hash: "39bf9981613ac2f35d253c0c21b76a48abba7792c27da5dbf23e6021e2e6673f", + stripped_hash: "3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1", + }, + FlacTestCase::Success { + test_name: "subset_58", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/58 - GIF PICTURE.flac" + ), + in_hash: "7c2b1a963a665847167a7275f9924f65baeb85c21726c218f61bf3f803f301c8", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 2853, + max_frame_size: 6683, + sample_rate: 44100, + channels: 2, + bits_per_sample: 16, + total_samples: 219826, + md5_signature: "7c1810602a7db96d7a48022ac4aa495c", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + FlacBlockOutput::Picture { + picture_type: PictureType::FrontCover, + mime: mime::IMAGE_GIF, + description: "", + width: 1920, + height: 1080, + bit_depth: 24, + color_count: 32, + img_data: "e33cccc1d799eb2bb618f47be7099cf02796df5519f3f0e1cc258606cf6e8bb1", + }, + ], + audio_hash: "30e3292e9f56cf88658eeadfdec8ad3a440690ce6d813e1b3374f60518c8e0ae", + stripped_hash: "4cd771e27870e2a586000f5b369e0426183a521b61212302a2f5802b046910b2", + }, + FlacTestCase::Success { + test_name: "subset_59", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/59 - AVIF PICTURE.flac" + ), + in_hash: "7395d02bf8d9533dc554cce02dee9de98c77f8731a45f62d0a243bd0d6f9a45c", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 153, + max_frame_size: 7041, + sample_rate: 44100, + channels: 2, + bits_per_sample: 16, + total_samples: 221423, + md5_signature: "d354246011ca204159c06f52cad5f634", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + FlacBlockOutput::Picture { + picture_type: PictureType::FrontCover, + mime: Mime::from_str("image/avif").unwrap(), + description: "", + width: 1920, + height: 1080, + bit_depth: 24, + color_count: 0, + img_data: "a431123040c74f75096237f20544a7fb56b4eb71ddea62efa700b0a016f5b2fc", + }, + ], + audio_hash: "b208c73d274e65b27232bfffbfcbcf4805ee3cbc9cfbf7d2104db8f53370273b", + stripped_hash: "d5215e16c6b978fc2c3e6809e1e78981497cb8514df297c5169f3b4a28fd875c", + }, + FlacTestCase::Success { + test_name: "custom_01", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_custom/01 - many images.flac" + ), + in_hash: "8a5df37488866cd91ac16773e549ef4e3a85d9f88a0d9d345f174807bb536b96", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 5099, + max_frame_size: 7126, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 265617, + md5_signature: "82164e4da30ed43b47e6027cef050648", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[FlacBlockOutput::Picture { + picture_type: PictureType::FrontCover, + mime: mime::IMAGE_PNG, + description: "", + width: 960, + height: 540, + bit_depth: 24, + color_count: 0, + img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2", + }], + }, + FlacBlockOutput::Picture { + picture_type: PictureType::FrontCover, + mime: mime::IMAGE_JPEG, + description: "", + width: 3200, + height: 2252, + bit_depth: 24, + color_count: 0, + img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563", + }, + FlacBlockOutput::Picture { + picture_type: PictureType::ABrightColoredFish, + mime: mime::IMAGE_JPEG, + description: "lorem", + width: 1920, + height: 1080, + bit_depth: 24, + color_count: 0, + img_data: "7a3ed658f80f433eee3914fff451ea0312807de0af709e37cc6a4f3f6e8a47c6", + }, + FlacBlockOutput::Picture { + picture_type: PictureType::OtherFileIcon, + mime: mime::IMAGE_PNG, + description: "ipsum", + width: 960, + height: 540, + bit_depth: 24, + color_count: 0, + img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2", + }, + FlacBlockOutput::Picture { + picture_type: PictureType::Lyricist, + mime: mime::IMAGE_GIF, + description: "dolor", + width: 1920, + height: 1080, + bit_depth: 24, + color_count: 32, + img_data: "e33cccc1d799eb2bb618f47be7099cf02796df5519f3f0e1cc258606cf6e8bb1", + }, + FlacBlockOutput::Picture { + picture_type: PictureType::BackCover, + mime: Mime::from_str("image/avif").unwrap(), + description: "est", + width: 1920, + height: 1080, + bit_depth: 24, + color_count: 0, + img_data: "a431123040c74f75096237f20544a7fb56b4eb71ddea62efa700b0a016f5b2fc", + }, + ], + audio_hash: "9778b25c5d1f56cfcd418e550baed14f9d6a4baf29489a83ed450fbebb28de8c", + stripped_hash: "20df129287d94f9ae5951b296d7f65fcbed92db423ba7db4f0d765f1f0a7e18c", + }, + FlacTestCase::Success { + test_name: "custom_02", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_custom/02 - picture in vorbis comment.flac" + ), + in_hash: "f6bb1a726fe6a3e25a4337d36e29fdced8ff01a46d627b7c2e1988c88f461f8c", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 463, + max_frame_size: 6770, + sample_rate: 44100, + channels: 2, + bits_per_sample: 16, + total_samples: 221623, + md5_signature: "ad16957bcf8d5a3ec8caf261e43d5ff7", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[FlacBlockOutput::Picture { + picture_type: PictureType::FrontCover, + mime: mime::IMAGE_PNG, + description: "", + width: 960, + height: 540, + bit_depth: 24, + color_count: 0, + img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2", + }], + }, + ], + audio_hash: "39bf9981613ac2f35d253c0c21b76a48abba7792c27da5dbf23e6021e2e6673f", + stripped_hash: "3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1", + }, + FlacTestCase::Error { + test_name: "custom_03", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_custom/03 - faulty picture in vorbis comment.flac" + ), + in_hash: "7177f0ae4f04a563292be286ec05967f81ab16eb0a28b70fc07a1e47da9cafd0", + check_error: &|x| matches!(x, FlacDecodeError::MalformedPicture(_)), + stripped_hash: Some("3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1"), + pictures: None, + }, + ] +} + +#[test] +fn manifest_sanity_check() { + assert!(manifest().iter().map(|x| x.get_name()).all_unique()); + assert!(manifest().iter().map(|x| x.get_path()).all_unique()); +} diff --git a/crates/pile-flac/src/vorbiscomment.rs b/crates/pile-flac/src/vorbiscomment.rs new file mode 100644 index 0000000..237c02a --- /dev/null +++ b/crates/pile-flac/src/vorbiscomment.rs @@ -0,0 +1,194 @@ +//! Decode and write Vorbis comment blocks + +use base64::Engine; +use smartstring::{LazyCompact, SmartString}; +use std::{ + io::{Cursor, Read, Write}, + str::FromStr, +}; + +use super::tagtype::TagType; +use crate::{ + FlacDecodeError, FlacEncodeError, + blocks::{FlacMetablockDecode, FlacMetablockEncode, FlacPictureBlock}, +}; + +/// A decoded vorbis comment block +#[derive(Debug)] +pub struct VorbisComment { + /// This comment's vendor string + pub vendor: SmartString, + + /// List of (tag, value) + /// Repeated tags are allowed! + pub comments: Vec<(TagType, SmartString)>, + + /// A list of pictures found in this comment + pub pictures: Vec, +} + +impl VorbisComment { + /// Try to decode the given data as a vorbis comment block + pub fn decode(data: &[u8]) -> Result { + let mut d = Cursor::new(data); + + // This is re-used whenever we need to read four bytes + let mut block = [0u8; 4]; + + let vendor = { + d.read_exact(&mut block)?; + let length = u32::from_le_bytes(block); + + #[expect(clippy::expect_used)] + let mut text = vec![ + 0u8; + length + .try_into() + .expect("vendor length does not fit into usize") + ]; + + d.read_exact(&mut text)?; + String::from_utf8(text)? + }; + + d.read_exact(&mut block)?; + + #[expect(clippy::expect_used)] + let n_comments: usize = u32::from_le_bytes(block) + .try_into() + .expect("comment count does not fit into usize"); + + let mut comments = Vec::new(); + let mut pictures = Vec::new(); + for _ in 0..n_comments { + let comment = { + d.read_exact(&mut block)?; + + let length = u32::from_le_bytes(block); + + #[expect(clippy::expect_used)] + let mut text = vec![ + 0u8; + length + .try_into() + .expect("comment length does not fit into usize") + ]; + + d.read_exact(&mut text)?; + + String::from_utf8(text)? + }; + let (var, val) = comment + .split_once('=') + .ok_or(FlacDecodeError::MalformedCommentString(comment.clone()))?; + + if !val.is_empty() { + if var.to_uppercase() == "METADATA_BLOCK_PICTURE" { + pictures.push(FlacPictureBlock::decode( + &base64::prelude::BASE64_STANDARD + .decode(val) + .map_err(FlacDecodeError::MalformedPicture)?, + )?); + } else { + // Make sure empty strings are saved as "None" + comments.push(( + TagType::from_str(var).unwrap_or(TagType::Other(var.into())), + val.into(), + )); + } + }; + } + + Ok(Self { + vendor: vendor.into(), + comments, + pictures, + }) + } +} + +impl VorbisComment { + /// Get the number of bytes that `encode()` will write. + #[expect(clippy::expect_used)] + pub fn get_len(&self) -> u32 { + let mut sum: u32 = 0; + sum += u32::try_from(self.vendor.len()).expect("vendor length does not fit into u32") + 4; + sum += 4; + + for (tagtype, value) in &self.comments { + let tagtype_str = tagtype.to_vorbis_string(); + let str = format!("{tagtype_str}={value}"); + sum += + 4 + u32::try_from(str.len()).expect("comment string length does not fit into u32"); + } + + for p in &self.pictures { + // Compute b64 len + let mut x = p.get_len(); + if x % 3 != 0 { + x -= x % 3; + x += 3; + } + + #[expect(clippy::integer_division)] + { + sum += 4 * (x / 3); + } + + // Add "METADATA_BLOCK_PICTURE=" + sum += 23; + + // Add length bytes + sum += 4; + } + + return sum; + } + + /// Try to encode this vorbis comment + #[expect(clippy::expect_used)] + pub fn encode(&self, target: &mut impl Write) -> Result<(), FlacEncodeError> { + target.write_all( + &u32::try_from(self.vendor.len()) + .expect("vendor length does not fit into u32") + .to_le_bytes(), + )?; + target.write_all(self.vendor.as_bytes())?; + + target.write_all( + &u32::try_from(self.comments.len() + self.pictures.len()) + .expect("total comment count does not fit into u32") + .to_le_bytes(), + )?; + + for (tagtype, value) in &self.comments { + let tagtype_str = tagtype.to_vorbis_string(); + let str = format!("{tagtype_str}={value}"); + target.write_all( + &u32::try_from(str.len()) + .expect("comment string length does not fit into u32") + .to_le_bytes(), + )?; + target.write_all(str.as_bytes())?; + } + + for p in &self.pictures { + let mut pic_data = Vec::new(); + p.encode(false, false, &mut pic_data)?; + + let pic_string = format!( + "METADATA_BLOCK_PICTURE={}", + &base64::prelude::BASE64_STANDARD.encode(&pic_data) + ); + + target.write_all( + &u32::try_from(pic_string.len()) + .expect("picture string length does not fit into u32") + .to_le_bytes(), + )?; + target.write_all(pic_string.as_bytes())?; + } + + return Ok(()); + } +} diff --git a/crates/pile-audio/tests/files/README.md b/crates/pile-flac/tests/files/README.md similarity index 100% rename from crates/pile-audio/tests/files/README.md rename to crates/pile-flac/tests/files/README.md diff --git a/crates/pile-audio/tests/files/flac_custom/01 - many images.flac b/crates/pile-flac/tests/files/flac_custom/01 - many images.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_custom/01 - many images.flac rename to crates/pile-flac/tests/files/flac_custom/01 - many images.flac diff --git a/crates/pile-audio/tests/files/flac_custom/02 - picture in vorbis comment.flac b/crates/pile-flac/tests/files/flac_custom/02 - picture in vorbis comment.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_custom/02 - picture in vorbis comment.flac rename to crates/pile-flac/tests/files/flac_custom/02 - picture in vorbis comment.flac diff --git a/crates/pile-audio/tests/files/flac_custom/03 - faulty picture in vorbis comment.flac b/crates/pile-flac/tests/files/flac_custom/03 - faulty picture in vorbis comment.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_custom/03 - faulty picture in vorbis comment.flac rename to crates/pile-flac/tests/files/flac_custom/03 - faulty picture in vorbis comment.flac diff --git a/crates/pile-audio/tests/files/flac_custom/LICENSE.txt b/crates/pile-flac/tests/files/flac_custom/LICENSE.txt similarity index 100% rename from crates/pile-audio/tests/files/flac_custom/LICENSE.txt rename to crates/pile-flac/tests/files/flac_custom/LICENSE.txt diff --git a/crates/pile-audio/tests/files/flac_custom/README.md b/crates/pile-flac/tests/files/flac_custom/README.md similarity index 100% rename from crates/pile-audio/tests/files/flac_custom/README.md rename to crates/pile-flac/tests/files/flac_custom/README.md diff --git a/crates/pile-audio/tests/files/flac_faulty/01 - wrong max blocksize.flac b/crates/pile-flac/tests/files/flac_faulty/01 - wrong max blocksize.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_faulty/01 - wrong max blocksize.flac rename to crates/pile-flac/tests/files/flac_faulty/01 - wrong max blocksize.flac diff --git a/crates/pile-audio/tests/files/flac_faulty/02 - wrong maximum framesize.flac b/crates/pile-flac/tests/files/flac_faulty/02 - wrong maximum framesize.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_faulty/02 - wrong maximum framesize.flac rename to crates/pile-flac/tests/files/flac_faulty/02 - wrong maximum framesize.flac diff --git a/crates/pile-audio/tests/files/flac_faulty/03 - wrong bit depth.flac b/crates/pile-flac/tests/files/flac_faulty/03 - wrong bit depth.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_faulty/03 - wrong bit depth.flac rename to crates/pile-flac/tests/files/flac_faulty/03 - wrong bit depth.flac diff --git a/crates/pile-audio/tests/files/flac_faulty/04 - wrong number of channels.flac b/crates/pile-flac/tests/files/flac_faulty/04 - wrong number of channels.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_faulty/04 - wrong number of channels.flac rename to crates/pile-flac/tests/files/flac_faulty/04 - wrong number of channels.flac diff --git a/crates/pile-audio/tests/files/flac_faulty/05 - wrong total number of samples.flac b/crates/pile-flac/tests/files/flac_faulty/05 - wrong total number of samples.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_faulty/05 - wrong total number of samples.flac rename to crates/pile-flac/tests/files/flac_faulty/05 - wrong total number of samples.flac diff --git a/crates/pile-audio/tests/files/flac_faulty/06 - missing streaminfo metadata block.flac b/crates/pile-flac/tests/files/flac_faulty/06 - missing streaminfo metadata block.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_faulty/06 - missing streaminfo metadata block.flac rename to crates/pile-flac/tests/files/flac_faulty/06 - missing streaminfo metadata block.flac diff --git a/crates/pile-audio/tests/files/flac_faulty/07 - other metadata blocks preceding streaminfo metadata block.flac b/crates/pile-flac/tests/files/flac_faulty/07 - other metadata blocks preceding streaminfo metadata block.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_faulty/07 - other metadata blocks preceding streaminfo metadata block.flac rename to crates/pile-flac/tests/files/flac_faulty/07 - other metadata blocks preceding streaminfo metadata block.flac diff --git a/crates/pile-audio/tests/files/flac_faulty/08 - blocksize 65536.flac b/crates/pile-flac/tests/files/flac_faulty/08 - blocksize 65536.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_faulty/08 - blocksize 65536.flac rename to crates/pile-flac/tests/files/flac_faulty/08 - blocksize 65536.flac diff --git a/crates/pile-audio/tests/files/flac_faulty/09 - blocksize 1.flac b/crates/pile-flac/tests/files/flac_faulty/09 - blocksize 1.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_faulty/09 - blocksize 1.flac rename to crates/pile-flac/tests/files/flac_faulty/09 - blocksize 1.flac diff --git a/crates/pile-audio/tests/files/flac_faulty/10 - invalid vorbis comment metadata block.flac b/crates/pile-flac/tests/files/flac_faulty/10 - invalid vorbis comment metadata block.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_faulty/10 - invalid vorbis comment metadata block.flac rename to crates/pile-flac/tests/files/flac_faulty/10 - invalid vorbis comment metadata block.flac diff --git a/crates/pile-audio/tests/files/flac_faulty/11 - incorrect metadata block length.flac b/crates/pile-flac/tests/files/flac_faulty/11 - incorrect metadata block length.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_faulty/11 - incorrect metadata block length.flac rename to crates/pile-flac/tests/files/flac_faulty/11 - incorrect metadata block length.flac diff --git a/crates/pile-audio/tests/files/flac_faulty/LICENSE.txt b/crates/pile-flac/tests/files/flac_faulty/LICENSE.txt similarity index 100% rename from crates/pile-audio/tests/files/flac_faulty/LICENSE.txt rename to crates/pile-flac/tests/files/flac_faulty/LICENSE.txt diff --git a/crates/pile-audio/tests/files/flac_faulty/README.md b/crates/pile-flac/tests/files/flac_faulty/README.md similarity index 100% rename from crates/pile-audio/tests/files/flac_faulty/README.md rename to crates/pile-flac/tests/files/flac_faulty/README.md diff --git a/crates/pile-audio/tests/files/flac_subset/01 - blocksize 4096.flac b/crates/pile-flac/tests/files/flac_subset/01 - blocksize 4096.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/01 - blocksize 4096.flac rename to crates/pile-flac/tests/files/flac_subset/01 - blocksize 4096.flac diff --git a/crates/pile-audio/tests/files/flac_subset/02 - blocksize 4608.flac b/crates/pile-flac/tests/files/flac_subset/02 - blocksize 4608.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/02 - blocksize 4608.flac rename to crates/pile-flac/tests/files/flac_subset/02 - blocksize 4608.flac diff --git a/crates/pile-audio/tests/files/flac_subset/03 - blocksize 16.flac b/crates/pile-flac/tests/files/flac_subset/03 - blocksize 16.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/03 - blocksize 16.flac rename to crates/pile-flac/tests/files/flac_subset/03 - blocksize 16.flac diff --git a/crates/pile-audio/tests/files/flac_subset/04 - blocksize 192.flac b/crates/pile-flac/tests/files/flac_subset/04 - blocksize 192.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/04 - blocksize 192.flac rename to crates/pile-flac/tests/files/flac_subset/04 - blocksize 192.flac diff --git a/crates/pile-audio/tests/files/flac_subset/05 - blocksize 254.flac b/crates/pile-flac/tests/files/flac_subset/05 - blocksize 254.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/05 - blocksize 254.flac rename to crates/pile-flac/tests/files/flac_subset/05 - blocksize 254.flac diff --git a/crates/pile-audio/tests/files/flac_subset/06 - blocksize 512.flac b/crates/pile-flac/tests/files/flac_subset/06 - blocksize 512.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/06 - blocksize 512.flac rename to crates/pile-flac/tests/files/flac_subset/06 - blocksize 512.flac diff --git a/crates/pile-audio/tests/files/flac_subset/07 - blocksize 725.flac b/crates/pile-flac/tests/files/flac_subset/07 - blocksize 725.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/07 - blocksize 725.flac rename to crates/pile-flac/tests/files/flac_subset/07 - blocksize 725.flac diff --git a/crates/pile-audio/tests/files/flac_subset/08 - blocksize 1000.flac b/crates/pile-flac/tests/files/flac_subset/08 - blocksize 1000.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/08 - blocksize 1000.flac rename to crates/pile-flac/tests/files/flac_subset/08 - blocksize 1000.flac diff --git a/crates/pile-audio/tests/files/flac_subset/09 - blocksize 1937.flac b/crates/pile-flac/tests/files/flac_subset/09 - blocksize 1937.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/09 - blocksize 1937.flac rename to crates/pile-flac/tests/files/flac_subset/09 - blocksize 1937.flac diff --git a/crates/pile-audio/tests/files/flac_subset/10 - blocksize 2304.flac b/crates/pile-flac/tests/files/flac_subset/10 - blocksize 2304.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/10 - blocksize 2304.flac rename to crates/pile-flac/tests/files/flac_subset/10 - blocksize 2304.flac diff --git a/crates/pile-audio/tests/files/flac_subset/11 - partition order 8.flac b/crates/pile-flac/tests/files/flac_subset/11 - partition order 8.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/11 - partition order 8.flac rename to crates/pile-flac/tests/files/flac_subset/11 - partition order 8.flac diff --git a/crates/pile-audio/tests/files/flac_subset/12 - qlp precision 15 bit.flac b/crates/pile-flac/tests/files/flac_subset/12 - qlp precision 15 bit.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/12 - qlp precision 15 bit.flac rename to crates/pile-flac/tests/files/flac_subset/12 - qlp precision 15 bit.flac diff --git a/crates/pile-audio/tests/files/flac_subset/13 - qlp precision 2 bit.flac b/crates/pile-flac/tests/files/flac_subset/13 - qlp precision 2 bit.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/13 - qlp precision 2 bit.flac rename to crates/pile-flac/tests/files/flac_subset/13 - qlp precision 2 bit.flac diff --git a/crates/pile-audio/tests/files/flac_subset/14 - wasted bits.flac b/crates/pile-flac/tests/files/flac_subset/14 - wasted bits.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/14 - wasted bits.flac rename to crates/pile-flac/tests/files/flac_subset/14 - wasted bits.flac diff --git a/crates/pile-audio/tests/files/flac_subset/15 - only verbatim subframes.flac b/crates/pile-flac/tests/files/flac_subset/15 - only verbatim subframes.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/15 - only verbatim subframes.flac rename to crates/pile-flac/tests/files/flac_subset/15 - only verbatim subframes.flac diff --git a/crates/pile-audio/tests/files/flac_subset/16 - partition order 8 containing escaped partitions.flac b/crates/pile-flac/tests/files/flac_subset/16 - partition order 8 containing escaped partitions.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/16 - partition order 8 containing escaped partitions.flac rename to crates/pile-flac/tests/files/flac_subset/16 - partition order 8 containing escaped partitions.flac diff --git a/crates/pile-audio/tests/files/flac_subset/17 - all fixed orders.flac b/crates/pile-flac/tests/files/flac_subset/17 - all fixed orders.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/17 - all fixed orders.flac rename to crates/pile-flac/tests/files/flac_subset/17 - all fixed orders.flac diff --git a/crates/pile-audio/tests/files/flac_subset/18 - precision search.flac b/crates/pile-flac/tests/files/flac_subset/18 - precision search.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/18 - precision search.flac rename to crates/pile-flac/tests/files/flac_subset/18 - precision search.flac diff --git a/crates/pile-audio/tests/files/flac_subset/19 - samplerate 35467Hz.flac b/crates/pile-flac/tests/files/flac_subset/19 - samplerate 35467Hz.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/19 - samplerate 35467Hz.flac rename to crates/pile-flac/tests/files/flac_subset/19 - samplerate 35467Hz.flac diff --git a/crates/pile-audio/tests/files/flac_subset/20 - samplerate 39kHz.flac b/crates/pile-flac/tests/files/flac_subset/20 - samplerate 39kHz.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/20 - samplerate 39kHz.flac rename to crates/pile-flac/tests/files/flac_subset/20 - samplerate 39kHz.flac diff --git a/crates/pile-audio/tests/files/flac_subset/21 - samplerate 22050Hz.flac b/crates/pile-flac/tests/files/flac_subset/21 - samplerate 22050Hz.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/21 - samplerate 22050Hz.flac rename to crates/pile-flac/tests/files/flac_subset/21 - samplerate 22050Hz.flac diff --git a/crates/pile-audio/tests/files/flac_subset/22 - 12 bit per sample.flac b/crates/pile-flac/tests/files/flac_subset/22 - 12 bit per sample.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/22 - 12 bit per sample.flac rename to crates/pile-flac/tests/files/flac_subset/22 - 12 bit per sample.flac diff --git a/crates/pile-audio/tests/files/flac_subset/23 - 8 bit per sample.flac b/crates/pile-flac/tests/files/flac_subset/23 - 8 bit per sample.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/23 - 8 bit per sample.flac rename to crates/pile-flac/tests/files/flac_subset/23 - 8 bit per sample.flac diff --git a/crates/pile-audio/tests/files/flac_subset/24 - variable blocksize file created with flake revision 264.flac b/crates/pile-flac/tests/files/flac_subset/24 - variable blocksize file created with flake revision 264.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/24 - variable blocksize file created with flake revision 264.flac rename to crates/pile-flac/tests/files/flac_subset/24 - variable blocksize file created with flake revision 264.flac diff --git a/crates/pile-audio/tests/files/flac_subset/25 - variable blocksize file created with flake revision 264, modified to create smaller blocks.flac b/crates/pile-flac/tests/files/flac_subset/25 - variable blocksize file created with flake revision 264, modified to create smaller blocks.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/25 - variable blocksize file created with flake revision 264, modified to create smaller blocks.flac rename to crates/pile-flac/tests/files/flac_subset/25 - variable blocksize file created with flake revision 264, modified to create smaller blocks.flac diff --git a/crates/pile-audio/tests/files/flac_subset/26 - variable blocksize file created with CUETools.Flake 2.1.6.flac b/crates/pile-flac/tests/files/flac_subset/26 - variable blocksize file created with CUETools.Flake 2.1.6.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/26 - variable blocksize file created with CUETools.Flake 2.1.6.flac rename to crates/pile-flac/tests/files/flac_subset/26 - variable blocksize file created with CUETools.Flake 2.1.6.flac diff --git a/crates/pile-audio/tests/files/flac_subset/27 - old format variable blocksize file created with Flake 0.11.flac b/crates/pile-flac/tests/files/flac_subset/27 - old format variable blocksize file created with Flake 0.11.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/27 - old format variable blocksize file created with Flake 0.11.flac rename to crates/pile-flac/tests/files/flac_subset/27 - old format variable blocksize file created with Flake 0.11.flac diff --git a/crates/pile-audio/tests/files/flac_subset/28 - high resolution audio, default settings.flac b/crates/pile-flac/tests/files/flac_subset/28 - high resolution audio, default settings.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/28 - high resolution audio, default settings.flac rename to crates/pile-flac/tests/files/flac_subset/28 - high resolution audio, default settings.flac diff --git a/crates/pile-audio/tests/files/flac_subset/29 - high resolution audio, blocksize 16384.flac b/crates/pile-flac/tests/files/flac_subset/29 - high resolution audio, blocksize 16384.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/29 - high resolution audio, blocksize 16384.flac rename to crates/pile-flac/tests/files/flac_subset/29 - high resolution audio, blocksize 16384.flac diff --git a/crates/pile-audio/tests/files/flac_subset/30 - high resolution audio, blocksize 13456.flac b/crates/pile-flac/tests/files/flac_subset/30 - high resolution audio, blocksize 13456.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/30 - high resolution audio, blocksize 13456.flac rename to crates/pile-flac/tests/files/flac_subset/30 - high resolution audio, blocksize 13456.flac diff --git a/crates/pile-audio/tests/files/flac_subset/31 - high resolution audio, using only 32nd order predictors.flac b/crates/pile-flac/tests/files/flac_subset/31 - high resolution audio, using only 32nd order predictors.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/31 - high resolution audio, using only 32nd order predictors.flac rename to crates/pile-flac/tests/files/flac_subset/31 - high resolution audio, using only 32nd order predictors.flac diff --git a/crates/pile-audio/tests/files/flac_subset/32 - high resolution audio, partition order 8 containing escaped partitions.flac b/crates/pile-flac/tests/files/flac_subset/32 - high resolution audio, partition order 8 containing escaped partitions.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/32 - high resolution audio, partition order 8 containing escaped partitions.flac rename to crates/pile-flac/tests/files/flac_subset/32 - high resolution audio, partition order 8 containing escaped partitions.flac diff --git a/crates/pile-audio/tests/files/flac_subset/33 - samplerate 192kHz.flac b/crates/pile-flac/tests/files/flac_subset/33 - samplerate 192kHz.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/33 - samplerate 192kHz.flac rename to crates/pile-flac/tests/files/flac_subset/33 - samplerate 192kHz.flac diff --git a/crates/pile-audio/tests/files/flac_subset/34 - samplerate 192kHz, using only 32nd order predictors.flac b/crates/pile-flac/tests/files/flac_subset/34 - samplerate 192kHz, using only 32nd order predictors.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/34 - samplerate 192kHz, using only 32nd order predictors.flac rename to crates/pile-flac/tests/files/flac_subset/34 - samplerate 192kHz, using only 32nd order predictors.flac diff --git a/crates/pile-audio/tests/files/flac_subset/35 - samplerate 134560Hz.flac b/crates/pile-flac/tests/files/flac_subset/35 - samplerate 134560Hz.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/35 - samplerate 134560Hz.flac rename to crates/pile-flac/tests/files/flac_subset/35 - samplerate 134560Hz.flac diff --git a/crates/pile-audio/tests/files/flac_subset/36 - samplerate 384kHz.flac b/crates/pile-flac/tests/files/flac_subset/36 - samplerate 384kHz.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/36 - samplerate 384kHz.flac rename to crates/pile-flac/tests/files/flac_subset/36 - samplerate 384kHz.flac diff --git a/crates/pile-audio/tests/files/flac_subset/37 - 20 bit per sample.flac b/crates/pile-flac/tests/files/flac_subset/37 - 20 bit per sample.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/37 - 20 bit per sample.flac rename to crates/pile-flac/tests/files/flac_subset/37 - 20 bit per sample.flac diff --git a/crates/pile-audio/tests/files/flac_subset/38 - 3 channels (3.0).flac b/crates/pile-flac/tests/files/flac_subset/38 - 3 channels (3.0).flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/38 - 3 channels (3.0).flac rename to crates/pile-flac/tests/files/flac_subset/38 - 3 channels (3.0).flac diff --git a/crates/pile-audio/tests/files/flac_subset/39 - 4 channels (4.0).flac b/crates/pile-flac/tests/files/flac_subset/39 - 4 channels (4.0).flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/39 - 4 channels (4.0).flac rename to crates/pile-flac/tests/files/flac_subset/39 - 4 channels (4.0).flac diff --git a/crates/pile-audio/tests/files/flac_subset/40 - 5 channels (5.0).flac b/crates/pile-flac/tests/files/flac_subset/40 - 5 channels (5.0).flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/40 - 5 channels (5.0).flac rename to crates/pile-flac/tests/files/flac_subset/40 - 5 channels (5.0).flac diff --git a/crates/pile-audio/tests/files/flac_subset/41 - 6 channels (5.1).flac b/crates/pile-flac/tests/files/flac_subset/41 - 6 channels (5.1).flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/41 - 6 channels (5.1).flac rename to crates/pile-flac/tests/files/flac_subset/41 - 6 channels (5.1).flac diff --git a/crates/pile-audio/tests/files/flac_subset/42 - 7 channels (6.1).flac b/crates/pile-flac/tests/files/flac_subset/42 - 7 channels (6.1).flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/42 - 7 channels (6.1).flac rename to crates/pile-flac/tests/files/flac_subset/42 - 7 channels (6.1).flac diff --git a/crates/pile-audio/tests/files/flac_subset/43 - 8 channels (7.1).flac b/crates/pile-flac/tests/files/flac_subset/43 - 8 channels (7.1).flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/43 - 8 channels (7.1).flac rename to crates/pile-flac/tests/files/flac_subset/43 - 8 channels (7.1).flac diff --git a/crates/pile-audio/tests/files/flac_subset/44 - 8-channel surround, 192kHz, 24 bit, using only 32nd order predictors.flac b/crates/pile-flac/tests/files/flac_subset/44 - 8-channel surround, 192kHz, 24 bit, using only 32nd order predictors.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/44 - 8-channel surround, 192kHz, 24 bit, using only 32nd order predictors.flac rename to crates/pile-flac/tests/files/flac_subset/44 - 8-channel surround, 192kHz, 24 bit, using only 32nd order predictors.flac diff --git a/crates/pile-audio/tests/files/flac_subset/45 - no total number of samples set.flac b/crates/pile-flac/tests/files/flac_subset/45 - no total number of samples set.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/45 - no total number of samples set.flac rename to crates/pile-flac/tests/files/flac_subset/45 - no total number of samples set.flac diff --git a/crates/pile-audio/tests/files/flac_subset/46 - no min-max framesize set.flac b/crates/pile-flac/tests/files/flac_subset/46 - no min-max framesize set.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/46 - no min-max framesize set.flac rename to crates/pile-flac/tests/files/flac_subset/46 - no min-max framesize set.flac diff --git a/crates/pile-audio/tests/files/flac_subset/47 - only STREAMINFO.flac b/crates/pile-flac/tests/files/flac_subset/47 - only STREAMINFO.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/47 - only STREAMINFO.flac rename to crates/pile-flac/tests/files/flac_subset/47 - only STREAMINFO.flac diff --git a/crates/pile-audio/tests/files/flac_subset/48 - Extremely large SEEKTABLE.flac b/crates/pile-flac/tests/files/flac_subset/48 - Extremely large SEEKTABLE.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/48 - Extremely large SEEKTABLE.flac rename to crates/pile-flac/tests/files/flac_subset/48 - Extremely large SEEKTABLE.flac diff --git a/crates/pile-audio/tests/files/flac_subset/49 - Extremely large PADDING.flac b/crates/pile-flac/tests/files/flac_subset/49 - Extremely large PADDING.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/49 - Extremely large PADDING.flac rename to crates/pile-flac/tests/files/flac_subset/49 - Extremely large PADDING.flac diff --git a/crates/pile-audio/tests/files/flac_subset/50 - Extremely large PICTURE.flac b/crates/pile-flac/tests/files/flac_subset/50 - Extremely large PICTURE.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/50 - Extremely large PICTURE.flac rename to crates/pile-flac/tests/files/flac_subset/50 - Extremely large PICTURE.flac diff --git a/crates/pile-audio/tests/files/flac_subset/51 - Extremely large VORBISCOMMENT.flac b/crates/pile-flac/tests/files/flac_subset/51 - Extremely large VORBISCOMMENT.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/51 - Extremely large VORBISCOMMENT.flac rename to crates/pile-flac/tests/files/flac_subset/51 - Extremely large VORBISCOMMENT.flac diff --git a/crates/pile-audio/tests/files/flac_subset/52 - Extremely large APPLICATION.flac b/crates/pile-flac/tests/files/flac_subset/52 - Extremely large APPLICATION.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/52 - Extremely large APPLICATION.flac rename to crates/pile-flac/tests/files/flac_subset/52 - Extremely large APPLICATION.flac diff --git a/crates/pile-audio/tests/files/flac_subset/53 - CUESHEET with very many indexes.flac b/crates/pile-flac/tests/files/flac_subset/53 - CUESHEET with very many indexes.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/53 - CUESHEET with very many indexes.flac rename to crates/pile-flac/tests/files/flac_subset/53 - CUESHEET with very many indexes.flac diff --git a/crates/pile-audio/tests/files/flac_subset/54 - 1000x repeating VORBISCOMMENT.flac b/crates/pile-flac/tests/files/flac_subset/54 - 1000x repeating VORBISCOMMENT.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/54 - 1000x repeating VORBISCOMMENT.flac rename to crates/pile-flac/tests/files/flac_subset/54 - 1000x repeating VORBISCOMMENT.flac diff --git a/crates/pile-audio/tests/files/flac_subset/55 - file 48-53 combined.flac b/crates/pile-flac/tests/files/flac_subset/55 - file 48-53 combined.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/55 - file 48-53 combined.flac rename to crates/pile-flac/tests/files/flac_subset/55 - file 48-53 combined.flac diff --git a/crates/pile-audio/tests/files/flac_subset/56 - JPG PICTURE.flac b/crates/pile-flac/tests/files/flac_subset/56 - JPG PICTURE.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/56 - JPG PICTURE.flac rename to crates/pile-flac/tests/files/flac_subset/56 - JPG PICTURE.flac diff --git a/crates/pile-audio/tests/files/flac_subset/57 - PNG PICTURE.flac b/crates/pile-flac/tests/files/flac_subset/57 - PNG PICTURE.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/57 - PNG PICTURE.flac rename to crates/pile-flac/tests/files/flac_subset/57 - PNG PICTURE.flac diff --git a/crates/pile-audio/tests/files/flac_subset/58 - GIF PICTURE.flac b/crates/pile-flac/tests/files/flac_subset/58 - GIF PICTURE.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/58 - GIF PICTURE.flac rename to crates/pile-flac/tests/files/flac_subset/58 - GIF PICTURE.flac diff --git a/crates/pile-audio/tests/files/flac_subset/59 - AVIF PICTURE.flac b/crates/pile-flac/tests/files/flac_subset/59 - AVIF PICTURE.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/59 - AVIF PICTURE.flac rename to crates/pile-flac/tests/files/flac_subset/59 - AVIF PICTURE.flac diff --git a/crates/pile-audio/tests/files/flac_subset/60 - mono audio.flac b/crates/pile-flac/tests/files/flac_subset/60 - mono audio.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/60 - mono audio.flac rename to crates/pile-flac/tests/files/flac_subset/60 - mono audio.flac diff --git a/crates/pile-audio/tests/files/flac_subset/61 - predictor overflow check, 16-bit.flac b/crates/pile-flac/tests/files/flac_subset/61 - predictor overflow check, 16-bit.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/61 - predictor overflow check, 16-bit.flac rename to crates/pile-flac/tests/files/flac_subset/61 - predictor overflow check, 16-bit.flac diff --git a/crates/pile-audio/tests/files/flac_subset/62 - predictor overflow check, 20-bit.flac b/crates/pile-flac/tests/files/flac_subset/62 - predictor overflow check, 20-bit.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/62 - predictor overflow check, 20-bit.flac rename to crates/pile-flac/tests/files/flac_subset/62 - predictor overflow check, 20-bit.flac diff --git a/crates/pile-audio/tests/files/flac_subset/63 - predictor overflow check, 24-bit.flac b/crates/pile-flac/tests/files/flac_subset/63 - predictor overflow check, 24-bit.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/63 - predictor overflow check, 24-bit.flac rename to crates/pile-flac/tests/files/flac_subset/63 - predictor overflow check, 24-bit.flac diff --git a/crates/pile-audio/tests/files/flac_subset/64 - rice partitions with escape code zero.flac b/crates/pile-flac/tests/files/flac_subset/64 - rice partitions with escape code zero.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/64 - rice partitions with escape code zero.flac rename to crates/pile-flac/tests/files/flac_subset/64 - rice partitions with escape code zero.flac diff --git a/crates/pile-audio/tests/files/flac_subset/LICENSE.txt b/crates/pile-flac/tests/files/flac_subset/LICENSE.txt similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/LICENSE.txt rename to crates/pile-flac/tests/files/flac_subset/LICENSE.txt diff --git a/crates/pile-audio/tests/files/flac_subset/README.md b/crates/pile-flac/tests/files/flac_subset/README.md similarity index 100% rename from crates/pile-audio/tests/files/flac_subset/README.md rename to crates/pile-flac/tests/files/flac_subset/README.md diff --git a/crates/pile-audio/tests/files/flac_uncommon/01 - changing samplerate.flac b/crates/pile-flac/tests/files/flac_uncommon/01 - changing samplerate.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_uncommon/01 - changing samplerate.flac rename to crates/pile-flac/tests/files/flac_uncommon/01 - changing samplerate.flac diff --git a/crates/pile-audio/tests/files/flac_uncommon/02 - increasing number of channels.flac b/crates/pile-flac/tests/files/flac_uncommon/02 - increasing number of channels.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_uncommon/02 - increasing number of channels.flac rename to crates/pile-flac/tests/files/flac_uncommon/02 - increasing number of channels.flac diff --git a/crates/pile-audio/tests/files/flac_uncommon/03 - decreasing number of channels.flac b/crates/pile-flac/tests/files/flac_uncommon/03 - decreasing number of channels.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_uncommon/03 - decreasing number of channels.flac rename to crates/pile-flac/tests/files/flac_uncommon/03 - decreasing number of channels.flac diff --git a/crates/pile-audio/tests/files/flac_uncommon/04 - changing bitdepth.flac b/crates/pile-flac/tests/files/flac_uncommon/04 - changing bitdepth.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_uncommon/04 - changing bitdepth.flac rename to crates/pile-flac/tests/files/flac_uncommon/04 - changing bitdepth.flac diff --git a/crates/pile-audio/tests/files/flac_uncommon/05 - 32bps audio.flac b/crates/pile-flac/tests/files/flac_uncommon/05 - 32bps audio.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_uncommon/05 - 32bps audio.flac rename to crates/pile-flac/tests/files/flac_uncommon/05 - 32bps audio.flac diff --git a/crates/pile-audio/tests/files/flac_uncommon/06 - samplerate 768kHz.flac b/crates/pile-flac/tests/files/flac_uncommon/06 - samplerate 768kHz.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_uncommon/06 - samplerate 768kHz.flac rename to crates/pile-flac/tests/files/flac_uncommon/06 - samplerate 768kHz.flac diff --git a/crates/pile-audio/tests/files/flac_uncommon/07 - 15 bit per sample.flac b/crates/pile-flac/tests/files/flac_uncommon/07 - 15 bit per sample.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_uncommon/07 - 15 bit per sample.flac rename to crates/pile-flac/tests/files/flac_uncommon/07 - 15 bit per sample.flac diff --git a/crates/pile-audio/tests/files/flac_uncommon/08 - blocksize 65535.flac b/crates/pile-flac/tests/files/flac_uncommon/08 - blocksize 65535.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_uncommon/08 - blocksize 65535.flac rename to crates/pile-flac/tests/files/flac_uncommon/08 - blocksize 65535.flac diff --git a/crates/pile-audio/tests/files/flac_uncommon/09 - Rice partition order 15.flac b/crates/pile-flac/tests/files/flac_uncommon/09 - Rice partition order 15.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_uncommon/09 - Rice partition order 15.flac rename to crates/pile-flac/tests/files/flac_uncommon/09 - Rice partition order 15.flac diff --git a/crates/pile-audio/tests/files/flac_uncommon/10 - file starting at frame header.flac b/crates/pile-flac/tests/files/flac_uncommon/10 - file starting at frame header.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_uncommon/10 - file starting at frame header.flac rename to crates/pile-flac/tests/files/flac_uncommon/10 - file starting at frame header.flac diff --git a/crates/pile-audio/tests/files/flac_uncommon/11 - file starting with unparsable data.flac b/crates/pile-flac/tests/files/flac_uncommon/11 - file starting with unparsable data.flac similarity index 100% rename from crates/pile-audio/tests/files/flac_uncommon/11 - file starting with unparsable data.flac rename to crates/pile-flac/tests/files/flac_uncommon/11 - file starting with unparsable data.flac diff --git a/crates/pile-audio/tests/files/flac_uncommon/LICENSE.txt b/crates/pile-flac/tests/files/flac_uncommon/LICENSE.txt similarity index 100% rename from crates/pile-audio/tests/files/flac_uncommon/LICENSE.txt rename to crates/pile-flac/tests/files/flac_uncommon/LICENSE.txt diff --git a/crates/pile-audio/tests/files/flac_uncommon/README.md b/crates/pile-flac/tests/files/flac_uncommon/README.md similarity index 100% rename from crates/pile-audio/tests/files/flac_uncommon/README.md rename to crates/pile-flac/tests/files/flac_uncommon/README.md diff --git a/crates/pile/src/config/logging.rs b/crates/pile/src/config/logging.rs index f88759d..7611378 100644 --- a/crates/pile/src/config/logging.rs +++ b/crates/pile/src/config/logging.rs @@ -38,7 +38,7 @@ pub struct LoggingConfig { other: LogLevel, pile: LogLevel, - pile_audio: LogLevel, + pile_flac: LogLevel, pile_config: LogLevel, pile_dataset: LogLevel, pile_toolbox: LogLevel, @@ -54,7 +54,7 @@ impl From for EnvFilter { format!("html5ever={}", LogLevel::Error), // Configurable sources format!("pile={}", conf.pile), - format!("pile_audio={}", conf.pile_audio), + format!("pile_flac={}", conf.pile_flac), format!("pile_config={}", conf.pile_config), format!("pile_dataset={}", conf.pile_dataset), format!("pile_toolbox={}", conf.pile_toolbox), @@ -79,7 +79,7 @@ impl LoggingPreset { other: LogLevel::Error, pile: LogLevel::Error, - pile_audio: LogLevel::Error, + pile_flac: LogLevel::Error, pile_config: LogLevel::Error, pile_dataset: LogLevel::Error, pile_toolbox: LogLevel::Error, @@ -89,7 +89,7 @@ impl LoggingPreset { other: LogLevel::Error, pile: LogLevel::Warn, - pile_audio: LogLevel::Warn, + pile_flac: LogLevel::Warn, pile_config: LogLevel::Warn, pile_dataset: LogLevel::Warn, pile_toolbox: LogLevel::Warn, @@ -99,7 +99,7 @@ impl LoggingPreset { other: LogLevel::Warn, pile: LogLevel::Info, - pile_audio: LogLevel::Info, + pile_flac: LogLevel::Info, pile_config: LogLevel::Info, pile_dataset: LogLevel::Info, pile_toolbox: LogLevel::Info, @@ -109,7 +109,7 @@ impl LoggingPreset { other: LogLevel::Warn, pile: LogLevel::Debug, - pile_audio: LogLevel::Debug, + pile_flac: LogLevel::Debug, pile_config: LogLevel::Debug, pile_dataset: LogLevel::Debug, pile_toolbox: LogLevel::Debug, @@ -119,7 +119,7 @@ impl LoggingPreset { other: LogLevel::Trace, pile: LogLevel::Trace, - pile_audio: LogLevel::Trace, + pile_flac: LogLevel::Trace, pile_config: LogLevel::Trace, pile_dataset: LogLevel::Trace, pile_toolbox: LogLevel::Trace,