diff --git a/Cargo.lock b/Cargo.lock index 3fa27fa..4087960 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,442 +2,13 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "ab_glyph" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "crossbeam" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "dasp" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7381b67da416b639690ac77c73b86a7b5e64a29e31d1f75fb3b1102301ef355a" -dependencies = [ - "dasp_envelope", - "dasp_frame", - "dasp_interpolate", - "dasp_peak", - "dasp_ring_buffer", - "dasp_rms", - "dasp_sample", - "dasp_signal", - "dasp_slice", - "dasp_window", -] - -[[package]] -name = "dasp_envelope" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ec617ce7016f101a87fe85ed44180839744265fae73bb4aa43e7ece1b7668b6" -dependencies = [ - "dasp_frame", - "dasp_peak", - "dasp_ring_buffer", - "dasp_rms", - "dasp_sample", -] - -[[package]] -name = "dasp_frame" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6" -dependencies = [ - "dasp_sample", -] - -[[package]] -name = "dasp_interpolate" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc975a6563bb7ca7ec0a6c784ead49983a21c24835b0bc96eea11ee407c7486" -dependencies = [ - "dasp_frame", - "dasp_ring_buffer", - "dasp_sample", -] - -[[package]] -name = "dasp_peak" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cf88559d79c21f3d8523d91250c397f9a15b5fc72fbb3f87fdb0a37b79915bf" -dependencies = [ - "dasp_frame", - "dasp_sample", -] - -[[package]] -name = "dasp_ring_buffer" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07d79e19b89618a543c4adec9c5a347fe378a19041699b3278e616e387511ea1" - -[[package]] -name = "dasp_rms" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6c5dcb30b7e5014486e2822537ea2beae50b19722ffe2ed7549ab03774575aa" -dependencies = [ - "dasp_frame", - "dasp_ring_buffer", - "dasp_sample", -] - -[[package]] -name = "dasp_sample" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" - -[[package]] -name = "dasp_signal" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa1ab7d01689c6ed4eae3d38fe1cea08cba761573fbd2d592528d55b421077e7" -dependencies = [ - "dasp_envelope", - "dasp_frame", - "dasp_interpolate", - "dasp_peak", - "dasp_ring_buffer", - "dasp_rms", - "dasp_sample", - "dasp_window", -] - -[[package]] -name = "dasp_slice" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e1c7335d58e7baedafa516cb361360ff38d6f4d3f9d9d5ee2a2fc8e27178fa1" -dependencies = [ - "dasp_frame", - "dasp_sample", -] - -[[package]] -name = "dasp_window" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ded7b88821d2ce4e8b842c9f1c86ac911891ab89443cc1de750cae764c5076" -dependencies = [ - "dasp_sample", -] - -[[package]] -name = "ecolor" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc4feb366740ded31a004a0e4452fbf84e80ef432ecf8314c485210229672fd1" -dependencies = [ - "emath", -] - -[[package]] -name = "egui" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd34cec49ab55d85ebf70139cb1ccd29c977ef6b6ba4fe85489d6877ee9ef3" -dependencies = [ - "ahash", - "bitflags 2.9.1", - "emath", - "epaint", - "nohash-hasher", - "profiling", -] - -[[package]] -name = "emath" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" - -[[package]] -name = "epaint" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fcc0f5a7c613afd2dee5e4b30c3e6acafb8ad6f0edb06068811f708a67c562" -dependencies = [ - "ab_glyph", - "ahash", - "ecolor", - "emath", - "epaint_default_fonts", - "nohash-hasher", - "parking_lot", - "profiling", -] - -[[package]] -name = "epaint_default_fonts" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7e7a64c02cf7a5b51e745a9e45f60660a286f151c238b9d397b3e923f5082f" - -[[package]] -name = "hound" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "jack" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f70ca699f44c04a32d419fc9ed699aaea89657fc09014bf3fa238e91d13041b9" -dependencies = [ - "bitflags 2.9.1", - "jack-sys", - "lazy_static", - "libc", - "log", -] - -[[package]] -name = "jack-sys" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6013b7619b95a22b576dfb43296faa4ecbe40abbdb97dfd22ead520775fc86ab" -dependencies = [ - "bitflags 1.3.2", - "lazy_static", - "libc", - "libloading", - "log", - "pkg-config", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.172" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" - -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - [[package]] name = "looper" version = "0.1.0" dependencies = [ - "bincode", - "crossbeam", - "dasp", - "egui", - "hound", - "jack", - "log", - "parking_lot", - "serde", - "serde_json", + "thiserror", ] -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "owned_ttf_parser" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" -dependencies = [ - "ttf-parser", -] - -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - [[package]] name = "proc-macro2" version = "1.0.95" @@ -447,12 +18,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "profiling" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" - [[package]] name = "quote" version = "1.0.40" @@ -462,65 +27,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "redox_syscall" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" -dependencies = [ - "bitflags 2.9.1", -] - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.140" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "smallvec" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" - [[package]] name = "syn" version = "2.0.101" @@ -533,125 +39,27 @@ dependencies = [ ] [[package]] -name = "ttf-parser" -version = "0.25.1" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "zerocopy" -version = "0.8.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/Cargo.toml b/Cargo.toml index a8b17a2..ca51c16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,13 +4,4 @@ version = "0.1.0" edition = "2021" [dependencies] -bincode = "1" -crossbeam = "0.8" -dasp = "0.11" -egui = "0.31" -hound = "3" -jack = "0.13" -log = "0.4.27" -parking_lot = "0.12" -serde = { version = "1", features = ["derive"] } -serde_json = "1" +thiserror = "1.0" \ No newline at end of file diff --git a/src/audio_chunk.rs b/src/audio_chunk.rs new file mode 100644 index 0000000..30956bc --- /dev/null +++ b/src/audio_chunk.rs @@ -0,0 +1,344 @@ +use std::sync::Arc; + +use crate::*; + +/// A chunk of audio data that can be linked to form longer recordings. +/// Designed for lock-free sharing between real-time and I/O threads. +#[derive(Debug)] +pub struct AudioChunk { + /// Fixed-size audio buffer + pub samples: Box<[f32]>, + + /// Number of valid samples (≤ samples.len()) + pub sample_count: usize, + + /// Next chunk in linked list + pub next: Option>, +} + +impl AudioChunk { + /// Create a new empty chunk with specified capacity (for buffer pool) + pub fn allocate(size: usize) -> Arc { + Arc::new(Self { + samples: vec![0.0; size].into_boxed_slice(), + sample_count: 0, + next: None, + }) + } + + /// Consolidate a linked list of chunks into a single optimized chunk + pub fn consolidate(this: &Arc) -> Arc { + // Calculate total sample count + let mut total_samples = 0; + let mut current = Some(this.clone()); + while let Some(chunk) = current { + total_samples += chunk.sample_count; + current = chunk.next.clone(); + } + + // Create consolidated buffer + let mut consolidated_samples = Vec::with_capacity(total_samples); + let mut current = Some(this.clone()); + while let Some(chunk) = current { + consolidated_samples.extend_from_slice(&chunk.samples[..chunk.sample_count]); + current = chunk.next.clone(); + } + + Arc::new(Self { + samples: consolidated_samples.into_boxed_slice(), + sample_count: total_samples, + next: None, + }) + } + + /// Write samples into this chunk and additional chunks as needed. + /// Uses the factory to get pre-allocated chunks when current chunk fills up. + pub fn write_samples(this: &mut Arc, samples: &[f32], chunk_factory: F) -> Result<(), LooperError> + where + F: Fn() -> Result, LooperError> + { + let sample_count = samples.len(); + + if sample_count > 0 { + let available_space = this.samples.len(); + let samples_to_write = sample_count.min(available_space); + + // Get mutable access to write samples + let chunk_mut = Arc::get_mut(this).ok_or(LooperError::ChunkOwnership)?; + + // Write what fits + if let Some(dest) = chunk_mut.samples.get_mut(..samples_to_write) { + dest.copy_from_slice(&samples[..samples_to_write]); + chunk_mut.sample_count = samples_to_write; + } + + // Handle remaining samples if any + if samples_to_write < sample_count { + // Need a new chunk for remaining samples + let mut new_chunk = chunk_factory()?; + let remaining_samples = &samples[samples_to_write..]; + + // Recurse on the new chunk + Self::write_samples(&mut new_chunk, remaining_samples, chunk_factory)?; + + // Link the new chunk + chunk_mut.next = Some(new_chunk); + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_allocate_creates_empty_chunk() { + let chunk = AudioChunk::allocate(1024); + + assert_eq!(chunk.samples.len(), 1024); + assert_eq!(chunk.sample_count, 0); + assert!(chunk.next.is_none()); + + // All samples should be initialized to 0.0 + assert!(chunk.samples.iter().all(|&x| x == 0.0)); + } + + #[test] + fn test_consolidate_single_chunk() { + let mut chunk = AudioChunk::allocate(10); + + // Write some sample data + let samples = vec![1.0, 2.0, 3.0, 4.0, 5.0]; + let factory = || panic!("Factory should not be called"); + let result = AudioChunk::write_samples(&mut chunk, &samples, factory); + assert!(result.is_ok()); + + let consolidated = AudioChunk::consolidate(&chunk); + + assert_eq!(consolidated.samples.len(), 5); + assert_eq!(consolidated.sample_count, 5); + assert!(consolidated.next.is_none()); + assert_eq!(consolidated.samples[0], 1.0); + assert_eq!(consolidated.samples[4], 5.0); + } + + #[test] + fn test_consolidate_linked_chunks() { + // Create first chunk + let chunk1 = Arc::new(AudioChunk { + samples: vec![1.0, 2.0, 3.0].into_boxed_slice(), + sample_count: 3, + next: None, + }); + + // Create second chunk + let chunk2 = Arc::new(AudioChunk { + samples: vec![4.0, 5.0].into_boxed_slice(), + sample_count: 2, + next: None, + }); + + // Create third chunk + let chunk3 = Arc::new(AudioChunk { + samples: vec![6.0, 7.0, 8.0, 9.0].into_boxed_slice(), + sample_count: 4, + next: None, + }); + + // Link them together + let chunk1 = Arc::new(AudioChunk { + samples: chunk1.samples.clone(), + sample_count: chunk1.sample_count, + next: Some(Arc::new(AudioChunk { + samples: chunk2.samples.clone(), + sample_count: chunk2.sample_count, + next: Some(chunk3), + })), + }); + + let consolidated = AudioChunk::consolidate(&chunk1); + + assert_eq!(consolidated.samples.len(), 9); + assert_eq!(consolidated.sample_count, 9); + assert!(consolidated.next.is_none()); + + let expected = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]; + assert_eq!(consolidated.samples.as_ref(), expected.as_slice()); + } + + #[test] + fn test_consolidate_partial_chunks() { + // Test chunks where sample_count < samples.len() + let chunk1 = Arc::new(AudioChunk { + samples: vec![1.0, 2.0, 3.0, 0.0, 0.0].into_boxed_slice(), + sample_count: 3, // Only first 3 samples are valid + next: None, + }); + + let chunk2 = Arc::new(AudioChunk { + samples: vec![4.0, 5.0, 0.0].into_boxed_slice(), + sample_count: 2, // Only first 2 samples are valid + next: None, + }); + + let chunk1 = Arc::new(AudioChunk { + samples: chunk1.samples.clone(), + sample_count: chunk1.sample_count, + next: Some(chunk2), + }); + + let consolidated = AudioChunk::consolidate(&chunk1); + + assert_eq!(consolidated.samples.len(), 5); + assert_eq!(consolidated.sample_count, 5); + + let expected = vec![1.0, 2.0, 3.0, 4.0, 5.0]; + assert_eq!(consolidated.samples.as_ref(), expected.as_slice()); + } + + #[test] + fn test_write_samples_empty() { + let mut chunk = AudioChunk::allocate(5); + + let factory = || panic!("Factory should not be called for empty samples"); + let result = AudioChunk::write_samples(&mut chunk, &[], factory); + + assert!(result.is_ok()); + assert_eq!(chunk.sample_count, 0); + assert!(chunk.next.is_none()); + } + + #[test] + fn test_write_samples_fits_in_one_chunk() { + let mut chunk = AudioChunk::allocate(5); + + let factory = || panic!("Factory should not be called when samples fit"); + let samples = vec![1.0, 2.0, 3.0]; + let result = AudioChunk::write_samples(&mut chunk, &samples, factory); + + assert!(result.is_ok()); + assert_eq!(chunk.sample_count, 3); + assert_eq!(chunk.samples[0], 1.0); + assert_eq!(chunk.samples[1], 2.0); + assert_eq!(chunk.samples[2], 3.0); + assert!(chunk.next.is_none()); + } + + #[test] + fn test_write_samples_exactly_fills_chunk() { + let mut chunk = AudioChunk::allocate(3); + + let factory = || panic!("Factory should not be called when samples exactly fit"); + let samples = vec![1.0, 2.0, 3.0]; + let result = AudioChunk::write_samples(&mut chunk, &samples, factory); + + assert!(result.is_ok()); + assert_eq!(chunk.sample_count, 3); + assert_eq!(chunk.samples[0], 1.0); + assert_eq!(chunk.samples[1], 2.0); + assert_eq!(chunk.samples[2], 3.0); + assert!(chunk.next.is_none()); + } + + #[test] + fn test_write_samples_requires_new_chunk() { + let mut chunk = AudioChunk::allocate(2); + + use std::cell::RefCell; + let available_chunks = RefCell::new(vec![AudioChunk::allocate(3)]); + let factory = || available_chunks.borrow_mut().pop().ok_or(LooperError::ChunkAllocation); + + let samples = vec![1.0, 2.0, 3.0, 4.0]; // 4 samples, first chunk holds 2 + let result = AudioChunk::write_samples(&mut chunk, &samples, factory); + + assert!(result.is_ok()); + + // First chunk: 2 samples + assert_eq!(chunk.sample_count, 2); + assert_eq!(chunk.samples[0], 1.0); + assert_eq!(chunk.samples[1], 2.0); + + // Second chunk: remaining 2 samples + let chunk2 = chunk.next.as_ref().unwrap(); + assert_eq!(chunk2.sample_count, 2); + assert_eq!(chunk2.samples[0], 3.0); + assert_eq!(chunk2.samples[1], 4.0); + assert!(chunk2.next.is_none()); + } + + #[test] + fn test_write_samples_multiple_chunks_cascade() { + let mut chunk = AudioChunk::allocate(2); + + use std::cell::RefCell; + let available_chunks = RefCell::new(vec![ + AudioChunk::allocate(2), + AudioChunk::allocate(2), + ]); + let factory = || available_chunks.borrow_mut().pop().ok_or(LooperError::ChunkAllocation); + + let samples = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; // 6 samples, each chunk holds 2 + let result = AudioChunk::write_samples(&mut chunk, &samples, factory); + + assert!(result.is_ok()); + + // First chunk + assert_eq!(chunk.sample_count, 2); + assert_eq!(chunk.samples[0], 1.0); + assert_eq!(chunk.samples[1], 2.0); + + // Second chunk + let chunk2 = chunk.next.as_ref().unwrap(); + assert_eq!(chunk2.sample_count, 2); + assert_eq!(chunk2.samples[0], 3.0); + assert_eq!(chunk2.samples[1], 4.0); + + // Third chunk + let chunk3 = chunk2.next.as_ref().unwrap(); + assert_eq!(chunk3.sample_count, 2); + assert_eq!(chunk3.samples[0], 5.0); + assert_eq!(chunk3.samples[1], 6.0); + assert!(chunk3.next.is_none()); + } + + #[test] + fn test_write_samples_factory_failure() { + let mut chunk = AudioChunk::allocate(2); + + let factory = || Err(LooperError::ChunkAllocation); + let samples = vec![1.0, 2.0, 3.0]; // 3 samples, chunk only holds 2 + let result = AudioChunk::write_samples(&mut chunk, &samples, factory); + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), LooperError::ChunkAllocation)); + + // First chunk should have been written before factory failure + assert_eq!(chunk.sample_count, 2); + assert_eq!(chunk.samples[0], 1.0); + assert_eq!(chunk.samples[1], 2.0); + assert!(chunk.next.is_none()); + } + + #[test] + fn test_write_samples_ownership_failure() { + let chunk = AudioChunk::allocate(3); + let chunk_clone = chunk.clone(); // Create another reference + let mut chunk_original = chunk; + + let factory = || panic!("Factory should not be called"); + let samples = vec![1.0, 2.0]; + let result = AudioChunk::write_samples(&mut chunk_original, &samples, factory); + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), LooperError::ChunkOwnership)); + + // Chunk should be unchanged + assert_eq!(chunk_original.sample_count, 0); + + // Clean up the clone reference to avoid unused variable warning + drop(chunk_clone); + } +} \ No newline at end of file diff --git a/src/looper_error.rs b/src/looper_error.rs new file mode 100644 index 0000000..161932b --- /dev/null +++ b/src/looper_error.rs @@ -0,0 +1,8 @@ +#[derive(Debug, thiserror::Error)] +pub enum LooperError { + #[error("Failed to allocate new audio chunk")] + ChunkAllocation, + + #[error("Cannot modify chunk with multiple references")] + ChunkOwnership, +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index fbedd92..a5acc1e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,21 @@ +mod audio_chunk; +mod looper_error; + +use audio_chunk::AudioChunk; +use looper_error::LooperError; + fn main() { - println!("Hello, world!"); + let mut chunk = AudioChunk::allocate(2); + + use std::cell::RefCell; + let available_chunks = RefCell::new(vec![ + AudioChunk::allocate(2), + AudioChunk::allocate(2), + ]); + let factory = || available_chunks.borrow_mut().pop().ok_or(LooperError::ChunkAllocation); + + let samples = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; // 6 samples, each chunk holds 2 + AudioChunk::write_samples(&mut chunk, &samples, factory).unwrap(); + AudioChunk::consolidate(&chunk); + println!("Audio chunk allocated with size: {}", chunk.samples.len()); } \ No newline at end of file