diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e31ea6..0ed03be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,9 @@ jobs: with: components: clippy + - name: Install ALSA dependencies + run: sudo apt-get update && sudo apt-get install -y libasound2-dev + - name: Run Clippy run: cargo clippy --all-targets --all-features -- -D warnings @@ -48,6 +51,9 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable + - name: Install ALSA dependencies + run: sudo apt-get update && sudo apt-get install -y libasound2-dev + - name: Run tests run: cargo test --all-features -- --test-threads=1 @@ -60,8 +66,8 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable - - name: Build - run: cargo build --release + - name: Build (headless - no audio) + run: cargo build --release --no-default-features nightly: name: Nightly Build @@ -73,6 +79,9 @@ jobs: - name: Install Rust Nightly uses: dtolnay/rust-toolchain@nightly + - name: Install ALSA dependencies + run: sudo apt-get update && sudo apt-get install -y libasound2-dev + - name: Run tests run: cargo test --all-features -- --test-threads=1 diff --git a/Cargo.lock b/Cargo.lock index b1147a4..f1674f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,28 @@ dependencies = [ "memchr", ] +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.11.0", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -76,12 +98,24 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.0" @@ -94,6 +128,18 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + [[package]] name = "cc" version = "1.2.57" @@ -104,6 +150,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" @@ -170,19 +222,69 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "coreaudio-rs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" +dependencies = [ + "bitflags 1.3.2", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "cpal" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f" +dependencies = [ + "alsa", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows", +] + [[package]] name = "crossterm" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags", + "bitflags 2.11.0", "crossterm_winapi", "mio", "parking_lot", @@ -201,6 +303,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + [[package]] name = "dirs" version = "5.0.1" @@ -222,6 +330,25 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "objc2", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -238,6 +365,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -250,6 +383,30 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -307,7 +464,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -349,6 +506,50 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "js-sys" version = "0.3.91" @@ -407,6 +608,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.2.0" @@ -422,6 +632,26 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "minimp3-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e21c73734c69dc95696c9ed8926a2b393171d98b3f5f5935686a26a487ab9b90" +dependencies = [ + "cc", +] + +[[package]] +name = "minimp3_fixed" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b0f14e7e75da97ae396c2656b10262a3d4afa2ec98f35795630eff0c8b951b" +dependencies = [ + "minimp3-sys", + "slice-ring-buffer", + "thiserror", +] + [[package]] name = "mio" version = "1.1.1" @@ -434,6 +664,35 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -443,6 +702,47 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -452,6 +752,100 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" +dependencies = [ + "bitflags 2.11.0", + "libc", + "objc2", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.11.0", + "objc2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "objc2", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -499,6 +893,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "prettyplease" version = "0.2.37" @@ -509,6 +909,15 @@ dependencies = [ "syn", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -539,7 +948,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.0", ] [[package]] @@ -570,13 +979,26 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "rodio" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40ecf59e742e03336be6a3d53755e789fd05a059fa22dfa0ed624722319e183" +dependencies = [ + "cpal", + "dasp_sample", + "minimp3_fixed", + "num-rational", + "symphonia", +] + [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -589,6 +1011,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -690,6 +1121,23 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slice-ring-buffer" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84ae312bda09b2368f79f985fdb4df4a0b5cbc75546b511303972d195f8c27d6" +dependencies = [ + "libc", + "mach2", + "winapi", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -702,6 +1150,153 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "symphonia" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-aac", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-isomp4", + "symphonia-format-ogg", + "symphonia-format-riff", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-isomp4" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" +dependencies = [ + "extended", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "syn" version = "2.0.117" @@ -751,6 +1346,8 @@ dependencies = [ "clap", "crossterm", "dirs", + "once_cell", + "rodio", "serde", "serde_json", "tracing", @@ -758,6 +1355,36 @@ dependencies = [ "uuid", ] +[[package]] +name = "toml_datetime" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +dependencies = [ + "winnow", +] + [[package]] name = "tracing" version = "0.1.44" @@ -855,6 +1482,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -892,6 +1529,20 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.114" @@ -952,12 +1603,22 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap", "semver", ] +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -974,12 +1635,41 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[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" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -989,7 +1679,7 @@ dependencies = [ "windows-implement", "windows-interface", "windows-link", - "windows-result", + "windows-result 0.4.1", "windows-strings", ] @@ -1021,6 +1711,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -1039,6 +1738,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1066,6 +1774,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -1097,6 +1820,12 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -1109,6 +1838,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -1121,6 +1856,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -1139,6 +1880,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -1151,6 +1898,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -1163,6 +1916,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -1175,6 +1934,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -1187,6 +1952,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -1245,7 +2019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.0", "indexmap", "log", "serde", diff --git a/Cargo.toml b/Cargo.toml index 46be70c..c49b96b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [features] -interactive = [] +interactive = ["rodio", "once_cell"] default = [] [dependencies] @@ -18,5 +18,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } anyhow = "1.0" clap = { version = "4.5", features = ["derive"] } dirs = "5.0" +rodio = { version = "0.21", optional = true, features = ["minimp3"] } +once_cell = { version = "1.18", optional = true } -[dev-dependencies] \ No newline at end of file +[dev-dependencies] diff --git a/PianoKeyboard.md b/PianoKeyboard.md new file mode 100644 index 0000000..764fca8 --- /dev/null +++ b/PianoKeyboard.md @@ -0,0 +1,80 @@ +# Piano Keyboard Mapping + +Tinydew uses the PianoNote system to play notes across 3 octaves using the Salamander Grand Piano samples. + +## Keyboard Mapping + +| Key | Note | Key | Note | Key | Note | +|-----|------|-----|------|-----|------| +| Z | C3 | A | C4 | Q | C5 | +| X | D3 | S | D4 | W | D5 | +| C | E3 | D | E4 | E | E5 | +| V | F3 | F | F4 | R | F5 | +| B | G3 | G | G4 | T | G5 | +| N | A3 | H | A4 | Y | A5 | +| M | B3 | J | B4 | U | B5 | + +### Octave Breakdown + +**Lower Octave (Z-M):** C3 to B3 +- Z, X, C → C3v8.flac +- V, B → F#3v8.flac +- N, M → A3v8.flac + +**Middle Octave (A-J):** C4 to B4 +- A, S, D → C4v8.flac +- F, G → F#4v8.flac +- H, J → A4v8.flac + +**Upper Octave (Q-U):** C5 to B5 +- Q, W, E → C5v8.flac +- R, T → F#5v8.flac +- Y, U → A5v8.flac + +## Usage + +- Guest must be at position (6, 3) on the Square map (directly in front of the piano at 6, 2) +- Press any of the above keys to play the corresponding note +- Notes are debounced - press and release to hear the note again +- Uses shared audio thread with max 4 concurrent notes + +## Audio Implementation + +- **Shared OutputStream**: A single background thread owns the audio stream +- **mpsc channel**: Key presses send `AudioCommand::Play` messages +- **Sink cleanup**: Automatic cleanup of finished notes, max 4 concurrent sinks +- **Sample format**: .flac files from Salamander Grand Piano project +- **Pitch shifting**: Uses playback speed ratios to transpose samples + +## Sound Source + +Uses the [Salamander Grand Piano](https://github.com/alexholm/salamander-grand-piano-in-rust) samples by Alexander Holm. + +Available samples: +- C3v8.flac, C4v8.flac, C5v8.flac +- F#3v8.flac, F#4v8.flac, F#5v8.flac +- A3v8.flac, A4v8.flac, A5v8.flac + +## Requirements + +- Interactive mode must be enabled: `cargo build --features interactive` +- Audio system must be available (rodio uses system audio API via cpal) +- Sound samples must be present at: `../salamander-grand-piano-in-rust/Samples/` + +## Building with Interactive Mode + +```bash +cargo build --release --features interactive +``` + +Then run: + +```bash +cargo run --features interactive +``` + +## Troubleshooting + +If you see "Dropping OutputStream" warnings: +- Ensure only one audio stream is created +- The piano system uses a shared OutputStream for all notes diff --git a/README.md b/README.md index 262b5e2..61ffbaf 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ A cozy farming game with both interactive play and MCP automation support. Tinydew is a lightweight Rust game where you can: - move between regions (Farm, EastPath, Square, SouthRiver) - farm, fish, trade, and progress days +- play piano with guests at the North Square - run fully in MCP mode for automation agents like OpenClaw --- @@ -70,6 +71,33 @@ cargo test --- +## Piano Keyboard + +Tinydew features a piano at the North Square where guests can play music. + +**Keyboard Mapping:** +- Q, W, E, R, T, Y, U, I, O, P → C Major scale (C4–E5) using Salamander Grand Piano samples + +See [`PianoKeyboard.md`](./PianoKeyboard.md) for full details. + +--- + +## Save Data + +Default save location: + +```text +~/.local/share/tinydew/savegame.json +``` + +Reset save: + +```bash +rm -f ~/.local/share/tinydew/savegame.json +``` + +--- + ## OpenClaw Installation Guide For full OpenClaw setup and usage notes, see: diff --git a/agents/block-key-sound.spec.md b/agents/block-key-sound.spec.md new file mode 100644 index 0000000..3187060 --- /dev/null +++ b/agents/block-key-sound.spec.md @@ -0,0 +1,73 @@ +# Block Key Sound System — Spec + +## Status +Implemented in `src/block_key.rs`. + +## Overview + +A keyboard-driven sound instrument where each of the 10 keys `Q W E R T Y U I O P` triggers a distinct musical note sound on keydown. This is a one-row piano/sampler for the Rust CLI app. + +## Key-to-Sound Mapping (Option A - C Major Scale) + +| Key | Note | Frequency (Hz) | +|-----|------|----------------| +| Q | C4 | 261.63 | +| W | D4 | 293.66 | +| E | E4 | 329.63 | +| R | F4 | 349.23 | +| T | G4 | 392.00 | +| Y | A4 | 440.00 | +| U | B4 | 493.88 | +| I | C5 | 523.25 | +| O | D5 | 587.33 | +| P | E5 | 659.25 | + +## Audio Playback Mechanism + +**Rust rodio crate** — cross-platform audio playback. + +- On keydown → generate a sine wave at the mapped frequency +- Create a 500ms duration tone with simple envelope +- Apply gain (0.3 volume) to prevent clipping +- Each key press is fire-and-forget (no keyup handling needed) +- Audio initialization failures are silently ignored + +### Event Flow + +``` +key_code = KeyCode::Char('q') + → BlockKeyNote::Q(C4) generated from key code + → play_note(note) spawns new audio thread + → Generate 500ms sine wave at 261.63Hz + → Play via rodio::Sink +``` + +## State Management + +```rust +// No persistent state needed - each press is stateless. +// Active notes are managed internally by rodio's Sink. + +// Key tracking is done via Set in the input handler +// to prevent re-triggering on key repeat. +``` + +### Key Concerns + +1. **Debounce key repeat** — track pressed keys in a `Set` to ignore key held events +2. **Audio initialization** — silently fail if no audio device available +3. **Polyphony** — multiple keys create separate sinks (handled by rodio) +4. **No cleanup** — rodio handles sink cleanup automatically +5. **Case insensitivity** — normalize `key.to_ascii_lowercase()` to handle Caps Lock + +## Implementation Notes + +- **Input handling** (`src/main.rs`): In the interactive key loop, intercept `KeyCode::Char('q'..='p')` for the 10 mapped keys +- **Note mapping** (`src/block_key.rs`): `BlockKeyNote::from_key_code(key)` converts `KeyCode` to `BlockKeyNote` +- **Audio module**: `src/block_key.rs` exposes `play_note(note)` which generates and plays a sine wave via rodio +- **Cargo.toml**: `rodio` is already added as a dependency (from piano.rs) + +## Related Specs + +- `guest-piano-play.spec.md` — Existing piano implementation for reference +- `north-square-piano.spec.md` — Piano tile placement and walkability diff --git a/agents/entities-and-movement.spec.md b/agents/entities-and-movement.spec.md index d42eb00..8b960d0 100644 --- a/agents/entities-and-movement.spec.md +++ b/agents/entities-and-movement.spec.md @@ -17,7 +17,8 @@ Implemented. ## Guest Control - Interactive mode enables guest control path. - Space triggers guest greeting text. -- Guest obeys non-walkable tiles (including Fountain and Wonder). +- Guest obeys non-walkable tiles (including Fountain, Piano, and Wonder). +- Guest can play the piano when standing at Square `(6,3)` via A/S/D/F/G/H/J/K keys (see `guest-piano-play.spec.md`). ## Transitions - Farm <-> EastPath (return from EastPath to Farm spawns at `(6,5)`) diff --git a/agents/guest-piano-play.spec.md b/agents/guest-piano-play.spec.md new file mode 100644 index 0000000..1078616 --- /dev/null +++ b/agents/guest-piano-play.spec.md @@ -0,0 +1,92 @@ +# Guest Piano Play Spec + +## Status +Implemented in `src/piano.rs` and integrated in `src/main.rs`. + +## Recent Updates +- **2026-03-26**: Updated to reflect macOS compatibility fix. Audio thread owns `OutputStream` to avoid `Send` trait issues with CoreAudio's non-Send `PropertyListenerCallbackWrapper`. Uses `mpsc` channel for communication between main thread and audio thread. + +## Context +The guest girl can play the piano placed at Square `(6,2)`. When the guest stands directly below the piano at `(6,3)`, keyboard keys activate piano notes with audible sound output. Only the guest has this ability — the player character cannot play the piano. + +## Activation +- Guest must be enabled (`guest_enabled == true`). +- Guest must be in Square region (`guest_location == Location::Square`). +- Guest must be at tile `(6,3)` — directly south of the piano at `(6,2)`. +- When all conditions are met, the piano key bindings become active in the interactive input loop. +- Piano mode is passive — no explicit enter/exit action. Keys are simply available while standing at `(6,3)`. + +## Key Mapping +| Key | Note Name | Display | +|-----|-----------|---------| +| A | Do | Do | +| S | Re | Re | +| D | Mi | Mi | +| F | Fa | Fa | +| G | So | So | +| H | La | La | +| J | Si | Si | +| K | DO# | DO# | + +- Keys are case-insensitive (`a`/`A` both trigger Do). +- Each key press plays the corresponding note sound and updates the bottom message. + +## Message Display +- On key press, the bottom message line shows the note name: + - Example: pressing `D` shows `🎵 Mi` +- The message updates on each key press, replacing the previous note display. +- If the guest moves away from `(6,3)`, the keys revert to their normal behavior (farm action rejection messages). + +## Sound Playback + +### Sample Source +- Piano samples come from the **Salamander Grand Piano v3** by Alexander Holm (CC-BY license). +- Sample format: FLAC files, naming convention `[Note][Octave]v[Velocity].flac` (e.g., `C4v8.flac`, `A4v8.flac`). +- Only 5 source samples are needed (the direct-hit notes); the remaining notes are derived via pitch-shifting: + - **C4v8.flac** — used for Do (C4), Re (D4, +2 semitones), Mi (E4, +4 semitones) + - **F#4v8.flac** — used for Fa (F4, −1 semitone), So (G4, +1 semitone) + - **A4v8.flac** — used for La (A4, direct) + - **C5v8.flac** — used for Si (B4, −1 semitone), DO# (C5, direct) +- Velocity layer v8 (medium) is used for all notes — a single dynamic level is sufficient. +- Samples are stored in a `Samples/` directory at the project root. + +### Note-to-Pitch Mapping +| Note | Display | Source Sample | Semitone Offset | Speed Ratio (`2^(offset/12)`) | +|------|---------|---------------|-----------------|-------------------------------| +| Do | Do | C4v8.flac | 0 | 1.0 | +| Re | Re | C4v8.flac | +2 | 1.1225 | +| Mi | Mi | C4v8.flac | +4 | 1.2599 | +| Fa | Fa | F#4v8.flac | −1 | 0.9439 | +| So | So | F#4v8.flac | +1 | 1.0595 | +| La | La | A4v8.flac | 0 | 1.0 | +| Si | Si | C5v8.flac | −1 | 0.9439 | +| DO# | DO# | C5v8.flac | 0 | 1.0 | + +### Playback Engine +- Uses `rodio` crate for audio decoding and playback (cross-platform: ALSA/PulseAudio on Linux, CoreAudio on macOS, WASAPI on Windows). +- **Audio thread pattern**: A dedicated thread owns the `OutputStream` to avoid macOS `Send` trait issues (CoreAudio's `PropertyListenerCallbackWrapper` is not `Send`). The main thread communicates via `mpsc` channel. +- Playback pipeline per note: + 1. Main thread reads sample bytes from disk and sends via channel. + 2. Audio thread receives, wraps in `std::io::Cursor`, decodes with `rodio::Decoder::new()`. + 3. Applies pitch shift via `.speed(speed_ratio)` using the semitone formula `2.0_f32.powf(offset / 12.0)`. + 4. Applies a short fade-in (`.fade_in(Duration::from_millis(8))`) to prevent click artifacts. + 5. Appends to a `rodio::Sink` for non-blocking playback. +- **Polyphony**: Maintain up to 4 concurrent voice sinks in a `VecDeque`. When a 5th note triggers, stop the oldest sink. Prune empty sinks on each key press. +- Each key press triggers a single note playback (non-blocking, fire-and-forget). +- If audio initialization fails (e.g., no audio device), the game continues silently — sound is best-effort, never a hard failure. + +## Player Restriction +- When the player (non-guest) stands at `(6,3)`, the A/S/D/F/G/H/J/K keys do **not** trigger piano notes. +- The piano interaction message from `north-square-piano.spec.md` still applies when walking into `(6,2)`. + +## Implementation Notes +- **Input handling** (`src/main.rs`): Inside the guest-enabled branch of the interactive key loop, check if `guest_location == Square && guest_x == 6 && guest_y == 3`. If true, intercept `KeyCode::Char('a'..'k')` for the 8 mapped keys before falling through to the default "Guest can only walk around." handler. +- **State** (`src/state.rs`): Add a method `guest_play_piano(&mut self, note: &str)` that sets `self.message` to the note display string (e.g., `"🎵 Do"`). No persistent piano state is needed — each press is stateless. +- **Audio module**: `src/piano.rs` exposes `play_note(note: PianoNote)` that uses a dedicated audio thread pattern to avoid macOS Send/Sync issues. Audio initialization failures are silently ignored (sound is best-effort). +- **Cargo.toml**: `rodio` is added as a dependency with `rodio = { version = "0.21", optional = true }`. The `interactive` feature includes it. +- **No MCP impact**: Piano playback is interactive-only. The MCP command interface has no piano command and requires no changes. + +## Related Specs +- `north-square-piano.spec.md` — Piano tile placement and walkability. +- `entities-and-movement.spec.md` — Guest movement and non-walkable tile rules. +- `square-region.spec.md` — Square map layout. diff --git a/agents/north-square-piano.spec.md b/agents/north-square-piano.spec.md new file mode 100644 index 0000000..6477da7 --- /dev/null +++ b/agents/north-square-piano.spec.md @@ -0,0 +1,35 @@ +# North Square Piano Spec + +## Status +Implemented. + +## Context +The Square region (the northernmost plaza) receives a permanent decorative piano object. The piano is a non-walkable fixture placed at a specific tile. + +## Placement +- Piano tile (`🎹`) placed at Square `(6,2)`. +- Replaces the default Grass tile at that position. +- Piano is always present on the map (not seasonal or spawned). +- The tile sits on row 2 (the fountain row), two tiles right of the fountain (`⛲` at `(4,2)`). + +## Map Reference (Square, row 2 after placement) +``` +🌳 🌿 🌿 🌿 ⛲ 🌿 🎹 🌿 🌳 +``` + +## Walkability +- Piano is non-walkable for both player and guest. +- Movement, pathfinding, and collision must respect the piano as a blocking tile. +- Random spawn logic must treat `(6,2)` as a protected tile (no flower/mushroom spawn on it). + +## Interaction +- Attempting to walk onto the piano tile shows: + `A beautiful old piano. It hums quietly in the square.` +- Guest can play the piano by standing at `(6,3)` directly below the piano tile (see `guest-piano-play.spec.md`). + +## Implementation Notes +- Requires new `TileType::Piano` variant in the tile enum. +- `is_walkable()` must return `false` for `TileType::Piano`. +- `emoji()` must return `"🎹"` for `TileType::Piano`. +- `create_square_map()` sets `TileType::Piano` at `square_map[2][6]`. +- Protected tile list in spawn logic must include Piano (or the `(6,2)` coordinate in Square). diff --git a/agents/spawns.spec.md b/agents/spawns.spec.md index 29b6deb..b12b624 100644 --- a/agents/spawns.spec.md +++ b/agents/spawns.spec.md @@ -13,4 +13,5 @@ Consolidates random crop/flower/mushroom-style world spawning rules. - Per region, nightly random spawn executes only when that region currently has no flower/mushroom present (max one active flower-or-mushroom blocker per region). - Mushroom visual/emoji in specs is 🍄. - Protected tiles (house/wakeup-sensitive and similar guard tiles) are excluded from random placement. +- Piano tile at Square `(6,2)` is a protected tile excluded from random spawn (see `north-square-piano.spec.md`). - Square supports decorative spawn participation while preserving movement/collision constraints. diff --git a/agents/square-region.spec.md b/agents/square-region.spec.md index 4d9aace..bb10b90 100644 --- a/agents/square-region.spec.md +++ b/agents/square-region.spec.md @@ -6,12 +6,14 @@ Implemented. ## Map - Dimensions: 9 x 5. - Boundary trees block edges. -- Center fountain (`⛲`) is non-walkable. +- Center fountain (`⛲`) at `(4,2)` is non-walkable. +- Piano (`🎹`) at `(6,2)` is a permanent non-walkable fixture (see `north-square-piano.spec.md`). - Bottom center gate connects to EastPath top-center gate. ## Behavior - Supports player/guest movement with region-aware collision. - Square forbids farm actions like clear/plant. +- Tile `(6,3)` acts as the piano play zone for the guest (see `guest-piano-play.spec.md`). - Transition rules connect EastPath <-> Square for both player and guest. - Entering Square from EastPath spawns at `(4,3)`. - Returning from Square to EastPath spawns at `(5,1)`. diff --git a/docs/OPENCLAW_INSTALL.md b/docs/OPENCLAW_INSTALL.md deleted file mode 100644 index 116c0e6..0000000 --- a/docs/OPENCLAW_INSTALL.md +++ /dev/null @@ -1,144 +0,0 @@ -# Tinydew Installation (OpenClaw one-doc guide) - -This document is a single, copy-paste setup guide for getting **Tinydew** running in an OpenClaw environment. - ---- - -## 1) Prerequisites - -Install required tools: - -- `git` -- `rustup` (Rust toolchain) -- `cargo` - -Check: - -```bash -git --version -rustc --version -cargo --version -``` - -If Rust is missing: - -```bash -curl https://sh.rustup.rs -sSf | sh -source "$HOME/.cargo/env" -``` - ---- - -## 2) Clone Tinydew - -```bash -git clone https://github.com/rustq/tinydew.git -cd tinydew -``` - -(If already cloned, just `cd tinydew`.) - ---- - -## 3) Build - -Standard build: - -```bash -cargo build --release -``` - -Interactive build (TUI): - -```bash -cargo build --release --features interactive -``` - ---- - -## 4) Run Modes - -### A) MCP mode (for OpenClaw automation) - -Run without interactive feature: - -```bash -cargo run --quiet -``` - -- In non-TTY context, Tinydew falls back to MCP stdio server. -- OpenClaw can send JSON requests to stdin (`startSession`, `getState`, `command`, etc.). -- Recommended chat behavior: for every user message during game sessions, call `print` and include the latest Tinydew UI snapshot in the assistant reply. - -### B) Interactive mode (guest/player keyboard control) - -```bash -cargo run --quiet --features interactive -``` - -Controls (interactive): - -- Guest mode hint line: `move: ↑↓←→ | greet: [SPACE]` -- Player mode hint line: `move: ↑↓←→ | clear: [C] | plant: [P] | water: [W] | harvest: [H] | trade: [T]` -- Esc: quit - ---- - -## 5) Save File Location - -Tinydew save path: - -```text -~/.local/share/tinydew/savegame.json -``` - -Reset save (fresh Day 1): - -```bash -rm -f ~/.local/share/tinydew/savegame.json -``` - ---- - -## 6) OpenClaw usage note - -For OpenClaw automation, prefer MCP mode and send tool-style commands: - -- `startSession` -- `getState` -- `command` / `commandBatch` -- `print` for text UI snapshots - -Common MCP command strings: -- `move:up|down|left|right` -- `clear`, `plant:seed`, `water`, `harvest` - - Mature crop tiles are non-walkable (blocked until harvested) - - `plant:seed` consumes one generic seed and rolls a random crop type -- `fish` or `fish:up|down|left|right` -- `buy:[:]`, `sell:[:]`, `print`, `save`, `load` -- Sell examples: `sell:mushroom:1`, `sell:carrot:2`, `sell:fish:1` - - Mushroom (🍄) sell value: $25 each -- Spec inventory convention for seeds: show as `🫙 xN` - -This gives deterministic, scriptable game control. - -### Suggested response format for chat - -When replying in game flow: -1. One short status line (what happened). -2. A fenced code block with the latest `print` snapshot UI. -3. Optional one-line next prompt (e.g., "move where?"). - -Example: - -```text -Done — moved to Square. - -tinydew day 3 ☀️ 09:10 - - - -Money: 💰 $500 - -> Entered Square. -``` diff --git a/docs/tinydew-for-openclaw.SKILL.md b/docs/tinydew-for-openclaw.SKILL.md deleted file mode 100644 index a332261..0000000 --- a/docs/tinydew-for-openclaw.SKILL.md +++ /dev/null @@ -1,162 +0,0 @@ -# OpenClaw Skill Draft: Play Tinydew in Chat - -## Installation Reference - -Use this installation guide: -- https://raw.githubusercontent.com/rustq/tinydew/refs/heads/dev/docs/OPENCLAW_INSTALL.md - -## Goal - -Make Tinydew feel **live and interactive** in chat. - -When user talks about Tinydew, OpenClaw should: -1. Execute game commands through Tinydew MCP. -2. Show a fresh game UI snapshot in the reply. -3. Narrate key moments in a fun, human style. - ---- - -## Core Interaction Model - -### 1) Always-show UI loop -For every Tinydew-related user message: -1. Ensure session is active (`startSession` if needed). -2. Interpret intent (move, farm, fish, inspect, etc.). -3. Execute one or more commands (`command` / `commandBatch`). -4. Run `command: print`. -5. Reply with: - - brief action summary, - - current UI snapshot (`snapshot_text`), - - bottom status/message from game UI, - - next suggested actions. - -**Rule:** If user says anything about Tinydew, include the game UI in the message whenever possible. - ---- - -### 2) Planting & seed model (required) -Seed/shop model for this spec: -- Shop sells a single generic seed item: `seed` -- Inventory should show seed count as `🫙 xN` (no per-crop seed split) -- Planting consumes one `seed` -- After planting, crop type is randomized to one of the crop types (Carrot / Strawberry / Cauliflower / Flower) - -When planting succeeds, include growth timing: -- Rolled crop name (the randomized result) -- Days to mature -- Expected ready day (current day + growth days) -- Note: mature crop tiles are non-walkable (blocked until harvested) - -Example style: -- "🌱 Seed planted! It rolled into Carrot. Needs 4 watered days to mature (ready around Day 5)." - -If planting fails (no seed / invalid tile), explain clearly. - ---- - -### 3) Surprise narration moments (required) -Use short celebratory lines when notable events happen. - -#### Flowers / mushrooms found -Add a surprise tone, e.g.: -- "✨ Surprise find! A wild mushroom 🍄 popped up nearby." -- "🌸 Ooh—flowers in bloom. Nice little bonus spot." - -#### Fish caught in river region -Add surprise + reward tone, e.g.: -- "🎣 Splash! You hooked a fish from the river!" -- "🐟 Nice catch—river luck is on your side." - -#### Wonder view discovered -When map/location/message implies scenic moment, react with delight: -- "🌄 Whoa, that view is gorgeous—tiny vacation energy." -- "✨ Found a wonder view. Worth pausing for a second." - -Keep surprise lines to 1 sentence so chat stays fast. - ---- - -### 4) Bottom text forwarding (required) -Always pass along the game’s bottom message/status text from the latest `print` snapshot. - -Format recommendation: -- `Game says: ` - -If multiple important system messages occur, summarize them in bullets. - ---- - -## Reply Format (recommended) - -1. **Action result** (1-2 lines) -2. **Surprise/celebration line** (only if event triggered) -3. **Game UI snapshot** (code block) - - keep MCP snapshot style (no location/player/guest position lines) - - top line should be `tinydew day