diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..15636d0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,74 @@ +name: CI + +on: + push: + branches: [ '*' ] + pull_request: + branches: [ '*' ] + +env: + CARGO_TERM_COLOR: always + +jobs: + linux-build: + name: Linux CI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Update apt + run: sudo apt update + - name: Install alsa + run: sudo apt-get install libasound2-dev + - name: Install stable + uses: dtolnay/rust-toolchain@stable + - name: Check code formatting + run: cargo fmt --all -- --check + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose + - name: Check cargo clippy warnings + run: cargo clippy --workspace --all-targets --all-features -- -D warnings + + macos-build: + name: macOS CI + runs-on: macOS-latest + steps: + - uses: actions/checkout@v4 + - name: Install llvm and clang + run: brew install llvm + - name: Install stable + uses: dtolnay/rust-toolchain@stable + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose + - name: Check cargo clippy warnings + run: cargo clippy --workspace --all-targets --all-features -- -D warnings + + windows-build: + name: windows CI + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Install ASIO SDK + env: + LINK: https://www.steinberg.net/asiosdk + run: | + curl -L -o asio.zip $env:LINK + 7z x -oasio asio.zip + move asio\*\* asio\ + - name: Install ASIO4ALL + run: choco install asio4all + - name: Install llvm and clang + run: choco install llvm + - name: Install stable + uses: dtolnay/rust-toolchain@stable + with: + target: x86_64-pc-windows-msvc + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose + - name: Check cargo clippy warnings + run: cargo clippy --workspace --all-targets --all-features -- -D warnings \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..cefd565 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,67 @@ +name: release binaries + +on: + release: + types: [created] + +permissions: + contents: write + +jobs: + upload-bins: + name: "Upload release binaries" + strategy: + matrix: + include: + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: x86_64-pc-windows-msvc + os: windows-latest + - target: aarch64-pc-windows-msvc + os: windows-latest + - target: x86_64-apple-darwin + os: macos-latest + - target: aarch64-apple-darwin + os: macos-latest + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + # Install dependencies per OS + # copy the dependencies step from the ci.yml file + - if: matrix.os == 'ubuntu-latest' + name: Install dependencies (ubuntu-latest) + run: | + sudo apt update + sudo apt-get install libasound2-dev + + - if: matrix.os == 'macOS-latest' + name: Install dependencies (macOS-latest) + run: | + brew install llvm + + - if: matrix.os == 'windows-latest' + name: Install ASIO SDK + env: + LINK: https://www.steinberg.net/asiosdk + run: | + curl -L -o asio.zip $env:LINK + 7z x -oasio asio.zip + move asio\*\* asio\ + choco install asio4all + choco install llvm + + - if: matrix.os == 'windows-latest' + uses: dtolnay/rust-toolchain@stable + with: + target: x86_64-pc-windows-msvc + + - if: matrix.os != 'windows-latest' + uses: dtolnay/rust-toolchain@stable + + # All + - uses: taiki-e/upload-rust-binary-action@v1 + with: + target: ${{ matrix.target }} + bin: ruxguitar + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e928e08 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +.idea +/test-files/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..344ddb9 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4612 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ab_glyph" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79faae4620f45232f599d9bc7b290f88247a0834162c4495ab2f02d60004adfb" +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 = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "alsa" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37fe60779335388a88c01ac6c3be40304d1e349de3ada3b15f7808bb90fa9dce" +dependencies = [ + "alsa-sys", + "bitflags 2.6.0", + "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-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.6.0", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk 0.9.0", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum", + "thiserror", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.37.3+1.3.251" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" +dependencies = [ + "libloading 0.7.4", +] + +[[package]] +name = "ashpd" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand", + "serde", + "serde_repr", + "url", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8828ec6e544c02b0d6691d21ed9f9218d0384a82542855073c2a3f58304aaf0" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7eda79bbd84e29c2b308d1dc099d7de8dcc7035e48f4bf5dc4a531a44ff5e2a" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "async-signal" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "794f185324c2f00e771cd9f1ae8b5ac68be2ca7abb129a87afd6e86d228bc54d" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.71", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "bytemuck" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee891b04274a59bd38b412188e24b849617b2e45a0fd8d057deb63e7403761b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "bytes" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" + +[[package]] +name = "calloop" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" +dependencies = [ + "bitflags 2.6.0", + "log", + "polling", + "rustix", + "slab", + "thiserror", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" +dependencies = [ + "calloop", + "rustix", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e2d530f35b40a84124146478cd16f34225306a8441998836466a2e2961c950" +dependencies = [ + "jobserver", + "libc", + "once_cell", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading 0.8.4", +] + +[[package]] +name = "clap" +version = "4.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "clap_lex" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" + +[[package]] +name = "clipboard-win" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +dependencies = [ + "error-code", +] + +[[package]] +name = "clipboard_macos" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145a7f9e9b89453bc0a5e32d166456405d389cea5b578f57f1274b1397588a95" +dependencies = [ + "objc", + "objc-foundation", + "objc_id", +] + +[[package]] +name = "clipboard_wayland" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "003f886bc4e2987729d10c1db3424e7f80809f3fc22dbc16c685738887cb37b8" +dependencies = [ + "smithay-clipboard", +] + +[[package]] +name = "clipboard_x11" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4274ea815e013e0f9f04a2633423e14194e408a0576c943ce3d14ca56c50031c" +dependencies = [ + "thiserror", + "x11rb", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "com" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e17887fd17353b65b1b2ef1c526c83e26cd72e74f598a8dc1bee13a48f3d9f6" +dependencies = [ + "com_macros", +] + +[[package]] +name = "com_macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d375883580a668c7481ea6631fc1a8863e33cc335bf56bfad8d7e6d4b04b13a5" +dependencies = [ + "com_macros_support", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "com_macros_support" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f01585027057ff5f0a5bf276174ae4c1594a2c5bde93d5f46a016d76270f5a9" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cosmic-text" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75acbfb314aeb4f5210d379af45ed1ec2c98c7f1790bf57b8a4c562ac0c51b71" +dependencies = [ + "fontdb", + "libm", + "log", + "rangemap", + "rustc-hash", + "rustybuzz", + "self_cell", + "swash", + "sys-locale", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk 0.8.0", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b" + +[[package]] +name = "cursor-icon" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" + +[[package]] +name = "d3d12" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307" +dependencies = [ + "bitflags 2.6.0", + "libloading 0.8.4", + "winapi", +] + +[[package]] +name = "dark-light" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a76fa97167fa740dcdbfe18e8895601e1bc36525f09b044e00916e717c03a3c" +dependencies = [ + "dconf_rs", + "detect-desktop-environment", + "dirs", + "objc", + "rust-ini", + "web-sys", + "winreg", + "zbus", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "dconf_rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" + +[[package]] +name = "detect-desktop-environment" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21d8ad60dd5b13a4ee6bd8fa2d5d88965c597c67bce32b5fc49c94f55cb50810" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading 0.8.4", +] + +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.1" +source = "git+https://github.com/iced-rs/winit.git?rev=254d6b3420ce4e674f516f7a2bd440665e05484d#254d6b3420ce4e674f516f7a2bd440665e05484d" + +[[package]] +name = "drm" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1" +dependencies = [ + "bitflags 2.6.0", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "rustix", +] + +[[package]] +name = "drm-ffi" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97c98727e48b7ccb4f4aea8cfe881e5b07f702d17b7875991881b41af7278d53" +dependencies = [ + "drm-sys", + "rustix", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd39dde40b6e196c2e8763f23d119ddb1a8714534bf7d77fa97a65b0feda3986" +dependencies = [ + "libc", + "linux-raw-sys 0.6.4", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "error-code" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" + +[[package]] +name = "etagere" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e2f1e3be19fb10f549be8c1bf013e8675b4066c445e36eb76d2ebb2f54ee495" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "euclid" +version = "0.22.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f0eb73b934648cd7a4a61f1b15391cd95dab0b4da6e2e66c2a072c144b4a20" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + +[[package]] +name = "font-types" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34fd7136aca682873d859ef34494ab1a7d3f57ecd485ed40eb6437ee8c85aa29" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a595cb550439a117696039dfc69830492058211b771a2a165379f2a1a53d84d" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020e203f177c0fb250fb19455a252e838d2bbbce1f80f25ecc42402aafa8cd38" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2 0.8.0", + "slotmap", + "tinyvec", + "ttf-parser 0.19.2", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", + "num_cpus", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glam" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "glow" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8098adac955faa2d31079b65dc48841251f69efd3ac25477903fc424362ead" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "glyphon" +version = "0.5.0" +source = "git+https://github.com/hecrj/glyphon.git?rev=f07e7bab705e69d39a5e6e52c73039a93c4552f8#f07e7bab705e69d39a5e6e52c73039a93c4552f8" +dependencies = [ + "cosmic-text", + "etagere", + "lru", + "rustc-hash", + "wgpu", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.6.0", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "gpu-allocator" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f56f6318968d03c18e1bcf4857ff88c61157e9da8e47c5f29055d60e1228884" +dependencies = [ + "log", + "presser", + "thiserror", + "winapi", + "windows 0.52.0", +] + +[[package]] +name = "gpu-descriptor" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" +dependencies = [ + "bitflags 2.6.0", + "gpu-descriptor-types", + "hashbrown 0.14.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "guillotiere" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.11", + "allocator-api2", +] + +[[package]] +name = "hassle-rs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" +dependencies = [ + "bitflags 2.6.0", + "com", + "libc", + "libloading 0.8.4", + "thiserror", + "widestring", + "winapi", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "iced" +version = "0.13.0-dev" +source = "git+https://github.com/iced-rs/iced.git#d9a29f51760efc0b2a9d3b0947c15c51897a7a5e" +dependencies = [ + "iced_core", + "iced_futures", + "iced_renderer", + "iced_widget", + "iced_winit", + "thiserror", +] + +[[package]] +name = "iced_core" +version = "0.13.0-dev" +source = "git+https://github.com/iced-rs/iced.git#d9a29f51760efc0b2a9d3b0947c15c51897a7a5e" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "dark-light", + "glam", + "log", + "num-traits", + "once_cell", + "palette", + "rustc-hash", + "smol_str", + "thiserror", + "web-time", +] + +[[package]] +name = "iced_futures" +version = "0.13.0-dev" +source = "git+https://github.com/iced-rs/iced.git#d9a29f51760efc0b2a9d3b0947c15c51897a7a5e" +dependencies = [ + "futures", + "iced_core", + "log", + "rustc-hash", + "tokio", + "wasm-bindgen-futures", + "wasm-timer", +] + +[[package]] +name = "iced_graphics" +version = "0.13.0-dev" +source = "git+https://github.com/iced-rs/iced.git#d9a29f51760efc0b2a9d3b0947c15c51897a7a5e" +dependencies = [ + "bitflags 2.6.0", + "bytemuck", + "cosmic-text", + "half", + "iced_core", + "iced_futures", + "log", + "lyon_path", + "once_cell", + "raw-window-handle", + "rustc-hash", + "thiserror", + "unicode-segmentation", +] + +[[package]] +name = "iced_renderer" +version = "0.13.0-dev" +source = "git+https://github.com/iced-rs/iced.git#d9a29f51760efc0b2a9d3b0947c15c51897a7a5e" +dependencies = [ + "iced_graphics", + "iced_tiny_skia", + "iced_wgpu", + "log", + "thiserror", +] + +[[package]] +name = "iced_runtime" +version = "0.13.0-dev" +source = "git+https://github.com/iced-rs/iced.git#d9a29f51760efc0b2a9d3b0947c15c51897a7a5e" +dependencies = [ + "bytes", + "iced_core", + "iced_futures", + "raw-window-handle", + "thiserror", +] + +[[package]] +name = "iced_tiny_skia" +version = "0.13.0-dev" +source = "git+https://github.com/iced-rs/iced.git#d9a29f51760efc0b2a9d3b0947c15c51897a7a5e" +dependencies = [ + "bytemuck", + "cosmic-text", + "iced_graphics", + "kurbo", + "log", + "rustc-hash", + "softbuffer", + "tiny-skia", +] + +[[package]] +name = "iced_wgpu" +version = "0.13.0-dev" +source = "git+https://github.com/iced-rs/iced.git#d9a29f51760efc0b2a9d3b0947c15c51897a7a5e" +dependencies = [ + "bitflags 2.6.0", + "bytemuck", + "futures", + "glam", + "glyphon", + "guillotiere", + "iced_graphics", + "log", + "lyon", + "once_cell", + "rustc-hash", + "thiserror", + "wgpu", +] + +[[package]] +name = "iced_widget" +version = "0.13.0-dev" +source = "git+https://github.com/iced-rs/iced.git#d9a29f51760efc0b2a9d3b0947c15c51897a7a5e" +dependencies = [ + "iced_renderer", + "iced_runtime", + "num-traits", + "once_cell", + "rustc-hash", + "thiserror", + "unicode-segmentation", +] + +[[package]] +name = "iced_winit" +version = "0.13.0-dev" +source = "git+https://github.com/iced-rs/iced.git#d9a29f51760efc0b2a9d3b0947c15c51897a7a5e" +dependencies = [ + "iced_futures", + "iced_graphics", + "iced_runtime", + "log", + "rustc-hash", + "thiserror", + "tracing", + "wasm-bindgen-futures", + "web-sys", + "winapi", + "window_clipboard", + "winit", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[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", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading 0.8.4", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kurbo" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1618d4ebd923e97d67e7cd363d80aef35fe961005cbbbb3d2dad8bdd1bc63440" +dependencies = [ + "arrayvec", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[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 = "libloading" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libredox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" +dependencies = [ + "bitflags 2.6.0", + "libc", + "redox_syscall 0.4.1", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "linux-raw-sys" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b5399f6804fbab912acbd8878ed3532d506b7c951b8f9f164ef90fef39e3f4" + +[[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.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" + +[[package]] +name = "lyon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7f9cda98b5430809e63ca5197b06c7d191bf7e26dfc467d5a3f0290e2a74f" +dependencies = [ + "lyon_algorithms", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3bca95f9a4955b3e4a821fbbcd5edfbd9be2a9a50bb5758173e5358bfb4c623" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_geom" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edecfb8d234a2b0be031ab02ebcdd9f3b9ee418fb35e265f7a540a48d197bff9" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c08a606c7a59638d6c6aa18ac91a06aa9fb5f765a7efb27e6a4da58700740d7" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "lyon_tessellation" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579d42360a4b09846eff2feef28f538696c7d6c7439bfa65874ff3cbe0951b2c" +dependencies = [ + "float_next_after", + "lyon_path", + "num-traits", +] + +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed" +dependencies = [ + "libc", +] + +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" +dependencies = [ + "bitflags 2.6.0", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "naga" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e3524642f53d9af419ab5e8dd29d3ba155708267667c2f3f06c88c9e130843" +dependencies = [ + "bit-set", + "bitflags 2.6.0", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "num-traits", + "rustc-hash", + "spirv", + "termcolor", + "thiserror", + "unicode-xid", +] + +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.6.0", + "jni-sys", + "log", + "ndk-sys 0.5.0+25.2.9519653", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.6.0", + "jni-sys", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum", + "raw-window-handle", + "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.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[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 2.0.71", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.6.0", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2", + "objc2-contacts", + "objc2-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.6.0", + "block2", + "dispatch", + "libc", + "objc2", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +dependencies = [ + "memchr", +] + +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni", + "ndk 0.8.0", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "orbclient" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f0d54bde9774d3a51dcf281a5def240c71996bc6ca05d2c847ec8b2b216166" +dependencies = [ + "libredox 0.0.2", +] + +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.3", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490d3a563d3122bf7c911a59b0add9389e5ec0f5f0c3ac6b91ff235a0e6a7f90" +dependencies = [ + "ttf-parser 0.24.0", +] + +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "palette_derive", + "phf", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[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 0.9.10", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[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 0.5.2", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" + +[[package]] +name = "quick-xml" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "range-alloc" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" + +[[package]] +name = "rangemap" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60fcc7d6849342eff22c4350c8b9a989ee8ceabc4b481253e8946b9fe83d684" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "read-fonts" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b8af39d1f23869711ad4cea5e7835a20daa987f80232f7f2a2374d648ca64d" +dependencies = [ + "bytemuck", + "font-types", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox 0.1.3", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "rfd" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a73a7337fc24366edfca76ec521f51877b114e42dab584008209cca6719251" +dependencies = [ + "ashpd", + "block", + "dispatch", + "js-sys", + "log", + "objc", + "objc-foundation", + "objc_id", + "pollster", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "roxmltree" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" + +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys 0.4.14", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustybuzz" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee8fe2a8461a0854a37101fe7a1b13998d0cfa987e43248e81d2a5f4570f6fa" +dependencies = [ + "bitflags 1.3.2", + "bytemuck", + "libm", + "smallvec", + "ttf-parser 0.20.0", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "rustysynth" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2316fb90175e4f747331f29c9275aaddf2868b355472e94a3b82ad235a719b5" + +[[package]] +name = "ruxguitar" +version = "0.1.0" +dependencies = [ + "clap", + "cpal", + "encoding_rs", + "env_logger", + "iced", + "log", + "nom", + "rfd", + "rustysynth", + "thiserror", + "tokio", + "uuid", +] + +[[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 = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sctk-adwaita" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7555fcb4f753d095d734fdefebb0ad8c98478a21db500492d87c55913d3b0086" +dependencies = [ + "ab_glyph", + "log", + "memmap2 0.9.4", + "smithay-client-toolkit", + "tiny-skia", +] + +[[package]] +name = "self_cell" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" + +[[package]] +name = "serde" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "skrifa" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab45fb68b53576a43d4fc0e9ec8ea64e29a4d2cc7f44506964cb75f288222e9" +dependencies = [ + "bytemuck", + "read-fonts", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smithay-client-toolkit" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" +dependencies = [ + "bitflags 2.6.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2 0.9.4", + "rustix", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c091e7354ea8059d6ad99eace06dd13ddeedbb0ac72d40a9a6e7ff790525882d" +dependencies = [ + "libc", + "smithay-client-toolkit", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "softbuffer" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d623bff5d06f60d738990980d782c8c866997d9194cfe79ecad00aa2f76826dd" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "cfg_aliases 0.2.1", + "core-graphics", + "drm", + "fastrand", + "foreign-types", + "js-sys", + "log", + "memmap2 0.9.4", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall 0.5.2", + "rustix", + "tiny-xlib", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.52.0", + "x11rb", +] + +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "svg_fmt" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20e16a0f46cf5fd675563ef54f26e83e20f2366bcf027bcb3cc3ed2b98aaf2ca" + +[[package]] +name = "swash" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d7773d67fe3373048cf840bfcc54ec3207cfc1e95c526b287ef2eb5eff9faf6" +dependencies = [ + "skrifa", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sys-locale" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0" +dependencies = [ + "libc", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tiny-xlib" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d52f22673960ad13af14ff4025997312def1223bfa7c8e4949d099e6b3d5d1c" +dependencies = [ + "as-raw-xcb-connection", + "ctor-lite", + "libloading 0.8.4", + "pkg-config", + "tracing", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +dependencies = [ + "backtrace", + "num_cpus", + "pin-project-lite", +] + +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "ttf-parser" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1" + +[[package]] +name = "ttf-parser" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" + +[[package]] +name = "ttf-parser" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8686b91785aff82828ed725225925b33b4fde44c4bb15876e5f7c832724c420a" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" + +[[package]] +name = "unicode-ccc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" + +[[package]] +name = "unicode-script" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[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.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.71", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wayland-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "269c04f203640d0da2092d1b8d89a2d081714ae3ac2f1b53e99f205740517198" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bd0f46c069d3382a36c8666c1b9ccef32b8b04f41667ca1fef06a1adcc2982" +dependencies = [ + "bitflags 2.6.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.6.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09414bcf0fd8d9577d73e9ac4659ebc45bcc9cff1980a350543ad8e50ee263b2" +dependencies = [ + "rustix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edf466fc49a4feb65a511ca403fec3601494d0dee85dbf37fff6fa0dd4eec3b6" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6754825230fa5b27bafaa28c30b3c9e72c55530581220cef401fa422c0fae7" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wgpu" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd7311dbd2abcfebaabf1841a2824ed7c8be443a0f29166e5d3c6a53a762c01" +dependencies = [ + "arrayvec", + "cfg-if", + "cfg_aliases 0.1.1", + "js-sys", + "log", + "naga", + "parking_lot 0.12.3", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b94525fc99ba9e5c9a9e24764f2bc29bad0911a7446c12f446a8277369bf3a" +dependencies = [ + "arrayvec", + "bit-vec", + "bitflags 2.6.0", + "cfg_aliases 0.1.1", + "codespan-reporting", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot 0.12.3", + "profiling", + "raw-window-handle", + "rustc-hash", + "smallvec", + "thiserror", + "web-sys", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1a4924366df7ab41a5d8546d6534f1f33231aa5b3f72b9930e300f254e39c3" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.6.0", + "block", + "cfg_aliases 0.1.1", + "core-graphics-types", + "d3d12", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "hassle-rs", + "js-sys", + "khronos-egl", + "libc", + "libloading 0.8.4", + "log", + "metal", + "naga", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "parking_lot 0.12.3", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash", + "smallvec", + "thiserror", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "winapi", +] + +[[package]] +name = "wgpu-types" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b671ff9fb03f78b46ff176494ee1ebe7d603393f42664be55b64dc8d53969805" +dependencies = [ + "bitflags 2.6.0", + "js-sys", + "web-sys", +] + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + +[[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-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + +[[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 = "window_clipboard" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d692d46038c433f9daee7ad8757e002a4248c20b0a3fbc991d99521d3bcb6d" +dependencies = [ + "clipboard-win", + "clipboard_macos", + "clipboard_wayland", + "clipboard_x11", + "raw-window-handle", + "thiserror", +] + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.6", +] + +[[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.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "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", + "windows-targets 0.52.6", +] + +[[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-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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winit" +version = "0.30.1" +source = "git+https://github.com/iced-rs/winit.git?rev=254d6b3420ce4e674f516f7a2bd440665e05484d#254d6b3420ce4e674f516f7a2bd440665e05484d" +dependencies = [ + "ahash 0.8.11", + "android-activity", + "atomic-waker", + "bitflags 2.6.0", + "block2", + "bytemuck", + "calloop", + "cfg_aliases 0.2.1", + "concurrent-queue", + "core-foundation", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2 0.9.4", + "ndk 0.9.0", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix", + "sctk-adwaita", + "smithay-client-toolkit", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading 0.8.4", + "once_cell", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xcursor" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a0ccd7b4a5345edfcd0c3535718a4e9ff7798ffc536bb5b5a0e26ff84732911" + +[[package]] +name = "xdg-home" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca91dcf8f93db085f3a0a29358cd0b9d670915468f4290e8b85d118a34211ab8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.6.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" + +[[package]] +name = "yazi" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" + +[[package]] +name = "zbus" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "851238c133804e0aa888edf4a0229481c753544ca12a60fd1c3230c8a500fe40" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5a3f12c20bd473be3194af6b49d50d7bb804ef3192dc70eddedb26b85d9da7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.71", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zeno" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "zvariant" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1724a2b330760dc7d2a8402d841119dc869ef120b139d29862d6980e9c75bfc9" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "url", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55025a7a518ad14518fb243559c058a2e5b848b015e31f1d90414f36e3317859" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.71", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc242db087efc22bd9ade7aa7809e4ba828132edc312871584a6b4391bdf8786" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..dc363b8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "ruxguitar" +version = "0.1.0" +edition = "2021" +authors = ["Arnaud Gourlay "] +description = "Guitar pro tablature player" +repository = "https://github.com/agourlay/ruxguitar" +license = "Apache-2.0" +readme = "README.md" +categories = ["multimedia"] +keywords = ["guitar", "tablature", "music"] + +[dependencies] +nom = "7.1.3" +encoding_rs = "0.8.34" +# TODO switch to 0.13 release when available +iced = { git = "https://github.com/iced-rs/iced.git", features = ["advanced", "canvas", "tokio", "debug"] } +tokio = { version = "1.38.0", features = ["fs", "sync"] } +rfd = "0.14.1" +log = "0.4.22" +env_logger = "0.11.3" +rustysynth = "1.3.1" +cpal = "0.15.3" +uuid = { version = "1.10.0", features = ["v4"] } +thiserror = "1.0.62" +clap = { version = "4.5.9", features = ["derive", "cargo"] } \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e872977 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# ruxguitar + +[![Build status](https://github.com/agourlay/ruxguitar/actions/workflows/ci.yml/badge.svg)](https://github.com/agourlay/ruxguitar/actions/workflows/ci.yml) + +A guitar pro tablature player. + +The design of the application is described in details in the blog article "[Playing guitar tablatures in Rust](https://agourlay.github.io/ruxguitar-tablature-player/)". + +## Limitations + +- supports only gp5 files + +## Usage + +```bash +./ruxguitar --help +Guitar pro tablature player + +Usage: ruxguitar [OPTIONS] + +Options: + --sound-font-file Optional path to a sound font file + -h, --help Print help + -V, --version Print version +``` + +A basic soundfont is embedded in the binary for a plug and play experience, however it is possible to provide a larger soundfont file to get better sound quality. + +For instance I like to use `FluidR3_GM.sf2` which is present on most systems and easy to find online ([here](https://musical-artifacts.com/artifacts/738) or [there](https://member.keymusician.com/Member/FluidR3_GM/index.html)). + +```bash +./ruxguitar --sound-font-file /usr/share/sounds/sf2/FluidR3_GM.sf2 +``` + +## Installation + +### Releases + +Using the provided binaries in https://github.com/agourlay/ruxguitar/releases + +### Build + +Make sure to check the necessary dependencies for your system from the [CI configuration](https://github.com/agourlay/ruxguitar/blob/master/.github/workflows/ci.yml). + +## Acknowledgements + +This project is heavily inspired by the great [TuxGuitar](https://sourceforge.net/p/tuxguitar/code/HEAD/tree/trunk/) project. \ No newline at end of file diff --git a/resources/TimGM6mb.sf2 b/resources/TimGM6mb.sf2 new file mode 100644 index 0000000..e5f72e6 Binary files /dev/null and b/resources/TimGM6mb.sf2 differ diff --git a/resources/icons.ttf b/resources/icons.ttf new file mode 100644 index 0000000..c091308 Binary files /dev/null and b/resources/icons.ttf differ diff --git a/src/audio/midi_builder.rs b/src/audio/midi_builder.rs new file mode 100644 index 0000000..04e26dd --- /dev/null +++ b/src/audio/midi_builder.rs @@ -0,0 +1,964 @@ +use crate::audio::midi_event::MidiEvent; +use crate::audio::FIRST_TICK; +use crate::parser::song_parser::{ + Beat, BendEffect, HarmonicType, Measure, MeasureHeader, MidiChannel, Note, NoteType, Song, + Track, TremoloBarEffect, TripletFeel, MIN_VELOCITY, QUARTER_TIME, SEMITONE_LENGTH, + VELOCITY_INCREMENT, +}; +use std::rc::Rc; + +/// Thanks to `TuxGuitar` for the reference implementation in `MidiSequenceParser.java` + +const DEFAULT_DURATION_DEAD: usize = 30; +const DEFAULT_DURATION_PM: usize = 60; +const DEFAULT_BEND: usize = 64; +const DEFAULT_BEND_SEMI_TONE: f32 = 2.75; + +pub const NATURAL_FREQUENCIES: [(i32, i32); 6] = [ + (12, 12), //AH12 (+12 frets) + (9, 28), //AH9 (+28 frets) + (5, 24), //AH5 (+24 frets) + (7, 19), //AH7 (+19 frets) + (4, 28), //AH4 (+28 frets) + (3, 31), //AH3 (+31 frets) +]; + +pub struct MidiBuilder { + events: Vec, // events accumulated during build +} + +impl MidiBuilder { + pub fn new() -> Self { + Self { events: Vec::new() } + } + + /// Parse song and record events + pub fn build_for_song(mut self, song: &Rc) -> Vec { + for (track_id, track) in song.tracks.iter().enumerate() { + log::debug!("building events for track {}", track_id); + let midi_channel = song + .midi_channels + .iter() + .find(|c| c.channel_id == track.channel_id) + .unwrap_or_else(|| { + panic!( + "midi channel {} not found for track {}", + track.channel_id, track_id + ) + }); + self.add_track_events( + song.tempo.value, + track_id, + track, + &song.measure_headers, + midi_channel, + ); + } + // Sort events by tick + self.events.sort_by_key(|event| event.tick); + self.events + } + + fn add_track_events( + &mut self, + song_tempo: i32, + track_id: usize, + track: &Track, + measure_headers: &[MeasureHeader], + midi_channel: &MidiChannel, + ) { + // add MIDI control events for the track channel + self.add_track_channel_midi_control(track_id, midi_channel); + + let strings = &track.strings; + let mut prev_tempo = song_tempo; + assert_eq!(track.measures.len(), measure_headers.len()); + for (measure, measure_header) in track.measures.iter().zip(measure_headers) { + // add song info events once for all tracks + if track_id == 0 { + // change tempo if necessary + let measure_tempo = measure_header.tempo.value; + if measure_tempo != prev_tempo { + let tick = measure_header.start as usize; + self.add_tempo_change(tick, measure_tempo); + prev_tempo = measure_tempo; + } + } + self.add_beat_events( + track_id, + track, + measure, + measure_header, + midi_channel, + strings, + ) + } + } + + fn add_beat_events( + &mut self, + track_id: usize, + track: &Track, + measure: &Measure, + measure_header: &MeasureHeader, + midi_channel: &MidiChannel, + strings: &[(i32, i32)], + ) { + for voice in &measure.voices { + let beats = &voice.beats; + for (beat_id, beat) in beats.iter().enumerate() { + if beat.empty || beat.notes.is_empty() { + continue; + } + // extract surrounding beats + let previous_beat = if beat_id == 0 { + None + } else { + beats.get(beat_id - 1) + }; + let next_beat = beats.get(beat_id + 1); + self.add_notes( + track_id, + track, + measure_header, + midi_channel, + previous_beat, + beat, + next_beat, + strings, + ); + } + } + } + + #[allow(clippy::too_many_arguments)] + fn add_notes( + &mut self, + track_id: usize, + track: &Track, + measure_header: &MeasureHeader, + midi_channel: &MidiChannel, + previous_beat: Option<&Beat>, + beat: &Beat, + next_beat: Option<&Beat>, + strings: &[(i32, i32)], + ) { + if measure_header.triplet_feel != TripletFeel::None { + log::warn!("Triplet feel not supported"); + } + let _stroke = &beat.effect.stroke; + let mut start = beat.start as usize; + let channel_id = midi_channel.channel_id; + let tempo = measure_header.tempo.value; + // TODO when to use effect channel instead? + assert!(channel_id < 16); + let track_offset = track.offset; + let beat_duration = beat.duration.time() as usize; + for (note_offset, note) in beat.notes.iter().enumerate() { + if note.kind != NoteType::Tie { + let (string_id, string_tuning) = strings[note.string as usize - 1]; + assert_eq!(string_id, note.string as i32); + + // compute key without effect + let initial_key = track_offset + note.value as i32 + string_tuning; + + // surrounding notes on the same string on the previous & next beat + let previous_note = previous_beat.and_then(|b| b.notes.get(note_offset)); + let next_note = next_beat.and_then(|b| b.notes.get(note_offset)); + + // apply effects on duration + let mut duration = apply_duration_effect(note, next_note, tempo, beat_duration); + assert_ne!(duration, 0); + + // apply effects on velocity + let velocity = apply_velocity_effect(note, previous_note, midi_channel); + + // apply effects on key + if let Some(key) = self.add_key_effect( + track_id, + &mut start, + &mut duration, + tempo, + note, + next_note, + next_beat, + initial_key, + velocity, + midi_channel, + ) { + self.add_note(track_id, key, start, duration, velocity, channel_id as i32) + } + } + } + } + + #[allow(clippy::too_many_arguments)] + fn add_key_effect( + &mut self, + track_id: usize, + start: &mut usize, + duration: &mut usize, + tempo: i32, + note: &Note, + next_note: Option<&Note>, + next_beat: Option<&Beat>, + initial_key: i32, + velocity: i16, + midi_channel: &MidiChannel, + ) -> Option { + let channel_id = midi_channel.channel_id; + let is_percussion = midi_channel.is_percussion(); + + // key with effect + let mut key = initial_key; + + if note.effect.fade_in { + // TODO fade_in + } + + // grace note + if let Some(grace) = ¬e.effect.grace { + let grace_key = initial_key + grace.fret as i32; + let grace_length = grace.duration_time() as usize; + let grace_velocity = grace.velocity; + let grace_duration = if grace.is_dead { + apply_static_duration(tempo, DEFAULT_DURATION_DEAD, grace_length) + } else { + grace_length + }; + let on_beat_duration = *start - grace_length; + if grace.is_on_beat || on_beat_duration < QUARTER_TIME as usize { + *start += grace_length; + *duration -= grace_length; + } + self.add_note( + track_id, + grace_key, + *start - grace_length, + grace_duration, + grace_velocity, + channel_id as i32, + ) + } + + if let Some(trill) = ¬e.effect.trill { + if !is_percussion { + let trill_key = trill.fret as i32 + initial_key - note.value as i32; + let mut trill_length = trill.duration.time() as usize; + + let trill_tick_limit = *start + *duration; + let mut real_key = false; + let mut tick = *start; + + let mut counter = 0; + while tick + 10 < trill_tick_limit { + if tick + trill_length >= trill_tick_limit { + trill_length = trill_tick_limit - tick - 1; + } + let iter_key = if real_key { initial_key } else { trill_key }; + self.add_note( + track_id, + iter_key, + tick, + trill_length, + velocity, + channel_id as i32, + ); + real_key = !real_key; + tick += trill_length; + counter += 1; + } + assert!( + counter > 0, + "No trill notes published! trill_length: {}, tick: {}, trill_tick_limit: {}", + trill_length, + tick, + trill_tick_limit + ); + + // all notes published - the caller does not need to publish the note + return None; + } + } + + // tremolo picking + if let Some(tremolo_picking) = ¬e.effect.tremolo_picking { + let mut tp_length = tremolo_picking.duration.time() as usize; + let mut tick = *start; + let tp_tick_limit = *start + *duration; + let mut counter = 0; + while tick + 10 < tp_tick_limit { + if tick + tp_length >= tp_tick_limit { + tp_length = tp_tick_limit - tick - 1; + } + self.add_note( + track_id, + initial_key, + tick, + tp_length, + velocity, + channel_id as i32, + ); + tick += tp_length; + counter += 1; + } + assert!( + counter > 0, + "No tremolo notes published! tp_length: {}, tick: {}, tp_tick_limit: {}", + tp_length, + tick, + tp_tick_limit + ); + // all notes published - the caller does not need to publish the note + return None; + } + + // bend + if let Some(bend_effect) = ¬e.effect.bend { + if !is_percussion { + self.add_bend( + track_id, + *start, + *duration, + channel_id as usize, + bend_effect, + ) + } + } + + if let Some(tremolo_bar) = ¬e.effect.tremolo_bar { + if !is_percussion { + self.add_tremolo_bar( + track_id, + *start, + *duration, + channel_id as usize, + tremolo_bar, + ) + } + } + + // slide + if let Some(_slide) = ¬e.effect.slide { + if !is_percussion { + if let Some((next_note, next_beat)) = next_note.zip(next_beat) { + let value_1 = note.value as i32; + let value_2 = next_note.value as i32; + + let tick1 = *start; + let tick2 = next_beat.start as usize; + + // make slide + let distance: i32 = value_2 - value_1; + let length: i32 = (tick2 - tick1) as i32; + let points = length / (QUARTER_TIME as usize / 8) as i32; + for p_offset in 1..=points { + let tone = ((length / points) * p_offset) * distance / length; + let bend = + DEFAULT_BEND as f32 + (tone as f32 * DEFAULT_BEND_SEMI_TONE * 2.0); + let bend_tick = tick1 as i32 + (length / points) * p_offset; + self.add_pitch_bend( + bend_tick as usize, + track_id, + channel_id as i32, + bend as i32, + ); + } + + // normalise the bend + self.add_pitch_bend(tick2, track_id, channel_id as i32, DEFAULT_BEND as i32); + } + } + } + + // vibrato + if note.effect.vibrato && !is_percussion { + self.add_vibrato(track_id, *start, *duration, channel_id as usize); + } + + // harmonic + if let Some(harmonic) = ¬e.effect.harmonic { + if !is_percussion { + match harmonic.kind { + HarmonicType::Natural => { + for (harmonic_value, harmonic_frequency) in NATURAL_FREQUENCIES { + if note.value % 12 == (harmonic_value % 12) as i16 { + key = (initial_key + harmonic_frequency) - note.value as i32; + break; + } + } + } + HarmonicType::Semi => { + let velocity = MIN_VELOCITY.max(velocity - VELOCITY_INCREMENT * 3); + self.add_note( + track_id, + initial_key, + *start, + *duration, + velocity, + channel_id as i32, + ); + key = initial_key + NATURAL_FREQUENCIES[0].1; + } + HarmonicType::Artificial | HarmonicType::Pinch => { + key = initial_key + NATURAL_FREQUENCIES[0].1; + } + HarmonicType::Tapped => { + if let Some(right_hand_fret) = harmonic.right_hand_fret { + for (harmonic_value, harmonic_frequency) in NATURAL_FREQUENCIES { + if right_hand_fret as i16 - note.value == harmonic_value as i16 { + key = initial_key + harmonic_frequency; + break; + } + } + } + } + } + if key - 12 > 0 { + let velocity = MIN_VELOCITY.max(velocity - VELOCITY_INCREMENT * 4); + self.add_note( + track_id, + key - 12, + *start, + *duration, + velocity, + channel_id as i32, + ); + } + } + } + + Some(key) + } + + fn add_vibrato(&mut self, track_id: usize, start: usize, duration: usize, channel_id: usize) { + let end = start + duration; + let mut next_start = start; + while next_start < end { + next_start = if next_start + 160 > end { + end + } else { + next_start + 160 + }; + self.add_pitch_bend(next_start, track_id, channel_id as i32, DEFAULT_BEND as i32); + + next_start = if next_start + 160 > end { + end + } else { + next_start + 160 + }; + let value = DEFAULT_BEND as f32 + DEFAULT_BEND_SEMI_TONE / 2.0; + self.add_pitch_bend(next_start, track_id, channel_id as i32, value as i32); + } + self.add_pitch_bend(next_start, track_id, channel_id as i32, DEFAULT_BEND as i32) + } + + // TODO investigate why it does not sound good :( + fn add_bend( + &mut self, + track_id: usize, + start: usize, + duration: usize, + channel_id: usize, + bend: &BendEffect, + ) { + for (point_id, point) in bend.points.iter().enumerate() { + let value = DEFAULT_BEND as f32 + + (point.value as f32 * DEFAULT_BEND_SEMI_TONE / SEMITONE_LENGTH); + let mut value = value.clamp(0.0, 127.0); + let mut bend_start = start + point.get_time(duration); + self.add_pitch_bend(bend_start, track_id, channel_id as i32, value as i32); + + // look ahead to next point + if let Some(next_point) = bend.points.get(point_id + 1) { + let next_value = DEFAULT_BEND as f32 + + (next_point.value as f32 * DEFAULT_BEND_SEMI_TONE / SEMITONE_LENGTH); + let next_bend_start = start + next_point.get_time(duration); + if value != next_value { + let width = (next_bend_start - bend_start) as f32 / (next_value - value).abs(); + // ascending + if value < next_value { + while value < next_value { + value += 1.0; + bend_start += width as usize; + self.add_pitch_bend( + bend_start, + track_id, + channel_id as i32, + value as i32, + ); + } + } + // descending + if value > next_value { + while value > next_value { + value -= 1.0; + bend_start += width as usize; + self.add_pitch_bend( + bend_start, + track_id, + channel_id as i32, + value as i32, + ); + } + } + } + } + } + self.add_pitch_bend( + start + duration, + track_id, + channel_id as i32, + DEFAULT_BEND as i32, + ) + } + + fn add_tremolo_bar( + &mut self, + track_id: usize, + start: usize, + duration: usize, + channel_id: usize, + tremolo_bar: &TremoloBarEffect, + ) { + for (point_id, point) in tremolo_bar.points.iter().enumerate() { + let value = DEFAULT_BEND as f32 + (point.value as f32 * DEFAULT_BEND_SEMI_TONE * 2.0); + let mut value = value.clamp(0.0, 127.0); + let mut bend_start = start + point.get_time(duration); + self.add_pitch_bend(bend_start, track_id, channel_id as i32, value as i32); + + // look ahead to next point + if let Some(next_point) = tremolo_bar.points.get(point_id + 1) { + let next_value = + DEFAULT_BEND as f32 + (next_point.value as f32 * DEFAULT_BEND_SEMI_TONE * 2.0); + let next_bend_start = start + next_point.get_time(duration); + if value != next_value { + let width = (next_bend_start - bend_start) as f32 / (next_value - value).abs(); + // ascending + if value < next_value { + while value < next_value { + value += 1.0; + bend_start += width as usize; + self.add_pitch_bend( + bend_start, + track_id, + channel_id as i32, + value as i32, + ); + } + } + // descending + if value > next_value { + while value > next_value { + value -= 1.0; + bend_start += width as usize; + self.add_pitch_bend( + bend_start, + track_id, + channel_id as i32, + value as i32, + ); + } + } + } + } + } + self.add_pitch_bend( + start + duration, + track_id, + channel_id as i32, + DEFAULT_BEND as i32, + ) + } + + fn add_note( + &mut self, + track_id: usize, + key: i32, + start: usize, + duration: usize, + velocity: i16, + channel: i32, + ) { + let note_on = MidiEvent::new_note_on(start, track_id, key, velocity, channel); + self.add_event(note_on); + if duration > 0 { + let tick = start + duration; + let note_off = MidiEvent::new_note_off(tick, track_id, key, channel); + self.add_event(note_off); + } + } + + fn add_tempo_change(&mut self, tick: usize, tempo: i32) { + let event = MidiEvent::new_tempo_change(tick, tempo); + self.add_event(event); + } + + fn add_bank_selection(&mut self, tick: usize, track_id: usize, channel: i32, bank: i32) { + let event = MidiEvent::new_midi_message(tick, track_id, channel, 0xB0, 0x00, bank); + self.add_event(event); + } + + fn add_volume_selection(&mut self, tick: usize, track_id: usize, channel: i32, volume: i32) { + let event = MidiEvent::new_midi_message(tick, track_id, channel, 0xB0, 0x27, volume); + self.add_event(event); + } + + fn add_expression_selection( + &mut self, + tick: usize, + track_id: usize, + channel: i32, + expression: i32, + ) { + let event = MidiEvent::new_midi_message(tick, track_id, channel, 0xB0, 0x2B, expression); + self.add_event(event); + } + + fn add_chorus_selection(&mut self, tick: usize, track_id: usize, channel: i32, chorus: i32) { + let event = MidiEvent::new_midi_message(tick, track_id, channel, 0xB0, 0x5D, chorus); + self.add_event(event); + } + + fn add_reverb_selection(&mut self, tick: usize, track_id: usize, channel: i32, reverb: i32) { + let event = MidiEvent::new_midi_message(tick, track_id, channel, 0xB0, 0x5B, reverb); + self.add_event(event); + } + + fn add_pitch_bend(&mut self, tick: usize, track_id: usize, channel: i32, value: i32) { + let event = MidiEvent::new_midi_message(tick, track_id, channel, 0xE0, 0, value); + self.add_event(event); + } + + fn add_program_selection(&mut self, tick: usize, track_id: usize, channel: i32, program: i32) { + let event = MidiEvent::new_midi_message(tick, track_id, channel, 0xC0, program, 0); + self.add_event(event); + } + + fn add_track_channel_midi_control(&mut self, track_id: usize, midi_channel: &MidiChannel) { + let channel_id = midi_channel.channel_id; + // publish MIDI control messages for the track channel at the start + let info_tick = FIRST_TICK; + self.add_volume_selection( + info_tick, + track_id, + channel_id as i32, + midi_channel.volume as i32, + ); + self.add_expression_selection(info_tick, track_id, channel_id as i32, 127); + self.add_chorus_selection( + info_tick, + track_id, + channel_id as i32, + midi_channel.chorus as i32, + ); + self.add_reverb_selection( + info_tick, + track_id, + channel_id as i32, + midi_channel.reverb as i32, + ); + self.add_bank_selection( + info_tick, + track_id, + channel_id as i32, + midi_channel.bank as i32, + ); + self.add_program_selection( + info_tick, + track_id, + channel_id as i32, + midi_channel.instrument, + ); + } + + fn add_event(&mut self, event: MidiEvent) { + self.events.push(event); + } +} + +fn apply_velocity_effect( + note: &Note, + previous_note: Option<&Note>, + midi_channel: &MidiChannel, +) -> i16 { + let effect = ¬e.effect; + let mut velocity = note.velocity; + + if !midi_channel.is_percussion() && previous_note.is_some_and(|n| n.effect.hammer) { + velocity = MIN_VELOCITY.max(velocity - 25); + } + + if effect.ghost_note { + velocity = MIN_VELOCITY.max(velocity - VELOCITY_INCREMENT); + } else if effect.accentuated_note { + velocity = MIN_VELOCITY.max(velocity + VELOCITY_INCREMENT); + } else if effect.heavy_accentuated_note { + velocity = MIN_VELOCITY.max(velocity + VELOCITY_INCREMENT * 2); + } + if velocity > 127 { + 127 + } else { + velocity + } +} + +fn apply_duration_effect( + note: &Note, + next_note: Option<&Note>, + tempo: i32, + mut duration: usize, +) -> usize { + let note_type = ¬e.kind; + if let Some(next_note) = next_note { + if next_note.kind == NoteType::Tie { + // approximation? + duration += duration; + } + } + if note_type == &NoteType::Dead { + return apply_static_duration(tempo, DEFAULT_DURATION_DEAD, duration); + } + if note.effect.palm_mute { + return apply_static_duration(tempo, DEFAULT_DURATION_PM, duration); + } + if note.effect.staccato { + return ((duration * 50) as f64 / 100.00) as usize; + } + if note.effect.let_ring { + return duration * 2; + } + duration +} + +fn apply_static_duration(tempo: i32, duration: usize, maximum: usize) -> usize { + let value = tempo as usize * duration / 60; + if value < maximum { + value + } else { + maximum + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::audio::midi_event::MidiEventType; + use crate::parser::song_parser_tests::parse_gp_file; + use std::collections::HashSet; + + #[test] + fn test_midi_events_for_all_gp5_song() { + let test_dir = std::path::Path::new("test-files"); + for entry in std::fs::read_dir(test_dir).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.extension().unwrap() != "gp5" { + continue; + } + let file_name = path.file_name().unwrap().to_str().unwrap(); + eprintln!("Parsing file: {}", file_name); + let file_path = path.to_str().unwrap(); + let song = parse_gp_file(file_path) + .unwrap_or_else(|err| panic!("Failed to parse file: {}\n{}", file_name, err)); + let song = Rc::new(song); + let builder = MidiBuilder::new(); + let events = builder.build_for_song(&song); + assert!(!events.is_empty(), "No events found for {}", file_name); + + // assert sorted by tick + assert!(events.windows(2).all(|w| w[0].tick <= w[1].tick)); + assert_eq!(events[0].tick, 1); + } + } + + #[test] + fn test_midi_events_for_demo_song() { + const FILE_PATH: &str = "test-files/Demo v5.gp5"; + let song = parse_gp_file(FILE_PATH).unwrap(); + let song = Rc::new(song); + let builder = MidiBuilder::new(); + let events = builder.build_for_song(&song); + + assert_eq!(events.len(), 4450); + assert_eq!(events[0].tick, 1); + assert_eq!(events.iter().last().unwrap().tick, 189_120); + + // assert number of tracks + let track_count = song.tracks.len(); + let unique_tracks: HashSet<_> = events.iter().map(|event| event.track).collect(); + assert_eq!(unique_tracks.len(), track_count + 1); // plus None for info events + + // skip MIDI program messages + let rhythm_track_events: Vec<_> = events + .iter() + .filter(|e| e.track == Some(0)) + .skip(6) + .collect(); + + // print 20 first for debugging + for (i, event) in rhythm_track_events.iter().enumerate().take(20) { + eprintln!("{} {:?}", i, event); + } + + // C5 ON + let event = &rhythm_track_events[0]; + assert_eq!(event.tick, 960); + assert_eq!(event.track, Some(0)); + assert!(matches!(event.event, MidiEventType::NoteOn(0, 60, 95))); + + let event = &rhythm_track_events[1]; + assert_eq!(event.tick, 960); + assert_eq!(event.track, Some(0)); + assert!(matches!(event.event, MidiEventType::NoteOn(0, 55, 95))); + + let event = &rhythm_track_events[2]; + assert_eq!(event.tick, 960); + assert_eq!(event.track, Some(0)); + assert!(matches!(event.event, MidiEventType::NoteOn(0, 48, 127))); + + // C5 OFF + let event = &rhythm_track_events[3]; + assert_eq!(event.tick, 1440); + assert_eq!(event.track, Some(0)); + assert!(matches!(event.event, MidiEventType::NoteOff(0, 60))); + + let event = &rhythm_track_events[4]; + assert_eq!(event.tick, 1440); + assert_eq!(event.track, Some(0)); + assert!(matches!(event.event, MidiEventType::NoteOff(0, 55))); + + let event = &rhythm_track_events[5]; + assert_eq!(event.tick, 1440); + assert_eq!(event.track, Some(0)); + assert!(matches!(event.event, MidiEventType::NoteOff(0, 48))); + + // single note `3` on string `1` (E2) + let event = &rhythm_track_events[6]; + assert_eq!(event.tick, 1440); + assert_eq!(event.track, Some(0)); + assert!(matches!(event.event, MidiEventType::NoteOn(0, 48, 95))); + + // single note OFF (palm mute) + let event = &rhythm_track_events[7]; + assert_eq!(event.tick, 1605); + assert_eq!(event.track, Some(0)); + assert!(matches!(event.event, MidiEventType::NoteOff(0, 48))); + + // single note `3` on string `1` (E2) + let event = &rhythm_track_events[8]; + assert_eq!(event.tick, 1920); + assert_eq!(event.track, Some(0)); + assert!(matches!(event.event, MidiEventType::NoteOn(0, 48, 95))); + + // single note OFF (palm mute) + let event = &rhythm_track_events[9]; + assert_eq!(event.tick, 2085); + assert_eq!(event.track, Some(0)); + assert!(matches!(event.event, MidiEventType::NoteOff(0, 48))); + + // C5 ON + let event = &rhythm_track_events[10]; + assert_eq!(event.tick, 2400); + assert_eq!(event.track, Some(0)); + assert!(matches!(event.event, MidiEventType::NoteOn(0, 60, 95))); + + let event = &rhythm_track_events[11]; + assert_eq!(event.tick, 2400); + assert_eq!(event.track, Some(0)); + assert!(matches!(event.event, MidiEventType::NoteOn(0, 55, 95))); + + let event = &rhythm_track_events[12]; + assert_eq!(event.tick, 2400); + assert_eq!(event.track, Some(0)); + assert!(matches!(event.event, MidiEventType::NoteOn(0, 48, 127))); + + // skip MIDI program messages + let solo_track_events: Vec<_> = events + .iter() + .filter(|e| e.track == Some(1)) + .skip(6) + .collect(); + + // print 20 first for debugging + for (i, event) in solo_track_events.iter().enumerate().take(60) { + eprintln!("{} {:?}", i, event); + } + + // trill ON + let event = &solo_track_events[0]; + assert_eq!(event.tick, 12480); + assert_eq!(event.track, Some(1)); + assert!(matches!(event.event, MidiEventType::NoteOn(2, 72, 95))); + + // trill OFF + let event = &solo_track_events[1]; + assert_eq!(event.tick, 12720); + assert_eq!(event.track, Some(1)); + assert!(matches!(event.event, MidiEventType::NoteOff(2, 72))); + + // trill ON + let event = &solo_track_events[2]; + assert_eq!(event.tick, 12720); + assert_eq!(event.track, Some(1)); + assert!(matches!(event.event, MidiEventType::NoteOn(2, 69, 95))); + + // trill OFF + let event = &solo_track_events[3]; + assert_eq!(event.tick, 12960); + assert_eq!(event.track, Some(1)); + assert!(matches!(event.event, MidiEventType::NoteOff(2, 69))); + + // trill ON + let event = &solo_track_events[4]; + assert_eq!(event.tick, 12960); + assert_eq!(event.track, Some(1)); + assert!(matches!(event.event, MidiEventType::NoteOn(2, 72, 95))); + + // trill OFF + let event = &solo_track_events[5]; + assert_eq!(event.tick, 13200); + assert_eq!(event.track, Some(1)); + assert!(matches!(event.event, MidiEventType::NoteOff(2, 72))); + + // pass trill notes... + + // tremolo ON + let event = &solo_track_events[32]; + assert_eq!(event.tick, 16320); + assert_eq!(event.track, Some(1)); + assert!(matches!(event.event, MidiEventType::NoteOn(2, 60, 95))); + + // tremolo OFF + let event = &solo_track_events[33]; + assert_eq!(event.tick, 16440); + assert_eq!(event.track, Some(1)); + assert!(matches!(event.event, MidiEventType::NoteOff(2, 60))); + + // tremolo ON + let event = &solo_track_events[34]; + assert_eq!(event.tick, 16440); + assert_eq!(event.track, Some(1)); + assert!(matches!(event.event, MidiEventType::NoteOn(2, 60, 95))); + + // tremolo OFF + let event = &solo_track_events[35]; + assert_eq!(event.tick, 16560); + assert_eq!(event.track, Some(1)); + assert!(matches!(event.event, MidiEventType::NoteOff(2, 60))); + + // tremolo ON + let event = &solo_track_events[36]; + assert_eq!(event.tick, 16560); + assert_eq!(event.track, Some(1)); + assert!(matches!(event.event, MidiEventType::NoteOn(2, 60, 95))); + + // tremolo OFF + let event = &solo_track_events[37]; + assert_eq!(event.tick, 16680); + assert_eq!(event.track, Some(1)); + assert!(matches!(event.event, MidiEventType::NoteOff(2, 60))); + + // etc... + } +} diff --git a/src/audio/midi_event.rs b/src/audio/midi_event.rs new file mode 100644 index 0000000..5d29ff1 --- /dev/null +++ b/src/audio/midi_event.rs @@ -0,0 +1,100 @@ +use uuid::Uuid; + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct MidiEvent { + pub id: Uuid, // unique id for testing purpose + pub tick: usize, + pub event: MidiEventType, + pub track: Option, // None = info event +} + +impl MidiEvent { + pub fn is_midi_message(&self) -> bool { + matches!(self.event, MidiEventType::MidiMessage(_, _, _, _)) + } + + pub fn is_tempo_change(&self) -> bool { + matches!(self.event, MidiEventType::TempoChange(_)) + } + + pub fn is_note_event(&self) -> bool { + !self.is_tempo_change() || !self.is_midi_message() + } + + pub fn new_note_on(tick: usize, track: usize, key: i32, velocity: i16, channel: i32) -> Self { + let event = MidiEventType::note_on(channel, key, velocity); + let id = Uuid::new_v4(); + Self { + id, + tick, + event, + track: Some(track), + } + } + + pub fn new_note_off(tick: usize, track: usize, key: i32, channel: i32) -> Self { + let event = MidiEventType::note_off(channel, key); + let id = Uuid::new_v4(); + Self { + id, + tick, + event, + track: Some(track), + } + } + + pub fn new_tempo_change(tick: usize, tempo: i32) -> Self { + let event = MidiEventType::tempo_change(tempo); + let id = Uuid::new_v4(); + Self { + id, + tick, + event, + track: None, + } + } + + pub fn new_midi_message( + tick: usize, + track: usize, + channel: i32, + command: i32, + data1: i32, + data2: i32, + ) -> Self { + let event = MidiEventType::midi_message(channel, command, data1, data2); + let id = Uuid::new_v4(); + Self { + id, + tick, + event, + track: Some(track), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum MidiEventType { + NoteOn(i32, i32, i16), // channel, note, velocity + NoteOff(i32, i32), // channel, note + TempoChange(i32), // tempo in BPM + MidiMessage(i32, i32, i32, i32), // channel: i32, command: i32, data1: i32, data2: i32 +} + +impl MidiEventType { + fn note_on(channel: i32, key: i32, velocity: i16) -> Self { + Self::NoteOn(channel, key, velocity) + } + + fn note_off(channel: i32, key: i32) -> Self { + Self::NoteOff(channel, key) + } + + fn tempo_change(tempo: i32) -> Self { + Self::TempoChange(tempo) + } + + fn midi_message(channel: i32, command: i32, data1: i32, data2: i32) -> Self { + Self::MidiMessage(channel, command, data1, data2) + } +} diff --git a/src/audio/midi_player.rs b/src/audio/midi_player.rs new file mode 100644 index 0000000..aa87a26 --- /dev/null +++ b/src/audio/midi_player.rs @@ -0,0 +1,307 @@ +use crate::audio::midi_builder::MidiBuilder; +use crate::audio::midi_event::MidiEventType; +use crate::audio::midi_player_params::MidiPlayerParams; +use crate::audio::midi_sequencer::MidiSequencer; +use crate::audio::FIRST_TICK; +use crate::parser::song_parser::Song; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::BufferSize; +use rustysynth::{SoundFont, Synthesizer, SynthesizerSettings}; +use std::fs::File; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; +use tokio::sync::watch::Sender; + +const SAMPLE_RATE: u32 = 44100; // number of samples per second + +/// Default sound font file is embedded in the binary (6MB) +const TIMIDITY_SOUND_FONT: &[u8] = include_bytes!("../../resources/TimGM6mb.sf2"); + +pub struct AudioPlayer { + is_playing: bool, + song: Rc, // Song to play (shared with app) + stream: Option>, // Stream is not Send & Sync + sequencer: Arc>, // Need a handle to reset sequencer + player_params: Arc>, // Use to communicate play changes to sequencer + synthesizer: Arc>, // Synthesizer for audio output + beat_sender: Arc>, // Notify beat changes +} + +impl AudioPlayer { + pub fn new( + song: Rc, + song_tempo: i32, + sound_font_file: Option, + beat_sender: Arc>, + ) -> Self { + // default to no solo track + let solo_track_id = None; + + // player params + let player_params = Arc::new(Mutex::new(MidiPlayerParams::new(song_tempo, solo_track_id))); + + // midi sequencer initialization + let builder = MidiBuilder::new(); + let events = builder.build_for_song(&song); + + // sound font setup + let sound_font = match sound_font_file { + Some(sound_font_file) => { + let mut sf2 = File::open(sound_font_file).unwrap(); + SoundFont::new(&mut sf2).unwrap() + } + None => { + let mut sf2 = TIMIDITY_SOUND_FONT; + SoundFont::new(&mut sf2).unwrap() + } + }; + let sound_font = Arc::new(sound_font); + + let synthesizer_settings = SynthesizerSettings::new(SAMPLE_RATE as i32); + let synthesizer_settings = Arc::new(synthesizer_settings); + assert_eq!(synthesizer_settings.sample_rate, SAMPLE_RATE as i32); + + // build new synthesizer for the stream + let mut synthesizer = Synthesizer::new(&sound_font, &synthesizer_settings).unwrap(); + + // apply events at tick=FIRST_TICK to set up synthesizer state + // otherwise a picking a measure *before* playing does produce the correct sound + events + .iter() + .take_while(|event| event.tick == FIRST_TICK) + .filter(|event| event.is_midi_message()) + .for_each(|event| { + if let MidiEventType::MidiMessage(channel, command, data1, data2) = event.event { + synthesizer.process_midi_message(channel, command, data1, data2); + } + }); + let midi_sequencer = MidiSequencer::new(events); + + let synthesizer = Arc::new(Mutex::new(synthesizer)); + let sequencer = Arc::new(Mutex::new(midi_sequencer)); + Self { + is_playing: false, + song, + stream: None, + sequencer, + player_params, + synthesizer, + beat_sender, + } + } + + pub fn is_playing(&self) -> bool { + self.is_playing + } + + pub fn solo_track_id(&self) -> Option { + self.player_params.lock().unwrap().solo_track_id() + } + + pub fn toggle_solo_mode(&mut self, new_track_id: usize) { + let mut params_guard = self.player_params.lock().unwrap(); + if params_guard.solo_track_id() == Some(new_track_id) { + log::info!("Disable solo mode on track {}", new_track_id); + params_guard.set_solo_track_id(None); + } else { + log::info!("Enable solo mode on track {}", new_track_id); + params_guard.set_solo_track_id(Some(new_track_id)); + } + } + + pub fn stop(&mut self) { + // Pause stream + if let Some(stream) = &self.stream { + log::info!("Stopping audio stream"); + stream.pause().unwrap(); + } + self.is_playing = false; + + // reset ticks + let mut sequencer_guard = self.sequencer.lock().unwrap(); + sequencer_guard.reset_last_time(); + sequencer_guard.reset_ticks(); + drop(sequencer_guard); + + // stop all sound in synthesizer + let mut synthesizer_guard = self.synthesizer.lock().unwrap(); + synthesizer_guard.note_off_all(false); + + // Drop stream + self.stream.take(); + } + + pub fn toggle_play(&mut self) { + log::info!("Toggle audio stream"); + if let Some(ref stream) = self.stream { + if self.is_playing { + self.is_playing = false; + stream.pause().unwrap(); + } else { + self.is_playing = true; + // reset last time to not advance time too fast on resume + self.sequencer.lock().unwrap().reset_last_time(); + stream.play().unwrap(); + } + } else { + self.is_playing = true; + let stream = new_output_stream( + self.sequencer.clone(), + self.player_params.clone(), + self.synthesizer.clone(), + self.beat_sender.clone(), + ); + self.stream = Some(Rc::new(stream)); + } + } + + pub fn focus_measure(&mut self, measure_id: usize) { + log::debug!("Focus audio player on measure:{}", measure_id); + let measure = &self.song.measure_headers[measure_id]; + let measure_start_tick = measure.start; + let tempo = measure.tempo.value; + + // move sequencer to measure start tick + let mut sequencer_guard = self.sequencer.lock().unwrap(); + sequencer_guard.set_tick(measure_start_tick as usize); + drop(sequencer_guard); + + // stop current sound + let mut synthesizer_guard = self.synthesizer.lock().unwrap(); + synthesizer_guard.note_off_all(false); + drop(synthesizer_guard); + + // set tempo for focuses measure + let mut player_params_guard = self.player_params.lock().unwrap(); + player_params_guard.set_tempo(tempo); + } +} + +/// Create a new output stream for audio playback. +fn new_output_stream( + sequencer: Arc>, + player_params: Arc>, + synthesizer: Arc>, + beat_notifier: Arc>, +) -> cpal::Stream { + // Initialize audio output + let host = cpal::default_host(); + let device = host.default_output_device().unwrap(); + + let config = device.default_output_config().unwrap(); + assert!( + config.sample_format().is_float(), + "{}", + format!("Unsupported sample format {}", config.sample_format()) + ); + let stream_config: cpal::StreamConfig = config.into(); + + let channels_count = stream_config.channels as usize; + assert_eq!(channels_count, 2); + assert_eq!(stream_config.sample_rate.0, SAMPLE_RATE); + assert_eq!(stream_config.buffer_size, BufferSize::Default); + + // TODO Size initial buffer properly? + // 4410 samples at 44100 Hz is 0.1 second + let mono_sample_count = 4410; + + // reuse buffer for left and right channels across all calls + let mut left: Vec = vec![0_f32; mono_sample_count]; + let mut right: Vec = vec![0_f32; mono_sample_count]; + + let err_fn = |err| log::error!("an error occurred on stream: {}", err); + + let stream = device.build_output_stream( + &stream_config, + move |output: &mut [f32], _: &cpal::OutputCallbackInfo| { + let mut player_params_guard = player_params.lock().unwrap(); + let mut sequencer_guard = sequencer.lock().unwrap(); + sequencer_guard.advance(player_params_guard.tempo()); + let mut synthesizer_guard = synthesizer.lock().unwrap(); + // process midi events for current tick + if let Some(events) = sequencer_guard.get_next_events() { + let tick = sequencer_guard.get_tick(); + let last_tick = sequencer_guard.get_last_tick(); + if !events.is_empty() { + log::debug!( + "Increase {} ticks [{} -> {}] ({} events)", + tick - last_tick, + last_tick, + tick, + events.len() + ); + } + let solo_track_id = player_params_guard.solo_track_id(); + if events.iter().any(|event| event.is_note_event()) { + beat_notifier + .send(tick) + .expect("Failed to send beat notification"); + } + for midi_event in events { + match midi_event.event { + MidiEventType::NoteOn(channel, key, velocity) => { + if let Some(track_id) = solo_track_id { + // skip note on events for other tracks in solo mode + if midi_event.track != Some(track_id) { + continue; + } + } + log::debug!( + "Note on: channel={}, key={}, velocity={}", + channel, + key, + velocity + ); + synthesizer_guard.note_on(channel, key, velocity as i32); + } + MidiEventType::NoteOff(channel, key) => { + log::debug!("Note off: channel={}, key={}", channel, key); + synthesizer_guard.note_off(channel, key); + } + MidiEventType::TempoChange(tempo) => { + log::info!("Tempo changed to {}", tempo); + player_params_guard.set_tempo(tempo); + } + MidiEventType::MidiMessage(channel, command, data1, data2) => { + log::debug!( + "Midi message: channel={}, command={}, data1={}, data2={}", + channel, + command, + data1, + data2 + ); + synthesizer_guard.process_midi_message(channel, command, data1, data2) + } + } + } + } + // Split buffer in two channels (left and right) + let channel_len = output.len() / 2; + + if left.len() < channel_len || right.len() < channel_len { + log::warn!("Buffer too small, skipping audio rendering"); + return; + } + + // Render the waveform. + synthesizer_guard.render(&mut left[..channel_len], &mut right[..channel_len]); + + // Drop locks + drop(sequencer_guard); + drop(synthesizer_guard); + drop(player_params_guard); + + // Interleave the left and right channels into the output buffer. + for (i, (l, r)) in left.iter().zip(right.iter()).take(channel_len).enumerate() { + output[i * 2] = *l; + output[i * 2 + 1] = *r; + } + }, + err_fn, + None, // blocking stream + ); + let stream = stream.unwrap(); + stream.play().unwrap(); + stream +} diff --git a/src/audio/midi_player_params.rs b/src/audio/midi_player_params.rs new file mode 100644 index 0000000..b55dfea --- /dev/null +++ b/src/audio/midi_player_params.rs @@ -0,0 +1,30 @@ +/// Hold values changed during playback of a MIDI events. +pub struct MidiPlayerParams { + tempo: i32, + solo_track_id: Option, +} + +impl MidiPlayerParams { + pub fn new(tempo: i32, solo_track_id: Option) -> Self { + Self { + tempo, + solo_track_id, + } + } + + pub fn solo_track_id(&self) -> Option { + self.solo_track_id + } + + pub fn set_solo_track_id(&mut self, solo_track_id: Option) { + self.solo_track_id = solo_track_id; + } + + pub fn tempo(&self) -> i32 { + self.tempo + } + + pub fn set_tempo(&mut self, tempo: i32) { + self.tempo = tempo; + } +} diff --git a/src/audio/midi_sequencer.rs b/src/audio/midi_sequencer.rs new file mode 100644 index 0000000..9a0eb53 --- /dev/null +++ b/src/audio/midi_sequencer.rs @@ -0,0 +1,201 @@ +use crate::audio::midi_event::MidiEvent; +use std::time::Instant; + +const QUARTER_TIME: i32 = 960; // 1 quarter note = 960 ticks + +pub struct MidiSequencer { + current_tick: usize, // current Midi tick + last_tick: usize, // last Midi tick + last_time: Instant, // last time in milliseconds + sorted_events: Vec, // sorted Midi events +} + +impl MidiSequencer { + pub fn new(sorted_events: Vec) -> Self { + // events are sorted by tick + assert!(sorted_events + .as_slice() + .windows(2) + .all(|w| w[0].tick <= w[1].tick)); + Self { + current_tick: 0, + last_tick: 0, + last_time: Instant::now(), + sorted_events, + } + } + + pub fn set_tick(&mut self, tick: usize) { + self.last_tick = tick; + self.current_tick = tick; + } + + pub fn reset_last_time(&mut self) { + self.last_time = Instant::now(); + } + + pub fn reset_ticks(&mut self) { + self.current_tick = 0; + self.last_tick = 0; + } + + pub fn get_tick(&self) -> usize { + self.current_tick + } + + pub fn get_last_tick(&self) -> usize { + self.last_tick + } + + pub fn get_next_events(&self) -> Option<&[MidiEvent]> { + // do not return events if tick did not change + if self.last_tick == self.current_tick { + return Some(&[]); + } + + assert!(self.last_tick <= self.current_tick); + + // get all events between last tick and next tick using binary search + // TODO could be improved by saving `end_index` to the next `start_index` + let start_index = match self + .sorted_events + .binary_search_by_key(&self.last_tick, |event| event.tick) + { + Ok(position) => position + 1, + Err(position) => { + // exit if end reached + if position == self.sorted_events.len() { + return None; + } + position + } + }; + + let end_index = match self.sorted_events[start_index..] + .binary_search_by_key(&self.current_tick, |event| event.tick) + { + Ok(next_position) => start_index + next_position, + Err(next_position) => { + if next_position == 0 { + // no matching elements + return Some(&[]); + } + // return slice until the last event + start_index + next_position - 1 + } + }; + Some(&self.sorted_events[start_index..=end_index]) + } + + pub fn advance(&mut self, tempo: i32) { + // init sequencer if first advance after reset + if self.current_tick == self.last_tick { + self.current_tick += 1; + self.last_time = Instant::now(); + return; + } + // check how many ticks have passed since last advance + let now = Instant::now(); + let elapsed = now.duration_since(self.last_time); + let elapsed_secs = elapsed.as_secs_f64(); + let tick_increase = tick_increase(tempo, elapsed_secs); + self.last_time = now; + self.last_tick = self.current_tick; + self.current_tick += tick_increase; + } + + #[cfg(test)] + pub fn advance_tick(&mut self, tick: usize) { + self.last_tick = self.current_tick; + self.current_tick += tick; + } +} + +fn tick_increase(tempo_bpm: i32, elapsed_seconds: f64) -> usize { + let tempo_bps = tempo_bpm as f64 / 60.0; + let bump = QUARTER_TIME as f64 * tempo_bps * elapsed_seconds; + bump as usize +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::audio::midi_builder::MidiBuilder; + use crate::parser::song_parser_tests::parse_gp_file; + use std::collections::HashSet; + use std::rc::Rc; + use std::time::Duration; + + #[test] + fn test_tick_increase() { + let tempo = 100; + let elapsed = Duration::from_millis(32); + let result = tick_increase(tempo, elapsed.as_secs_f64()); + assert_eq!(result, 51); + } + + #[test] + fn test_tick_increase_bis() { + let tempo = 120; + let elapsed = Duration::from_millis(100); + let result = tick_increase(tempo, elapsed.as_secs_f64()); + assert_eq!(result, 192); + } + #[test] + fn test_sequence_song() { + const FILE_PATH: &str = "test-files/Demo v5.gp5"; + let song = parse_gp_file(FILE_PATH).unwrap(); + let song = Rc::new(song); + let builder = MidiBuilder::new(); + let events = builder.build_for_song(&song); + let events_len = 4450; + assert_eq!(events.len(), events_len); + assert_eq!(events[0].tick, 1); + assert_eq!(events.iter().last().unwrap().tick, 189_120); + let mut sequencer = MidiSequencer::new(events.clone()); + + // keep track of elements seen to detect duplicates + let mut seen = HashSet::new(); + + // last_tick:0 current_tick:0 + let batch = sequencer.get_next_events().unwrap(); + assert_eq!(batch.len(), 0); + + // advance time by 1 tick + sequencer.advance_tick(1); + + // last_tick:0 current_tick:1 + let batch = sequencer.get_next_events().unwrap(); + // assert no duplicates + for b in batch { + assert!(seen.insert(b.clone()), "duplicate event {:?}", b); + } + let count_1 = batch.len(); + assert_eq!(&events[0..count_1], batch); + assert!(batch.iter().all(|e| e.is_midi_message())); + + let mut pos = count_1; + loop { + let prev_tick = sequencer.get_tick(); + // advance time by 112 tick + sequencer.advance_tick(112); + let next_tick = sequencer.get_tick(); + assert_eq!(next_tick - prev_tick, 112); + + if let Some(batch) = sequencer.get_next_events() { + // assert no duplicates + for (id, b) in batch.iter().enumerate() { + assert!(seen.insert(b.clone()), "duplicate event at {} {:?}", id, b); + } + + let count = batch.len(); + assert_eq!(&events[pos..pos + count], batch); + pos += count; + } else { + break; + } + } + assert_eq!(pos, events.len()); + assert_eq!(seen.len(), events_len); + } +} diff --git a/src/audio/mod.rs b/src/audio/mod.rs new file mode 100644 index 0000000..4ab7fa4 --- /dev/null +++ b/src/audio/mod.rs @@ -0,0 +1,8 @@ +pub mod midi_builder; +pub mod midi_event; +pub mod midi_player; +mod midi_player_params; +pub mod midi_sequencer; + +/// First tick of a song +pub const FIRST_TICK: usize = 1; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..fe930a5 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,85 @@ +use crate::ui::application::RuxApplication; +use crate::RuxError::ConfigError; +use clap::Parser; +use std::io; +use std::path::PathBuf; + +mod audio; +mod parser; +mod ui; + +fn main() { + let result = main_result(); + std::process::exit(match result { + Ok(()) => 0, + Err(err) => { + // use Display instead of Debug for user friendly error messages + log::error!("{}", err); + 1 + } + }); +} + +pub fn main_result() -> Result<(), RuxError> { + // setup logging + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("ruxguitar=info")) + .init(); + + // args + let mut args = CliArgs::parse(); + let sound_font_file = args.sound_font_file.take().map(PathBuf::from); + + // check if sound font file exists + if let Some(sound_font_file) = &sound_font_file { + if !sound_font_file.exists() { + let err = ConfigError(format!("Sound font file not found {:?}", sound_font_file)); + return Err(err); + } + log::info!("Starting with custom sound font file {:?}", sound_font_file); + } + + let args = ApplicationArgs { + sound_font_bank: sound_font_file, + }; + + // go! + RuxApplication::start(args)?; + Ok(()) +} + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct CliArgs { + /// Optional path to a sound font file. + #[arg(long)] + sound_font_file: Option, +} + +#[derive(Debug, Clone)] +pub struct ApplicationArgs { + sound_font_bank: Option, +} + +#[derive(Debug, thiserror::Error)] +pub enum RuxError { + #[error("iced error: {0}")] + IcedError(iced::Error), + #[error("configuration error: {0}")] + ConfigError(String), + #[error("parsing error: {0}")] + ParsingError(String), + #[error("other error: {0}")] + OtherError(String), +} + +impl From for RuxError { + fn from(error: iced::Error) -> Self { + RuxError::IcedError(error) + } +} + +impl From for RuxError { + fn from(error: io::Error) -> Self { + RuxError::OtherError(error.to_string()) + } +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs new file mode 100644 index 0000000..bc0e734 --- /dev/null +++ b/src/parser/mod.rs @@ -0,0 +1,4 @@ +mod music_parser; +mod primitive_parser; +pub mod song_parser; +pub mod song_parser_tests; diff --git a/src/parser/music_parser.rs b/src/parser/music_parser.rs new file mode 100644 index 0000000..0145414 --- /dev/null +++ b/src/parser/music_parser.rs @@ -0,0 +1,531 @@ +use crate::parser::primitive_parser::{ + parse_byte, parse_byte_size_string, parse_int, parse_int_byte_sized_string, parse_signed_byte, + skip, +}; +use crate::parser::song_parser::{ + convert_velocity, parse_beat_effects, parse_chord, parse_color, parse_duration, + parse_measure_headers, parse_note_effects, Beat, GpVersion, Measure, Note, NoteEffect, + NoteType, Song, Track, Voice, QUARTER_TIME, +}; +use nom::multi::count; +use nom::sequence::tuple; +use nom::IResult; + +pub struct MusicParser { + song: Song, +} + +impl MusicParser { + pub fn new(song: Song) -> Self { + Self { song } + } + pub fn take_song(&mut self) -> Song { + std::mem::take(&mut self.song) + } + + pub fn parse_music_data<'a>(&'a mut self, i: &'a [u8]) -> IResult<&[u8], ()> { + // skip directions & master reverb + let i = skip(i, 42); + + let (i, (measure_count, track_count)) = tuple(( + parse_int, // Measure count + parse_int, // Track count + ))(i)?; + + log::debug!( + "Parsing music data -> track_count: {} measure_count {}", + track_count, + measure_count + ); + + let song_tempo = self.song.tempo.value; + let (i, measure_headers) = parse_measure_headers(measure_count, song_tempo)(i)?; + self.song.measure_headers = measure_headers; + + let (i, tracks) = self.parse_tracks(track_count as usize)(i)?; + self.song.tracks = tracks; + + let (i, _measures) = self.parse_measures(measure_count, track_count)(i)?; + + Ok((i, ())) + } + + pub fn parse_tracks( + &mut self, + tracks_count: usize, + ) -> impl FnMut(&[u8]) -> IResult<&[u8], Vec> + '_ { + move |i| { + log::debug!("Parsing {} tracks", tracks_count); + let mut i = i; + let mut tracks = Vec::with_capacity(tracks_count); + for index in 1..=tracks_count { + let (inner, track) = self.parse_track(index)(i)?; + i = inner; + tracks.push(track); + } + // tracks done + if self.song.version == GpVersion::GP5 { + i = skip(i, 2); + } else { + i = skip(i, 1); + } + + Ok((i, tracks)) + } + } + + pub fn parse_track( + &mut self, + number: usize, + ) -> impl FnMut(&[u8]) -> IResult<&[u8], Track> + '_ { + move |i| { + log::debug!("--------"); + log::debug!("Parsing track {}", number); + let mut i = skip(i, 1); + let mut track = Track::default(); + if number == 1 || self.song.version == GpVersion::GP5 { + i = skip(i, 1); + }; + + track.number = number as i32; + + let (inner, name) = parse_byte_size_string(40)(i)?; + i = inner; + log::debug!("Track name:{}", name); + track.name = name; + + let (inner, string_count) = parse_int(i)?; + i = inner; + log::debug!("String count: {}", string_count); + + // tunings + let (inner, tunings) = count(parse_int, 7)(i)?; + i = inner; + log::debug!("Tunings: {:?}", tunings); + track.strings = tunings + .iter() + .enumerate() + .filter(|(i, _)| (*i as i32) < string_count) + .map(|(i, &t)| (i as i32 + 1, t)) + .collect(); + + // midi port + let (inner, port) = parse_int(i)?; + i = inner; + track.midi_port = port as u8; + + // parse track channel info + let (inner, channel_id) = self.parse_track_channel()(i)?; + track.channel_id = channel_id as u8; + i = inner; + + // fret + let (inner, fret_count) = parse_int(i)?; + i = inner; + track.fret_count = fret_count as u8; + + let (inner, offset) = parse_int(i)?; + i = inner; + track.offset = offset; + + let (inner, color) = parse_color(i)?; + i = inner; + track.color = color; + + i = if self.song.version == GpVersion::GP5 { + // skip 44 + skip(i, 44) + } else { + // skip 49 + skip(i, 49) + }; + + if self.song.version > GpVersion::GP5 { + let (inner, _) = parse_int_byte_sized_string(i)?; + i = inner; + let (inner, _) = parse_int_byte_sized_string(i)?; + i = inner; + }; + Ok((i, track)) + } + } + + /// Read MIDI channel. MIDI channel in Guitar Pro is represented by two integers. + /// First is zero-based number of channel, second is zero-based number of channel used for effects. + pub fn parse_track_channel(&mut self) -> impl FnMut(&[u8]) -> IResult<&[u8], i32> + '_ { + log::debug!("Parsing track channel"); + |i| { + let (i, (mut gm_channel_1, mut gm_channel_2)) = tuple((parse_int, parse_int))(i)?; + gm_channel_1 -= 1; + gm_channel_2 -= 1; + + log::debug!("Track channel gm1: {} gm2: {}", gm_channel_1, gm_channel_2); + + if let Some(channel) = self.song.midi_channels.get_mut(gm_channel_1 as usize) { + // if not percussion - set effect channel + if channel.channel_id != 9 { + channel.effect_channel_id = gm_channel_2 as u8; + } + } else { + log::debug!("channel {} not found", gm_channel_1); + } + Ok((i, gm_channel_1)) + } + } + + /// Read measures. Measures are written in the following order: + /// - measure 1/track 1 + /// - measure 1/track 2 + /// - ... + /// - measure 1/track m + /// - measure 2/track 1 + /// - measure 2/track 2 + /// - ... + /// - measure 2/track m + /// - ... + /// - measure n/track 1 + /// - measure n/track 2 + /// - ... + /// - measure n/track m + pub fn parse_measures( + &mut self, + measure_count: i32, + track_count: i32, + ) -> impl FnMut(&[u8]) -> IResult<&[u8], ()> + '_ { + move |i: &[u8]| { + log::debug!("--------"); + log::debug!("Parsing measures"); + let mut start = QUARTER_TIME; + let mut i = i; + for measure_index in 0..measure_count as usize { + // set header start + self.song.measure_headers[measure_index].start = start; + for track_index in 0..track_count as usize { + let (inner, measure) = + self.parse_measure(start, measure_index, track_index)(i)?; + i = inner; + // push measure on track + self.song.tracks[track_index].measures.push(measure); + i = skip(i, 1); + } + // update start with measure length + let measure_length = self.song.measure_headers[measure_index].length(); + assert!(measure_length > 0, "Measure length is 0"); + start += measure_length; + } + Ok((i, ())) + } + } + + pub fn parse_measure( + &mut self, + measure_start: i64, + measure_index: usize, + track_index: usize, + ) -> impl FnMut(&[u8]) -> IResult<&[u8], Measure> + '_ { + move |i: &[u8]| { + log::debug!("--------"); + log::debug!( + "Parsing measure {} for track {}", + measure_index, + track_index + ); + let mut i = i; + let mut measure = Measure { + header_index: measure_index, + track_index, + ..Default::default() + }; + for voice_index in 0..crate::parser::song_parser::MAX_VOICES { + // voices have the same start value + let beat_start = measure_start; + log::debug!("--------"); + log::debug!("Parsing voice {}", voice_index); + let (inner, voice) = self.parse_voice(beat_start, track_index, measure_index)(i)?; + i = inner; + measure.voices.push(voice); + } + Ok((i, measure)) + } + } + + pub fn parse_voice( + &mut self, + mut beat_start: i64, + track_index: usize, + measure_index: usize, + ) -> impl FnMut(&[u8]) -> IResult<&[u8], Voice> + '_ { + move |i: &[u8]| { + let mut i = i; + let (inner, beats) = parse_int(i)?; + i = inner; + let mut voice = Voice { + measure_index: measure_index as i16, + ..Default::default() + }; + log::debug!("--------"); + log::debug!("...with {} beats", beats); + for b in 0..beats { + log::debug!("--------"); + log::debug!("Parsing beat {}", b); + let (inner, beat) = self.parse_beat(beat_start, track_index, measure_index)(i)?; + if !beat.empty { + beat_start += beat.duration.time() as i64; + } + i = inner; + voice.beats.push(beat); + } + Ok((i, voice)) + } + } + + pub fn parse_beat( + &mut self, + start: i64, + track_index: usize, + measure_index: usize, + ) -> impl FnMut(&[u8]) -> IResult<&[u8], Beat> + '_ { + move |i: &[u8]| { + let mut i = i; + let (inner, flags) = parse_byte(i)?; + i = inner; + + // make new beat at starting time + let mut beat = Beat { + start, + ..Default::default() + }; + + // beat type + if (flags & 0x40) == 0x40 { + let (inner, beat_type) = parse_byte(i)?; + i = inner; + beat.empty = beat_type & 0x02 == 0 + } + + // beat duration is an eighth note + let (inner, duration) = parse_duration(flags)(i)?; + beat.duration = duration; + i = inner; + + // beat chords + if (flags & 0x02) != 0 { + let track = &self.song.tracks[track_index]; + let (inner, chord) = parse_chord(track.strings.len() as u8)(i)?; + i = inner; + beat.effect.chord = Some(chord); + } + + // beat text + if (flags & 0x04) != 0 { + let (inner, text) = parse_int_byte_sized_string(i)?; + i = inner; + log::debug!("Beat text: {}", text); + beat.text = text; + } + + let mut note_effect = NoteEffect::default(); + // beat effect + if (flags & 0x08) != 0 { + let (inner, ()) = parse_beat_effects(&mut beat, &mut note_effect)(i)?; + i = inner; + } + + // parse mix change + if (flags & 0x10) != 0 { + let (inner, ()) = self.parse_mix_change(measure_index)(i)?; + i = inner; + } + + // parse notes + let (inner, string_flags) = parse_byte(i)?; + i = inner; + let track = &self.song.tracks[track_index]; + log::debug!("Parsing notes for beat ({} strings)", track.strings.len()); + assert!(!track.strings.is_empty()); + for (string_id, string_value) in track.strings.iter().enumerate() { + if (string_flags & 1 << (7 - string_value.0)) > 0 { + log::debug!("Parsing note for string {}", string_id + 1); + let mut note = Note::new(note_effect.clone()); + let (inner, ()) = self.parse_note(&mut note, string_value, track_index)(i)?; + i = inner; + beat.notes.push(note); + } + } + + i = skip(i, 1); + let (inner, read) = parse_byte(i)?; + i = inner; + if (read & 0x08) != 0 { + i = skip(i, 1); + } + Ok((i, beat)) + } + } + + /// Get note value of tied note + fn get_tied_note_value(&self, string_index: i8, track_index: usize) -> i16 { + let track = &self.song.tracks[track_index]; + for m in (0usize..track.measures.len()).rev() { + for v in (0usize..track.measures[m].voices.len()).rev() { + for b in 0..track.measures[m].voices[v].beats.len() { + for n in 0..track.measures[m].voices[v].beats[b].notes.len() { + if track.measures[m].voices[v].beats[b].notes[n].string == string_index { + return track.measures[m].voices[v].beats[b].notes[n].value; + } + } + } + } + } + -1 + } + + pub fn parse_mix_change( + &mut self, + measure_index: usize, + ) -> impl FnMut(&[u8]) -> IResult<&[u8], ()> + '_ { + move |i: &[u8]| { + log::debug!("Parsing mix change"); + let mut i = i; + + // instrument + let (inner, _) = parse_signed_byte(i)?; + i = inner; + + i = skip(i, 16); + + let (inner, (volume, pan, chorus, reverb, phaser, tremolo)) = tuple(( + parse_signed_byte, + parse_signed_byte, + parse_signed_byte, + parse_signed_byte, + parse_signed_byte, + parse_signed_byte, + ))(i)?; + i = inner; + + let (inner, tempo_name) = parse_int_byte_sized_string(i)?; + log::debug!("Tempo name: {}", tempo_name); + i = inner; + + let (inner, tempo_value) = parse_int(i)?; + i = inner; + + if volume >= 0 { + i = skip(i, 1); + } + if pan >= 0 { + i = skip(i, 1); + } + if chorus >= 0 { + i = skip(i, 1); + } + if reverb >= 0 { + i = skip(i, 1); + } + if phaser >= 0 { + i = skip(i, 1); + } + if tremolo >= 0 { + i = skip(i, 1); + } + + if tempo_value >= 0 { + // update tempo value for all next measure headers + self.song.measure_headers[measure_index..] + .iter_mut() + .for_each(|mh| { + mh.tempo.value = tempo_value; + mh.tempo.name = Some(tempo_name.clone()); + }); + i = skip(i, 1); + if self.song.version > GpVersion::GP5 { + i = skip(i, 1); + } + } + + i = skip(i, 2); + + if self.song.version > GpVersion::GP5 { + let (inner, _) = + tuple((parse_int_byte_sized_string, parse_int_byte_sized_string))(i)?; + i = inner; + } + + Ok((i, ())) + } + } + + pub fn parse_note<'a>( + &'a self, + note: &'a mut Note, + guitar_string: &'a (i32, i32), + track_index: usize, + ) -> impl FnMut(&[u8]) -> IResult<&[u8], ()> + 'a { + move |i| { + log::debug!("Parsing note {:?}", guitar_string); + let mut i = i; + let (inner, flags) = parse_byte(i)?; + i = inner; + let string = guitar_string.0 as i8; + note.string = string; + note.effect.heavy_accentuated_note = (flags & 0x02) == 0x02; + note.effect.ghost_note = (flags & 0x04) == 0x04; + note.effect.accentuated_note = (flags & 0x40) == 0x40; + + // note type + if (flags & 0x20) == 0x20 { + let (inner, note_type) = parse_byte(i)?; + i = inner; + note.kind = NoteType::get_note_type(note_type); + } + + // note velocity + if (flags & 0x10) == 0x10 { + let (inner, velocity) = parse_signed_byte(i)?; + i = inner; + note.velocity = convert_velocity(velocity as i16); + } + + // note value + if (flags & 0x20) == 0x20 { + let (inner, fret) = parse_signed_byte(i)?; + i = inner; + + let value = if note.kind == NoteType::Tie { + self.get_tied_note_value(string, track_index) + } else { + fret as i16 + }; + // value is between 0 and 99 + if (0..100).contains(&value) { + note.value = value; + } else { + note.value = 0; + } + } + + // fingering + if (flags & 0x80) != 0 { + i = skip(i, 2); + } + + // duration percent + if (flags & 0x01) != 0 { + i = skip(i, 8); + } + + // swap accidentals + let (inner, swap) = parse_byte(i)?; + i = inner; + note.swap_accidentals = swap & 0x02 == 0x02; + + if (flags & 0x08) == 0x08 { + let (inner, ()) = parse_note_effects(note)(i)?; + i = inner; + } + + Ok((i, ())) + } + } +} diff --git a/src/parser/primitive_parser.rs b/src/parser/primitive_parser.rs new file mode 100644 index 0000000..cf78a20 --- /dev/null +++ b/src/parser/primitive_parser.rs @@ -0,0 +1,132 @@ +use encoding_rs::WINDOWS_1252; +use nom::combinator::{flat_map, map}; +use nom::{bytes, number, IResult}; + +pub fn parse_signed_byte(i: &[u8]) -> IResult<&[u8], i8> { + number::complete::le_i8(i) +} + +pub fn parse_int(i: &[u8]) -> IResult<&[u8], i32> { + number::complete::le_i32(i) +} + +pub fn parse_bool(i: &[u8]) -> IResult<&[u8], bool> { + map(number::complete::le_u8, |b| b == 1)(i) +} + +pub fn parse_short(i: &[u8]) -> IResult<&[u8], i16> { + number::complete::le_i16(i) +} + +/// Skip `n` bytes. +pub fn skip(i: &[u8], n: usize) -> &[u8] { + if i.is_empty() { + return i; + } + &i[n..] +} + +pub fn parse_byte(i: &[u8]) -> IResult<&[u8], u8> { + number::complete::le_u8(i) +} + +pub fn make_string(i: &[u8]) -> String { + let (cow, encoding_used, had_errors) = WINDOWS_1252.decode(i); + if had_errors { + log::debug!("Error parsing string with {:?}", encoding_used); + match std::str::from_utf8(i) { + Ok(s) => s.to_string(), + Err(e) => { + log::debug!("Error UTF-8 string parsing:{}", e); + String::new() + } + } + } else { + cow.to_string() + } +} + +/// Parse string of length `len`. +pub fn parse_string(len: i32) -> impl FnMut(&[u8]) -> IResult<&[u8], String> { + log::debug!("Parsing string of length {}", len); + move |i: &[u8]| { + map( + bytes::complete::take(len as usize), + move |data: &[u8]| { + if len == 0 { + return String::new(); + } + let sub = &data[0..len as usize]; + make_string(sub) + }, + ) + }(i) +} + +/// Size of string encoded as Int. +pub fn parse_int_sized_string(i: &[u8]) -> IResult<&[u8], String> { + flat_map(parse_int, parse_string)(i) +} + +/// Size of Strings provided +/// `size`: real string length +/// `length`: optional provided length (in case of blank chars after the string) +pub fn parse_byte_size_string(size: usize) -> impl FnMut(&[u8]) -> IResult<&[u8], String> { + move |i: &[u8]| { + let (i, length) = parse_byte(i)?; + log::debug!( + "Parsing byte sized string of length {} for String size {}", + length, + size + ); + + let (i, peeked) = nom::combinator::peek(bytes::complete::take(length))(i)?; + let sub = if length > size as u8 { + &peeked[..size] + } else { + peeked + }; + let string = make_string(sub); + log::debug!("Parsed raw string:{:?}", string); + // consume size + let (i, _) = bytes::complete::take(size)(i)?; + Ok((i, string)) + } +} + +/// Size of string encoded as Byte. +#[allow(unused)] +pub fn parse_byte_sized_string(i: &[u8]) -> IResult<&[u8], String> { + flat_map(parse_byte, |str_len| parse_string(str_len as i32))(i) +} + +/// Size of string encoded as Int, but the size is encoded as a byte. +pub fn parse_int_byte_sized_string(i: &[u8]) -> IResult<&[u8], String> { + flat_map(parse_int, |len| { + flat_map(number::complete::i8, move |str_len| { + if str_len < 0 { + log::info!("Negative string length: {}", str_len); + parse_string(len - 1) + } else { + assert_eq!(len - 1, str_len as i32, "String length mismatch"); + parse_string(str_len as i32) + } + }) + })(i) +} + +#[cfg(test)] +mod tests { + use crate::parser::primitive_parser::parse_byte_size_string; + + #[test] + fn test_read_byte_size_string() { + let data: Vec = vec![ + 0x18, 0x46, 0x49, 0x43, 0x48, 0x49, 0x45, 0x52, 0x20, 0x47, 0x55, 0x49, 0x54, 0x41, + 0x52, 0x20, 0x50, 0x52, 0x4f, 0x20, 0x76, 0x33, 0x2e, 0x30, 0x30, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + ]; + let (_rest, res) = parse_byte_size_string(30)(&data).unwrap(); + assert_eq!(res, "FICHIER GUITAR PRO v3.00"); + } +} diff --git a/src/parser/song_parser.rs b/src/parser/song_parser.rs new file mode 100644 index 0000000..a6f62f0 --- /dev/null +++ b/src/parser/song_parser.rs @@ -0,0 +1,1549 @@ +use crate::parser::music_parser::MusicParser; +use crate::parser::primitive_parser::{ + parse_bool, parse_byte, parse_byte_size_string, parse_int, parse_int_byte_sized_string, + parse_int_sized_string, parse_short, parse_signed_byte, skip, +}; +use crate::RuxError; +use nom::bytes::complete::take; +use nom::combinator::{cond, flat_map, map}; +use nom::multi::count; +use nom::sequence::{preceded, tuple}; +use nom::IResult; +use std::fmt::Debug; + +// GP4 docs at +// GP5 docs thanks to Tuxguitar and for the help + +pub const MAX_VOICES: usize = 2; + +pub const QUARTER_TIME: i64 = 960; +pub const QUARTER: u16 = 4; + +pub const DURATION_EIGHTH: u8 = 8; +pub const DURATION_SIXTEENTH: u8 = 16; +pub const DURATION_THIRTY_SECOND: u8 = 32; +pub const DURATION_SIXTY_FOURTH: u8 = 64; + +pub const BEND_EFFECT_MAX_POSITION: u8 = 12; + +pub const SEMITONE_LENGTH: f32 = 1.0; +pub const GP_BEND_SEMITONE: f32 = 25.0; +pub const GP_BEND_POSITION: f32 = 60.0; + +pub const SHARP_NOTES: [&str; 12] = [ + "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", +]; + +pub const DEFAULT_PERCUSSION_BANK: u8 = 128; + +pub const DEFAULT_BANK: u8 = 0; + +pub const MIN_VELOCITY: i16 = 15; +pub const VELOCITY_INCREMENT: i16 = 16; +pub const DEFAULT_VELOCITY: i16 = MIN_VELOCITY + VELOCITY_INCREMENT * 5; // FORTE + +/// Convert Guitar Pro dynamic value to raw MIDI velocity +pub fn convert_velocity(v: i16) -> i16 { + MIN_VELOCITY + (VELOCITY_INCREMENT * v) - VELOCITY_INCREMENT +} + +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Default)] +pub enum GpVersion { + #[default] + GP3, + GP4, + GP4_06, + GP5, + GP5_10, +} + +#[derive(Debug, PartialEq, Default)] +pub struct Song { + pub version: GpVersion, + pub song_info: SongInfo, + pub triplet_feel: Option, // only < GP5 + pub lyrics: Option, + pub page_setup: Option, + pub tempo: Tempo, + pub hide_tempo: Option, + pub key_signature: i8, + pub octave: Option, + pub midi_channels: Vec, + pub measure_headers: Vec, + pub tracks: Vec, +} + +impl Song { + pub fn get_measure_beat_for_tick(&self, track_id: usize, tick: usize) -> (usize, usize) { + let mut measure_index = 0; + let mut beat_index = 0; + // TODO could pre-compute boundaries with btree map + for (i, measure) in self.measure_headers.iter().enumerate() { + if measure.start > tick as i64 { + break; + } else { + measure_index = i; + } + } + let voice = &self.tracks[track_id].measures[measure_index].voices[0]; + for (j, beat) in voice.beats.iter().enumerate() { + if beat.start > tick as i64 { + break; + } else { + beat_index = j; + } + } + (measure_index, beat_index) + } +} + +#[derive(Debug, PartialEq)] +pub struct MidiChannel { + pub channel_id: u8, + pub effect_channel_id: u8, + pub instrument: i32, + pub volume: i8, + pub balance: i8, + pub chorus: i8, + pub reverb: i8, + pub phaser: i8, + pub tremolo: i8, + pub bank: u8, +} + +impl MidiChannel { + pub fn is_percussion(&self) -> bool { + self.bank == DEFAULT_PERCUSSION_BANK + } +} + +#[derive(Debug, PartialEq)] +pub struct Padding { + pub right: i32, + pub top: i32, + pub left: i32, + pub bottom: i32, +} +#[derive(Debug, PartialEq)] +pub struct Point { + pub x: i32, + pub y: i32, +} + +#[derive(Debug, PartialEq)] +pub struct PageSetup { + pub page_size: Point, + pub page_margin: Padding, + pub score_size_proportion: f32, + pub header_and_footer: i16, + pub title: String, + pub subtitle: String, + pub artist: String, + pub album: String, + pub words: String, + pub music: String, + pub word_and_music: String, + pub copyright: String, + pub page_number: String, +} +#[derive(Debug, PartialEq)] +pub struct Lyrics { + pub track_choice: i32, + pub lines: Vec<(i32, String)>, +} + +#[derive(Debug, PartialEq, Default)] +pub struct SongInfo { + pub name: String, + pub subtitle: String, + pub artist: String, + pub album: String, + pub author: String, + pub words: Option, + pub copyright: String, + pub writer: String, + pub instructions: String, + pub notices: Vec, +} + +#[derive(Debug, PartialEq)] +pub struct Marker { + pub title: String, + pub color: i32, +} + +pub const KEY_SIGNATURES: [&str; 34] = [ + "Fâ™­ major", + "Câ™­ major", + "Gâ™­ major", + "Dâ™­ major", + "Aâ™­ major", + "Eâ™­ major", + "Bâ™­ major", + "F major", + "C major", + "G major", + "D major", + "A major", + "E major", + "B major", + "F# major", + "C# major", + "G# major", + "Dâ™­ minor", + "Aâ™­ minor", + "Eâ™­ minor", + "Bâ™­ minor", + "F minor", + "C minor", + "G minor", + "D minor", + "A minor", + "E minor", + "B minor", + "F# minor", + "C# minor", + "G# minor", + "D# minor", + "A# minor", + "E# minor", +]; + +#[derive(Debug, PartialEq, Eq)] +pub struct KeySignature { + pub key: i8, + pub is_minor: bool, +} + +impl KeySignature { + pub fn new(key: i8, is_minor: bool) -> Self { + KeySignature { key, is_minor } + } +} + +impl std::fmt::Display for KeySignature { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let index: usize = if self.is_minor { + (23i8 + self.key) as usize + } else { + (8i8 + self.key) as usize + }; + write!(f, "{}", KEY_SIGNATURES[index]) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum TripletFeel { + None, + Eighth, + Sixteenth, +} + +#[derive(Debug, PartialEq)] +pub struct Tempo { + pub value: i32, + pub name: Option, +} + +impl Tempo { + fn new(value: i32, name: Option) -> Self { + Tempo { value, name } + } +} + +impl Default for Tempo { + fn default() -> Self { + Tempo { + value: 120, + name: None, + } + } +} + +#[derive(Debug, PartialEq)] +pub struct MeasureHeader { + pub start: i64, + pub time_signature: TimeSignature, + pub tempo: Tempo, + pub marker: Option, + pub repeat_open: bool, + pub repeat_alternative: u8, + pub repeat_close: i8, + pub triplet_feel: TripletFeel, + pub key_signature: KeySignature, +} + +impl Default for MeasureHeader { + fn default() -> Self { + MeasureHeader { + start: QUARTER_TIME, + time_signature: TimeSignature::default(), + tempo: Tempo::default(), + marker: None, + repeat_open: false, + repeat_alternative: 0, + repeat_close: 0, + triplet_feel: TripletFeel::None, + key_signature: KeySignature::new(0, false), + } + } +} + +impl MeasureHeader { + pub fn length(&self) -> i64 { + let numerator = self.time_signature.numerator as i64; + let denominator = self.time_signature.denominator.time() as i64; + numerator * denominator + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct TimeSignature { + pub numerator: i8, + pub denominator: Duration, +} + +impl Default for TimeSignature { + fn default() -> Self { + TimeSignature { + numerator: 4, + denominator: Duration::default(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Duration { + pub value: u16, + pub dotted: bool, + pub double_dotted: bool, + pub tuplet_enters: u8, + pub tuplet_times: u8, +} + +impl Default for Duration { + fn default() -> Self { + Duration { + value: QUARTER, + dotted: false, + double_dotted: false, + tuplet_enters: 1, + tuplet_times: 1, + } + } +} + +impl Duration { + pub fn convert_time(&self, time: u32) -> u32 { + log::debug!( + "time:{} tuplet_times:{} tuplet_enters:{}", + time, + self.tuplet_times, + self.tuplet_enters + ); + time * self.tuplet_times as u32 / self.tuplet_enters as u32 + } + + pub fn time(&self) -> u32 { + let mut time = QUARTER_TIME as f64 * (4.0 / self.value as f64); + if self.dotted { + time += time / 2.0; + } else if self.double_dotted { + time += (time / 4.0) * 3.0; + } + self.convert_time(time as u32) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BendPoint { + pub position: u8, + pub value: i8, +} + +impl BendPoint { + pub fn get_time(&self, duration: usize) -> usize { + duration * self.position as usize / BEND_EFFECT_MAX_POSITION as usize + } +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct BendEffect { + pub points: Vec, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct TremoloBarEffect { + pub points: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GraceEffect { + pub duration: u8, + pub fret: i8, + pub is_dead: bool, + pub is_on_beat: bool, + pub transition: GraceEffectTransition, + pub velocity: i16, +} + +impl GraceEffect { + pub fn duration_time(&self) -> f32 { + (QUARTER_TIME as f32 / 16.00) * self.duration as f32 + } +} + +impl Default for GraceEffect { + fn default() -> Self { + GraceEffect { + duration: 0, + fret: 0, + is_dead: false, + is_on_beat: false, + transition: GraceEffectTransition::None, + velocity: 0, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GraceEffectTransition { + /// No transition + None = 0, + /// Slide from the grace note to the real one. + Slide, + /// Perform a bend from the grace note to the real one. + Bend, + /// Perform a hammer on. + Hammer, +} + +impl GraceEffectTransition { + pub fn get_grace_effect_transition(value: i8) -> GraceEffectTransition { + match value { + 0 => GraceEffectTransition::None, + 1 => GraceEffectTransition::Slide, + 2 => GraceEffectTransition::Bend, + 3 => GraceEffectTransition::Hammer, + _ => panic!("Cannot get transition for the grace effect"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PitchClass { + pub note: String, + pub just: i8, + /// flat (-1), none (0) or sharp (1). + pub accidental: i8, + pub value: i8, + pub sharp: bool, +} + +impl PitchClass { + pub fn from(just: i8, accidental: Option, sharp: Option) -> PitchClass { + let mut p = PitchClass { + just, + accidental: 0, + value: -1, + sharp: true, + note: String::with_capacity(2), + }; + let pitch: i8; + let accidental2: i8; + if let Some(a) = accidental { + pitch = p.just; + accidental2 = a; + } else { + let value = p.just % 12; + p.note = if value >= 0 { + String::from(SHARP_NOTES[value as usize]) + } else { + String::from(SHARP_NOTES[(12 + value) as usize]) + }; + if p.note.ends_with('b') { + accidental2 = -1; + p.sharp = false; + } else if p.note.ends_with('#') { + accidental2 = 1; + } else { + accidental2 = 0; + } + pitch = value - accidental2; + } + p.just = pitch % 12; + p.accidental = accidental2; + p.value = p.just + accidental2; + if sharp.is_none() { + p.sharp = p.accidental >= 0; + } + p + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum HarmonicType { + Natural, + Artificial, + Tapped, + Pinch, + Semi, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Octave { + None, + Ottava, + Quindicesima, + OttavaBassa, + QuindicesimaBassa, +} + +impl Octave { + pub fn get_octave(value: u8) -> Octave { + match value { + 0 => Octave::None, + 1 => Octave::Ottava, + 2 => Octave::Quindicesima, + 3 => Octave::OttavaBassa, + 4 => Octave::QuindicesimaBassa, + _ => panic!("Cannot get octave value"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HarmonicEffect { + pub kind: HarmonicType, + // artificial harmonic + pub pitch: Option, + pub octave: Option, + // tapped harmonic + pub right_hand_fret: Option, +} + +impl Default for HarmonicEffect { + fn default() -> Self { + HarmonicEffect { + kind: HarmonicType::Natural, + pitch: None, + octave: None, + right_hand_fret: None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SlideType { + IntoFromAbove, + IntoFromBelow, + ShiftSlideTo, + LegatoSlideTo, + OutDownwards, + OutUpWards, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct TrillEffect { + pub fret: i8, + pub duration: Duration, +} + +impl TrillEffect { + fn from_trill_period(period: i8) -> u16 { + match period { + 1 => DURATION_SIXTEENTH as u16, + 2 => DURATION_THIRTY_SECOND as u16, + 3 => DURATION_SIXTY_FOURTH as u16, + _ => panic!("Cannot get trill period"), + } + } +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct TremoloPickingEffect { + pub duration: Duration, +} + +impl TremoloPickingEffect { + fn from_tremolo_value(value: i8) -> u16 { + match value { + 1 => DURATION_EIGHTH as u16, + 3 => DURATION_SIXTEENTH as u16, + 2 => DURATION_THIRTY_SECOND as u16, + _ => panic!("Cannot get tremolo value"), + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum NoteType { + Rest, + Normal, + Tie, + Dead, + Unknown(u8), +} + +impl NoteType { + pub fn get_note_type(value: u8) -> NoteType { + match value { + 0 => NoteType::Rest, + 1 => NoteType::Normal, + 2 => NoteType::Tie, + 3 => NoteType::Dead, + _ => NoteType::Unknown(value), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NoteEffect { + pub accentuated_note: bool, + pub bend: Option, + pub ghost_note: bool, + pub grace: Option, + pub hammer: bool, + pub harmonic: Option, + pub heavy_accentuated_note: bool, + pub let_ring: bool, + pub palm_mute: bool, + pub slide: Option, + pub staccato: bool, + pub tremolo_picking: Option, + pub trill: Option, + pub fade_in: bool, + pub vibrato: bool, + pub slap: SlapEffect, + pub tremolo_bar: Option, +} + +impl Default for NoteEffect { + fn default() -> Self { + NoteEffect { + accentuated_note: false, + bend: None, + ghost_note: false, + grace: None, + hammer: false, + harmonic: None, + heavy_accentuated_note: false, + let_ring: false, + palm_mute: false, + slide: None, + staccato: false, + tremolo_picking: None, + trill: None, + fade_in: false, + vibrato: false, + slap: SlapEffect::None, + tremolo_bar: None, + } + } +} + +#[derive(Debug, Default, PartialEq, Eq)] +pub struct Chord { + pub length: u8, + pub sharp: Option, + pub root: Option, + pub bass: Option, + pub add: Option, + pub name: String, + pub first_fret: Option, + pub strings: Vec, + pub omissions: Vec, + pub show: Option, + pub new_format: Option, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum BeatStrokeDirection { + None, + Up, + Down, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct BeatStroke { + direction: BeatStrokeDirection, + value: u16, +} + +impl Default for BeatStroke { + fn default() -> Self { + BeatStroke { + direction: BeatStrokeDirection::None, + value: 0, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SlapEffect { + None, + Tapping, + Slapping, + Popping, +} + +#[derive(Debug, Default, PartialEq, Eq)] +pub struct BeatEffects { + pub stroke: BeatStroke, + pub chord: Option, +} + +#[derive(Debug, PartialEq)] +pub struct Note { + pub value: i16, + pub velocity: i16, + pub string: i8, + pub effect: NoteEffect, + pub swap_accidentals: bool, + pub kind: NoteType, + tuplet: Option, +} + +impl Note { + pub fn new(note_effect: NoteEffect) -> Self { + Note { + value: 0, + velocity: DEFAULT_VELOCITY, + string: 1, + effect: note_effect, + swap_accidentals: false, + kind: NoteType::Rest, + tuplet: None, + } + } +} + +#[derive(Debug, Default, PartialEq)] +pub struct Beat { + pub notes: Vec, + pub duration: Duration, + pub empty: bool, + pub text: String, + pub start: i64, + pub effect: BeatEffects, +} + +#[derive(Debug, Default, PartialEq)] +pub struct Voice { + pub measure_index: i16, + pub beats: Vec, +} + +#[derive(Debug, PartialEq)] +pub struct Measure { + pub key_signature: KeySignature, + pub time_signature: TimeSignature, + pub track_index: usize, + pub header_index: usize, + pub voices: Vec, +} + +impl Default for Measure { + fn default() -> Self { + Measure { + key_signature: KeySignature::new(0, false), + time_signature: TimeSignature::default(), + track_index: 0, + header_index: 0, + voices: vec![], + } + } +} + +#[derive(Debug, PartialEq)] +pub struct Track { + pub number: i32, + pub offset: i32, + pub channel_id: u8, + pub solo: bool, + pub mute: bool, + pub visible: bool, + pub name: String, + pub strings: Vec<(i32, i32)>, + pub color: i32, + pub midi_port: u8, + pub fret_count: u8, + pub measures: Vec, +} + +impl Default for Track { + fn default() -> Self { + Track { + number: 1, + offset: 0, + channel_id: 0, + solo: false, + mute: false, + visible: true, + name: String::new(), + strings: vec![], + color: 0, + midi_port: 0, + fret_count: 24, + measures: vec![], + } + } +} + +pub fn parse_chord(string_count: u8) -> impl FnMut(&[u8]) -> IResult<&[u8], Chord> { + move |i| { + log::debug!("Parsing chords for {} strings", string_count); + let mut i = i; + let mut chord = Chord { + strings: vec![-1; string_count.into()], + ..Default::default() + }; + i = skip(i, 17); + let (inner, chord_name) = parse_byte_size_string(21)(i)?; + i = inner; + chord.name = chord_name; + i = skip(i, 4); + let (inner, first_fret) = parse_int(i)?; + i = inner; + chord.first_fret = Some(first_fret as u32); + for c in 0..7 { + let (inner, fret) = parse_int(i)?; + if c < string_count { + chord.strings[c as usize] = fret as i8; + } + i = inner; + } + i = skip(i, 32); + Ok((i, chord)) + } +} + +pub fn parse_note_effects(note: &mut Note) -> impl FnMut(&[u8]) -> IResult<&[u8], ()> + '_ { + move |i| { + log::debug!("Parsing note effects"); + let mut i = i; + let (inner, (flags1, flags2)) = tuple((parse_byte, parse_byte))(i)?; + i = inner; + note.effect.hammer = (flags1 & 0x02) == 0x02; + note.effect.let_ring = (flags1 & 0x08) == 0x08; + + note.effect.staccato = (flags2 & 0x01) == 0x01; + note.effect.palm_mute = (flags2 & 0x02) == 0x02; + note.effect.vibrato = (flags2 & 0x40) == 0x40 || note.effect.vibrato; + + if (flags1 & 0x01) == 0x01 { + let (inner, bend_effect) = parse_bend_effect(i)?; + i = inner; + note.effect.bend = Some(bend_effect); + } + + if (flags1 & 0x10) == 0x10 { + let (inner, grace_effect) = parse_grace_effect(i)?; + i = inner; + note.effect.grace = Some(grace_effect); + } + + if (flags2 & 0x04) == 0x04 { + let (inner, tremolo_picking) = parse_tremolo_picking(i)?; + i = inner; + note.effect.tremolo_picking = Some(tremolo_picking); + } + + if (flags2 & 0x08) == 0x08 { + let (inner, slide_type) = parse_slide_type(i)?; + i = inner; + note.effect.slide = slide_type; + } + + if (flags2 & 0x10) == 0x10 { + let (inner, harmonic_effect) = parse_harmonic_effect(i)?; + i = inner; + note.effect.harmonic = Some(harmonic_effect); + } + + if (flags2 & 0x20) == 0x20 { + let (inner, trill_effect) = parse_trill_effect(i)?; + i = inner; + note.effect.trill = Some(trill_effect); + } + + Ok((i, ())) + } +} + +pub fn parse_trill_effect(i: &[u8]) -> IResult<&[u8], TrillEffect> { + log::debug!("Parsing trill effect"); + let mut i = i; + let mut trill_effect = TrillEffect::default(); + let (inner, (fret, period)) = tuple((parse_signed_byte, parse_signed_byte))(i)?; + i = inner; + trill_effect.fret = fret; + trill_effect.duration.value = TrillEffect::from_trill_period(period); + Ok((i, trill_effect)) +} + +pub fn parse_harmonic_effect(i: &[u8]) -> IResult<&[u8], HarmonicEffect> { + log::debug!("Parsing harmonic effect"); + let mut i = i; + let mut he = HarmonicEffect::default(); + let (inner, harmonic_type) = parse_signed_byte(i)?; + i = inner; + + match harmonic_type { + 1 => he.kind = HarmonicType::Natural, + 2 => { + he.kind = HarmonicType::Artificial; + let (inner, (semitone, accidental, octave)) = + tuple((parse_byte, parse_signed_byte, parse_byte))(i)?; + i = inner; + he.pitch = Some(PitchClass::from(semitone as i8, Some(accidental), None)); + he.octave = Some(Octave::get_octave(octave)); + } + 3 => { + he.kind = HarmonicType::Tapped; + let (inner, fret) = parse_byte(i)?; + i = inner; + he.right_hand_fret = Some(fret as i8); + } + 4 => he.kind = HarmonicType::Pinch, + 5 => he.kind = HarmonicType::Semi, + _ => panic!("Cannot read harmonic type"), + }; + + Ok((i, he)) +} + +pub fn parse_slide_type(i: &[u8]) -> IResult<&[u8], Option> { + log::debug!("Parsing slide type"); + map(parse_byte, |t| { + if (t & 0x01) == 0x01 { + Some(SlideType::ShiftSlideTo) + } else if (t & 0x02) == 0x02 { + Some(SlideType::LegatoSlideTo) + } else if (t & 0x04) == 0x04 { + Some(SlideType::OutDownwards) + } else if (t & 0x08) == 0x08 { + Some(SlideType::OutUpWards) + } else if (t & 0x10) == 0x10 { + Some(SlideType::IntoFromBelow) + } else if (t & 0x20) == 0x20 { + Some(SlideType::IntoFromAbove) + } else { + None + } + })(i) +} + +pub fn parse_tremolo_picking(i: &[u8]) -> IResult<&[u8], TremoloPickingEffect> { + log::debug!("Parsing tremolo picking"); + map(parse_byte, |tp| { + let value = TremoloPickingEffect::from_tremolo_value(tp as i8); + let mut tremolo_picking_effect = TremoloPickingEffect::default(); + tremolo_picking_effect.duration.value = value; + tremolo_picking_effect + })(i) +} + +pub fn parse_grace_effect(i: &[u8]) -> IResult<&[u8], GraceEffect> { + log::debug!("Parsing grace effect"); + let mut i = i; + let mut grace_effect = GraceEffect::default(); + // fret + let (inner, fret) = parse_byte(i)?; + i = inner; + grace_effect.fret = fret as i8; + + // velocity + let (inner, velocity) = parse_byte(i)?; + i = inner; + grace_effect.velocity = convert_velocity(velocity as i16); + + // transition + let (inner, transition) = parse_signed_byte(i)?; + i = inner; + grace_effect.transition = GraceEffectTransition::get_grace_effect_transition(transition); + + // duration + let (inner, duration) = parse_byte(i)?; + i = inner; + grace_effect.duration = duration; + + let (inner, flags) = parse_byte(i)?; + i = inner; + grace_effect.is_dead = (flags & 0x01) == 0x01; + grace_effect.is_on_beat = (flags & 0x02) == 0x02; + + Ok((i, grace_effect)) +} + +pub fn parse_beat_effects<'a>( + beat: &'a mut Beat, + note_effect: &'a mut NoteEffect, +) -> impl FnMut(&[u8]) -> IResult<&[u8], ()> + 'a { + move |i| { + log::debug!("Parsing beat effects"); + let mut i = i; + let (inner, (flags1, flags2)) = tuple((parse_byte, parse_byte))(i)?; + i = inner; + + note_effect.fade_in = flags1 & 0x10 != 0; + note_effect.vibrato = flags1 & 0x02 != 0; + + if flags1 & 0x20 != 0 { + let (inner, effect) = parse_byte(i)?; + i = inner; + note_effect.slap = match effect { + 1 => SlapEffect::Slapping, + 2 => SlapEffect::Popping, + 3 => SlapEffect::Tapping, + _ => SlapEffect::None, + }; + } + + if flags2 & 0x04 != 0 { + let (inner, effect) = parse_tremolo_bar(i)?; + i = inner; + note_effect.tremolo_bar = Some(effect); + } + + if flags1 & 0x40 != 0 { + let (inner, (stroke_up, stroke_down)) = + tuple((parse_signed_byte, parse_signed_byte))(i)?; + i = inner; + if stroke_up > 0 { + beat.effect.stroke.value = stroke_up as u16; + beat.effect.stroke.direction = BeatStrokeDirection::Up; + } + if stroke_down > 0 { + beat.effect.stroke.value = stroke_down as u16; + beat.effect.stroke.direction = BeatStrokeDirection::Down; + } + } + + if flags2 & 0x02 != 0 { + i = skip(i, 1); + } + + Ok((i, ())) + } +} + +pub fn parse_bend_effect(i: &[u8]) -> IResult<&[u8], BendEffect> { + log::debug!("Parsing bend effect"); + let mut i = skip(i, 5); + let mut bend_effect = BendEffect::default(); + let (inner, num_points) = parse_int(i)?; + i = inner; + for _ in 0..num_points { + let (inner, (bend_position, bend_value, _vibrato)) = + tuple((parse_int, parse_int, parse_byte))(i)?; + i = inner; + + let point_position = + bend_position * BEND_EFFECT_MAX_POSITION as i32 / GP_BEND_POSITION as i32; + let point_value = bend_value as f32 * SEMITONE_LENGTH / GP_BEND_SEMITONE; + bend_effect.points.push(BendPoint { + position: point_position as u8, + value: point_value as i8, + }); + } + Ok((i, bend_effect)) +} + +pub fn parse_tremolo_bar(i: &[u8]) -> IResult<&[u8], TremoloBarEffect> { + log::debug!("Parsing tremolo bar"); + let mut i = skip(i, 5); + let mut tremolo_bar_effect = TremoloBarEffect::default(); + let (inner, num_points) = parse_int(i)?; + i = inner; + for _ in 0..num_points { + let (inner, (position, value, _vibrato)) = tuple((parse_int, parse_int, parse_byte))(i)?; + i = inner; + + let point_position = position * BEND_EFFECT_MAX_POSITION as i32 / GP_BEND_POSITION as i32; + let point_value = value as f32 / GP_BEND_SEMITONE * 2.0f32; + tremolo_bar_effect.points.push(BendPoint { + position: point_position as u8, + value: point_value as i8, + }); + } + Ok((i, tremolo_bar_effect)) +} + +/// Read beat duration. +/// Duration is composed of byte signifying duration and an integer that maps to `Tuplet`. The byte maps to following values: +/// +/// * *-2*: whole note +/// * *-1*: half note +/// * *0*: quarter note +/// * *1*: eighth note +/// * *2*: sixteenth note +/// * *3*: thirty-second note +/// +/// If flag at *0x20* is true, the tuplet is read +pub fn parse_duration(flags: u8) -> impl FnMut(&[u8]) -> IResult<&[u8], Duration> { + move |i: &[u8]| { + log::debug!("Parsing duration"); + let mut i = i; + let mut d = Duration::default(); + let (inner, value) = parse_signed_byte(i)?; + i = inner; + log::debug!("Duration value: {}", value); + d.value = (2_u32.pow((value + 4) as u32) / 4) as u16; + d.dotted = flags & 0x01 != 0; + + if (flags & 0x20) == 0x20 { + let (inner, i_tuplet) = parse_int(i)?; + i = inner; + + match i_tuplet { + 3 => { + d.tuplet_enters = i_tuplet as u8; + d.tuplet_times = 2; + } + 5..=7 => { + d.tuplet_enters = i_tuplet as u8; + d.tuplet_times = 4; + } + 9..=13 => { + d.tuplet_enters = i_tuplet as u8; + d.tuplet_times = 8; + } + x => panic!("Unknown tuplet: {}", x), + } + } + + Ok((i, d)) + } +} + +pub fn parse_color(i: &[u8]) -> IResult<&[u8], i32> { + log::debug!("Parsing RGB color"); + map( + tuple((parse_byte, parse_byte, parse_byte, parse_byte)), + |(r, g, b, _ignore)| (r as i32) << 16 | (g as i32) << 8 | b as i32, + )(i) +} + +pub fn parse_marker(i: &[u8]) -> IResult<&[u8], Marker> { + log::debug!("Parsing marker"); + map( + tuple((parse_int_sized_string, parse_color)), + |(title, color)| Marker { title, color }, + )(i) +} + +pub fn parse_triplet_feel(i: &[u8]) -> IResult<&[u8], TripletFeel> { + log::debug!("Parsing triplet feel"); + map(parse_signed_byte, |triplet_feel| match triplet_feel { + 0 => TripletFeel::None, + 1 => TripletFeel::Eighth, + 2 => TripletFeel::Sixteenth, + x => panic!("Unknown triplet feel: {}", x), + })(i) +} + +/// Parse measure header. +/// the time signature is propagated to the next measure +pub fn parse_measure_header( + previous_time_signature: TimeSignature, + song_tempo: i32, +) -> impl FnMut(&[u8]) -> IResult<&[u8], MeasureHeader> { + move |i: &[u8]| { + log::debug!("Parsing measure header"); + let (mut i, flags) = parse_byte(i)?; + log::debug!("Flags: {:08b}", flags); + let mut mh = MeasureHeader::default(); + mh.tempo.value = song_tempo; // value updated later when parsing beats + mh.repeat_open = (flags & 0x04) == 0x04; + // propagate time signature + mh.time_signature = previous_time_signature.clone(); + + // Numerator of the (key) signature + if (flags & 0x01) != 0 { + log::debug!("Parsing numerator"); + let (inner, numerator) = parse_signed_byte(i)?; + i = inner; + mh.time_signature.numerator = numerator; + } + + // Denominator of the (key) signature + if (flags & 0x02) != 0 { + log::debug!("Parsing denominator"); + let (inner, denominator_value) = parse_signed_byte(i)?; + i = inner; + let denominator = Duration { + value: denominator_value as u16, + ..Default::default() + }; + mh.time_signature.denominator = denominator; + } + + // Beginning of repeat + if (flags & 0x08) != 0 { + log::debug!("Parsing repeat close"); + let (inner, repeat_close) = parse_signed_byte(i)?; + i = inner; + mh.repeat_close = repeat_close; + } + + // Presence of a marker + if (flags & 0x20) != 0 { + let (inner, marker) = parse_marker(i)?; + i = inner; + mh.marker = Some(marker); + } + + // Number of alternate ending + if (flags & 0x10) != 0 { + log::debug!("Parsing repeat alternative"); + let (inner, alternative) = parse_byte(i)?; + i = inner; + mh.repeat_alternative = alternative; + } + + // Tonality of the measure + if (flags & 0x40) != 0 { + log::debug!("Parsing key signature"); + let (inner, key_signature) = parse_signed_byte(i)?; + mh.key_signature.key = key_signature; + i = inner; + let (inner, is_minor) = parse_signed_byte(i)?; + i = inner; + mh.key_signature.is_minor = is_minor != 0; + } + + if (flags & 0x01) != 0 || (flags & 0x02) != 0 { + log::debug!("Skip 4"); + i = skip(i, 4); + } + + if (flags & 0x10) == 0 { + log::debug!("Skip one"); + i = skip(i, 1); + } + + let (inner, triplet_feel) = parse_triplet_feel(i)?; + mh.triplet_feel = triplet_feel; + log::debug!("{:?}", mh); + + Ok((inner, mh)) + } +} + +pub fn parse_measure_headers( + measure_count: i32, + song_tempo: i32, +) -> impl FnMut(&[u8]) -> IResult<&[u8], Vec> { + move |i: &[u8]| { + log::debug!("Parsing {} measure headers", measure_count); + // parse first header to account for the byte in between each header + let (mut i, first_header) = parse_measure_header(TimeSignature::default(), song_tempo)(i)?; + let mut previous_time_signature = first_header.time_signature.clone(); + let mut headers = vec![first_header]; + for _ in 1..measure_count { + let (rest, header) = preceded( + parse_byte, + parse_measure_header(previous_time_signature, song_tempo), + )(i)?; + // propagate time signature + previous_time_signature = header.time_signature.clone(); + i = rest; + headers.push(header); + } + Ok((i, headers)) + } +} + +pub fn parse_midi_channels(i: &[u8]) -> IResult<&[u8], Vec> { + log::debug!("Parsing midi channels"); + let mut channels = Vec::with_capacity(64); + let mut i = i; + for channel_index in 0..64 { + let (inner, channel) = parse_midi_channel(channel_index)(i)?; + i = inner; + channels.push(channel); + } + Ok((i, channels)) +} + +pub fn parse_midi_channel(channel_id: i32) -> impl FnMut(&[u8]) -> IResult<&[u8], MidiChannel> { + move |i: &[u8]| { + map( + tuple(( + parse_int, + parse_signed_byte, + parse_signed_byte, + parse_signed_byte, + parse_signed_byte, + parse_signed_byte, + parse_signed_byte, + parse_byte, + parse_byte, + )), + |( + mut instrument, + volume, + balance, + chorus, + reverb, + phaser, + tremolo, + _blank, + _blank2, + )| { + let bank = if channel_id == 9 { + DEFAULT_PERCUSSION_BANK + } else { + DEFAULT_BANK + }; + if instrument < 0 { + instrument = 0; + } + MidiChannel { + channel_id: channel_id as u8, + effect_channel_id: 0, // filled at the track level + instrument, + volume, + balance, + chorus, + reverb, + phaser, + tremolo, + bank, + } + }, + )(i) + } +} + +pub fn parse_page_setup(i: &[u8]) -> IResult<&[u8], PageSetup> { + log::debug!("Parsing page setup"); + map( + tuple(( + parse_point, + parse_padding, + parse_int, + parse_short, + parse_int_sized_string, + parse_int_sized_string, + parse_int_sized_string, + parse_int_sized_string, + parse_int_sized_string, + parse_int_sized_string, + parse_int_sized_string, + parse_int_sized_string, + parse_int_sized_string, + parse_int_sized_string, + )), + |( + page_size, + page_margin, + score_size_proportion, + header_and_footer, + title, + subtitle, + artist, + album, + words, + music, + word_and_music, + copyright_1, + copyright_2, + page_number, + )| PageSetup { + page_size, + page_margin, + score_size_proportion: score_size_proportion as f32 / 100.0, + header_and_footer, + title, + subtitle, + artist, + album, + words, + music, + word_and_music, + copyright: format!("{}\n{}", copyright_1, copyright_2), + page_number, + }, + )(i) +} + +pub fn parse_point(i: &[u8]) -> IResult<&[u8], Point> { + log::debug!("Parsing point"); + map(tuple((parse_int, parse_int)), |(x, y)| Point { x, y })(i) +} + +pub fn parse_padding(i: &[u8]) -> IResult<&[u8], Padding> { + log::debug!("Parsing padding"); + map( + tuple((parse_int, parse_int, parse_int, parse_int)), + |(right, top, left, bottom)| Padding { + right, + top, + left, + bottom, + }, + )(i) +} + +pub fn parse_lyrics(i: &[u8]) -> IResult<&[u8], Lyrics> { + log::debug!("Parsing lyrics"); + map( + tuple(( + parse_int, + count(tuple((parse_int, parse_int_sized_string)), 5), + )), + |(track_choice, lines)| Lyrics { + track_choice, + lines, + }, + )(i) +} + +/// Parse the version string from the file header. +/// +/// 30 character string (not counting the byte announcing the real length of the string) +/// +/// +pub fn parse_gp_version(i: &[u8]) -> IResult<&[u8], GpVersion> { + log::debug!("Parsing GP version"); + parse_byte_size_string(30)(i).map(|(i, version_string)| match version_string.as_str() { + "FICHIER GUITAR PRO v3.00" => (i, GpVersion::GP3), + "FICHIER GUITAR PRO v4.00" => (i, GpVersion::GP4), + "FICHIER GUITAR PRO v4.06" => (i, GpVersion::GP4_06), + "FICHIER GUITAR PRO v5.00" => (i, GpVersion::GP5), + "FICHIER GUITAR PRO v5.10" => (i, GpVersion::GP5_10), + _ => panic!("Unsupported GP version: {}", version_string), + }) +} + +fn parse_notices(i: &[u8]) -> IResult<&[u8], Vec> { + flat_map(parse_int, |notice_count| { + log::debug!("Notice count: {}", notice_count); + count(parse_int_byte_sized_string, notice_count as usize) + })(i) +} + +/// Par information about the piece of music. +/// +fn parse_info(version: GpVersion) -> impl FnMut(&[u8]) -> IResult<&[u8], SongInfo> { + move |i: &[u8]| { + log::debug!("Parsing song info"); + map( + tuple(( + parse_int_byte_sized_string, + parse_int_byte_sized_string, + parse_int_byte_sized_string, + parse_int_byte_sized_string, + parse_int_byte_sized_string, + cond(version >= GpVersion::GP5, parse_int_byte_sized_string), + parse_int_byte_sized_string, + parse_int_byte_sized_string, + parse_int_byte_sized_string, + parse_notices, + )), + |( + name, + subtitle, + artist, + album, + author, + words, + copyright, + writer, + instructions, + notices, + )| { + SongInfo { + name, + subtitle, + artist, + album, + author, + words, + copyright, + writer, + instructions, + notices, + } + }, + )(i) + } +} + +pub fn parse_gp_data(file_data: &[u8]) -> Result { + let (rest, base_song) = flat_map(parse_gp_version, |version| { + map( + tuple(( + parse_info(version), // Song info + cond(version < GpVersion::GP5, parse_bool), // Triplet feel + cond(version >= GpVersion::GP4, parse_lyrics), // Lyrics + cond(version >= GpVersion::GP5_10, take(19usize)), // Skip RSE master effect + cond(version >= GpVersion::GP5, parse_page_setup), // Page setup + cond(version >= GpVersion::GP5, parse_int_sized_string), // Tempo name + parse_int, // Tempo + cond(version > GpVersion::GP5, parse_bool), // Tempo hide + parse_signed_byte, // Key signature + cond(version > GpVersion::GP3, parse_int), // Octave + parse_midi_channels, // Midi channels + )), + move |( + song_info, + triplet_feel, + lyrics, + _master_effect, + page_setup, + tempo_name, + tempo, + hide_tempo, + key_signature, + octave, + midi_channels, + )| { + // init base song + let tempo = Tempo::new(tempo, tempo_name); + Song { + version, + song_info, + triplet_feel, + lyrics, + page_setup, + tempo, + hide_tempo, + key_signature, + octave, + midi_channels, + measure_headers: vec![], + tracks: vec![], + } + }, + ) + })(file_data) + .map_err(|_err| { + log::error!("Failed to parse GP data"); + RuxError::ParsingError("Failed to parse GP data".to_string()) + })?; + + // make parser and parse music data + let mut parser = MusicParser::new(base_song); + let (_rest, _unit) = parser.parse_music_data(rest).map_err(|e| { + log::error!("Failed to parse music data: {:?}", e); + RuxError::ParsingError("Failed to parse music data".to_string()) + })?; + let song = parser.take_song(); + Ok(song) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_gp_ordering() { + assert!(GpVersion::GP4 < GpVersion::GP5); + assert!(GpVersion::GP5 >= GpVersion::GP5); + assert!(GpVersion::GP3 < GpVersion::GP4); + assert!(GpVersion::GP3 < GpVersion::GP5); + } +} diff --git a/src/parser/song_parser_tests.rs b/src/parser/song_parser_tests.rs new file mode 100644 index 0000000..62901ee --- /dev/null +++ b/src/parser/song_parser_tests.rs @@ -0,0 +1,417 @@ +#[cfg(test)] +use crate::parser::song_parser::{parse_gp_data, Song}; +#[cfg(test)] +use crate::RuxError; +#[cfg(test)] +use std::io::Read; + +#[cfg(test)] +pub fn parse_gp_file(file_path: &str) -> Result { + let mut file = std::fs::File::open(file_path)?; + let mut file_data: Vec = vec![]; + file.read_to_end(&mut file_data)?; + parse_gp_data(&file_data) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parser::song_parser::{ + Duration, GpVersion, KeySignature, Marker, Padding, Point, TripletFeel, + }; + + #[test] + fn parse_all_gp5_files_successfully() { + env_logger::builder() + .is_test(true) + .try_init() + .unwrap_or_default(); + let test_dir = std::path::Path::new("test-files"); + for entry in std::fs::read_dir(test_dir).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.extension().unwrap() != "gp5" { + continue; + } + let file_name = path.file_name().unwrap().to_str().unwrap(); + eprintln!("Parsing file: {}", file_name); + let file_path = path.to_str().unwrap(); + let song = parse_gp_file(file_path) + .unwrap_or_else(|err| panic!("Failed to parse file: {}\n{}", file_name, err)); + // assert global invariant across all measures + for (t_id, t) in song.tracks.iter().enumerate() { + assert_eq!( + t.measures.len(), + song.measure_headers.len(), + "Track:{} File:{}", + t_id, + file_name + ); + for (m_id, m) in t.measures.iter().enumerate() { + assert_eq!( + m.track_index, t_id, + "Track:{} Measure:{} File:{}", + t_id, m_id, file_name + ); + assert_eq!( + m.header_index, m_id, + "Track:{} Measure:{} File:{}", + t_id, m_id, file_name + ); + assert_eq!( + m.voices.len(), + 2, + "Track:{} Measure:{} File:{}", + t_id, + m_id, + file_name + ); + let measure_header = &song.measure_headers[m_id]; + let measure_start = measure_header.start; + for v in &m.voices { + v.beats.iter().enumerate().for_each(|(i, b)| { + assert!( + b.start >= measure_start, + "track:{} measure:{} beat:{} file:{}", + t_id, + m_id, + i, + file_name + ); + }) + } + } + } + } + } + + #[test] + fn parse_gp5_00_demo() { + // test file from https://github.com/slundi/guitarpro/tree/master/test + const FILE_PATH: &str = "test-files/Demo v5.gp5"; + let song = parse_gp_file(FILE_PATH).unwrap(); + assert_eq!(song.version, GpVersion::GP5); + assert_eq!(song.tempo.value, 165); + assert_eq!(song.tracks.len(), 5); + assert_eq!(song.tracks[0].name, "Rhythm Guitar"); + assert_eq!(song.tracks[0].number, 1); + assert_eq!(song.tracks[0].offset, 0); + assert_eq!(song.tracks[0].channel_id, 0); + + assert_eq!(song.tracks[1].name, "Solo Guitar"); + assert_eq!(song.tracks[1].number, 2); + assert_eq!(song.tracks[1].offset, 0); + assert_eq!(song.tracks[1].channel_id, 2); + + assert_eq!(song.tracks[2].name, "Melody"); + assert_eq!(song.tracks[2].number, 3); + assert_eq!(song.tracks[2].offset, 0); + assert_eq!(song.tracks[2].channel_id, 6); + + assert_eq!(song.tracks[3].name, "Bass"); + assert_eq!(song.tracks[3].number, 4); + assert_eq!(song.tracks[3].offset, 0); + assert_eq!(song.tracks[3].channel_id, 4); + + assert_eq!(song.tracks[4].name, "Percussions"); + assert_eq!(song.tracks[4].number, 5); + assert_eq!(song.tracks[4].offset, 0); + assert_eq!(song.tracks[4].channel_id, 9); + + // inspect headers + assert_eq!(song.measure_headers.len(), 49); + assert_eq!(song.tracks[0].measures.len(), 49); + + let header = &song.measure_headers[0]; + assert_eq!(header.start, 960); + assert_eq!(header.tempo.value, 165); + assert_eq!(header.time_signature.numerator, 4); + assert_eq!( + header.time_signature.denominator, + Duration { + value: 4, + dotted: false, + double_dotted: false, + tuplet_enters: 1, + tuplet_times: 1, + } + ); + assert_eq!(header.time_signature.denominator.time(), 960); + // In a 4/4 time signature, the total measure duration is the equivalent of 4 quarter notes. + // In this case, the duration of a quarter note is 960 ticks. + // Therefore, the total measure duration is 3840 ticks. + // 4*960 = 3840 + assert_eq!(header.length(), 3840); + assert_eq!( + header.marker, + Some(Marker { + title: "\u{5}Intro".to_string(), + color: 16_711_680 + }) + ); + assert!(header.repeat_open); + assert_eq!(header.repeat_close, 0); + assert_eq!(header.triplet_feel, TripletFeel::None); + + let header = &song.measure_headers[1]; + // 3840 + 960 (offset of previous measure) = 4800 + assert_eq!(header.start, 4800); + assert_eq!(header.tempo.value, 165); + assert_eq!(header.time_signature.numerator, 4); + assert_eq!(header.length(), 3840); + assert_eq!(header.marker, None); + assert!(!header.repeat_open); + assert_eq!(header.repeat_close, 0); + assert_eq!(header.triplet_feel, TripletFeel::None); + + let header = &song.measure_headers[2]; + assert_eq!(header.start, 8640); + assert_eq!(header.tempo.value, 165); + assert_eq!(header.time_signature.numerator, 4); + assert_eq!(header.length(), 3840); + assert_eq!(header.marker, None); + assert!(!header.repeat_open); + assert_eq!(header.repeat_close, 0); + assert_eq!(header.triplet_feel, TripletFeel::None); + + let header = &song.measure_headers[3]; + assert_eq!(header.start, 12480); + assert_eq!(header.tempo.value, 165); + assert_eq!(header.time_signature.numerator, 4); + assert_eq!(header.length(), 3840); + assert_eq!(header.marker, None); + assert!(!header.repeat_open); + assert_eq!(header.repeat_close, 2); + assert_eq!(header.triplet_feel, TripletFeel::None); + + // first measure + let measure = &song.tracks[0].measures[0]; + assert_eq!(measure.track_index, 0); + assert_eq!(measure.voices.len(), 2); + + assert_eq!(measure.voices[1].beats.len(), 1); + assert_eq!(measure.voices[1].beats[0].notes.len(), 0); + assert_eq!(measure.voices[1].beats[0].start, 960); + assert!(measure.voices[1].beats[0].empty); + + assert_eq!(measure.voices[0].beats.len(), 8); + + assert_eq!(measure.voices[0].beats[0].start, 960); + // if there are 8 beats per measure, then each beat is an eighth note long (quarter note / 2 == 960/2). + assert_eq!(measure.voices[0].beats[0].duration.time(), 480); + assert!(!measure.voices[0].beats[0].empty); + assert_eq!(measure.voices[0].beats[0].notes.len(), 3); // C5 chord + + assert_eq!(measure.voices[0].beats[1].start, 1440); + assert_eq!(measure.voices[0].beats[1].duration.time(), 480); + assert!(!measure.voices[0].beats[1].empty); + assert_eq!(measure.voices[0].beats[1].notes.len(), 1); // E2 single + + assert_eq!(measure.voices[0].beats[2].start, 1920); + assert_eq!(measure.voices[0].beats[2].duration.time(), 480); + assert!(!measure.voices[0].beats[2].empty); + assert_eq!(measure.voices[0].beats[2].notes.len(), 1); // E2 single + + assert_eq!(measure.voices[0].beats[3].start, 2400); + assert_eq!(measure.voices[0].beats[3].duration.time(), 480); + assert!(!measure.voices[0].beats[3].empty); + assert_eq!(measure.voices[0].beats[3].notes.len(), 3); // C5 chord + + assert_eq!(measure.voices[0].beats[4].start, 2880); + assert_eq!(measure.voices[0].beats[4].duration.time(), 480); + assert!(!measure.voices[0].beats[4].empty); + assert_eq!(measure.voices[0].beats[4].notes.len(), 1); // E2 single + + assert_eq!(measure.voices[0].beats[5].start, 3360); + assert_eq!(measure.voices[0].beats[5].duration.time(), 480); + assert!(!measure.voices[0].beats[5].empty); + assert_eq!(measure.voices[0].beats[5].notes.len(), 1); // E2 single + + assert_eq!(measure.voices[0].beats[6].start, 3840); + assert_eq!(measure.voices[0].beats[6].duration.time(), 480); + assert!(!measure.voices[0].beats[6].empty); + assert_eq!(measure.voices[0].beats[6].notes.len(), 3); // C5 chord + + assert_eq!(measure.voices[0].beats[7].start, 4320); + assert_eq!(measure.voices[0].beats[7].duration.time(), 480); + assert!(!measure.voices[0].beats[7].empty); + assert_eq!(measure.voices[0].beats[7].notes.len(), 1); // E2 single + + // second measure + let measure = &song.tracks[0].measures[1]; + assert_eq!(measure.track_index, 0); + assert_eq!(measure.voices.len(), 2); + + assert_eq!(measure.voices[1].beats.len(), 1); + assert_eq!(measure.voices[1].beats[0].notes.len(), 0); + assert_eq!(measure.voices[1].beats[0].start, 4800); + assert!(measure.voices[1].beats[0].empty); + + assert_eq!(measure.voices[0].beats.len(), 8); + + assert_eq!(measure.voices[0].beats[0].start, 4800); + assert_eq!(measure.voices[0].beats[0].duration.time(), 480); + assert!(!measure.voices[0].beats[0].empty); + assert_eq!(measure.voices[0].beats[0].notes.len(), 3); // C5 chord + + assert_eq!(measure.voices[0].beats[1].start, 5280); + assert_eq!(measure.voices[0].beats[1].duration.time(), 480); + assert!(!measure.voices[0].beats[1].empty); + assert_eq!(measure.voices[0].beats[1].notes.len(), 1); // E2 single + + // inspect midi channels + assert_eq!(song.midi_channels.len(), 64); + assert_eq!(song.midi_channels[0].channel_id, 0); + assert_eq!(song.midi_channels[0].effect_channel_id, 1); + assert_eq!(song.midi_channels[0].instrument, 29); + + assert_eq!(song.midi_channels[1].channel_id, 1); + assert_eq!(song.midi_channels[1].effect_channel_id, 0); + assert_eq!(song.midi_channels[1].instrument, 29); + + assert_eq!(song.midi_channels[2].channel_id, 2); + assert_eq!(song.midi_channels[2].effect_channel_id, 3); + assert_eq!(song.midi_channels[2].instrument, 30); + + assert_eq!(song.midi_channels[3].channel_id, 3); + assert_eq!(song.midi_channels[3].effect_channel_id, 0); + assert_eq!(song.midi_channels[3].instrument, 30); + } + + #[test] + fn parse_gp5_10_ghost() { + const FILE_PATH: &str = "test-files/Ghost - Cirice.gp5"; + let song = parse_gp_file(FILE_PATH).unwrap(); + assert_eq!(song.version, GpVersion::GP5_10); + assert_eq!(song.song_info.name, "Cirice"); + assert_eq!(song.song_info.subtitle, ""); + assert_eq!(song.song_info.artist, "Ghost"); + assert_eq!(song.song_info.album, "Meliora"); + assert_eq!(song.song_info.author, "A Ghoul Writer"); + assert_eq!(song.song_info.words, Some("A Ghoul Writer".to_string())); + assert_eq!(song.song_info.copyright, ""); + assert_eq!(song.song_info.writer, "TheManPF"); + assert_eq!(song.song_info.instructions, ""); + assert!(song.song_info.notices.is_empty()); + assert_eq!(song.triplet_feel, None); + assert!(song.lyrics.is_some()); + let lyrics = song.lyrics.unwrap(); + assert_eq!(lyrics.track_choice, 0); + assert_eq!(lyrics.lines.len(), 5); + assert_eq!(lyrics.lines[0].1, "I feel your presence amongst us\r\nYou cannot hide in the darkness\r\nCan you hear the rumble?\r\nCan you hear the rumble that's calling?\r\n\r\nI know your soul is not tainted\r\nEven though you've been told so\r\nCan you hear the rumble?\r\nCan you hear the rumble that's calling?\r\n\r\nI can feel the thunder that's breaking in your heart\r\nI can see through the scars inside you\r\nI can feel the thunder that's breaking in your heart\r\nI can see through the scars inside you\r\n\r\nA candle casting a faint glow\r\nYou and I see eye to eye\r\nCan you hear the thunder?\r\nOh can you hear the thunder that's breaking?\r\n\r\nNow there is nothing between us\r\nFor now our merge is eternal\r\nCan't you see that you're lost?\r\nCan't you see that you're lost without me?\r\n\r\nI can feel the thunder that's breaking in your heart\r\nI can see through the scars inside you\r\nI can feel the thunder that's breaking in your heart\r\nI can see through the scars inside you\r\n\r\nCan't you see that you're lost without me?\r\n\r\nI can feel the thunder that's breaking in your heart\r\nI can see through the scars inside you\r\nI can feel the thunder that's breaking in your heart\r\nI can see through the scars inside you\r\n\r\nI can feel the thunder that's breaking in your heart\r\nI can see through the scars inside you\r\nI can feel the thunder that's breaking in your heart\r\nI can see through the scars inside you"); + assert!(song.page_setup.is_some()); + let page_setup = song.page_setup.unwrap(); + assert_eq!(page_setup.page_size, Point { x: 216, y: 279 }); + assert_eq!( + page_setup.page_margin, + Padding { + right: 10, + top: 10, + left: 15, + bottom: 10 + } + ); + + assert_eq!(page_setup.score_size_proportion, 1.0); + assert_eq!(page_setup.header_and_footer, 511); + assert_eq!(page_setup.title, "\u{7}%TITLE%"); + assert_eq!(page_setup.subtitle, "\n%SUBTITLE%"); + assert_eq!(page_setup.artist, "\u{8}%ARTIST%"); + assert_eq!(page_setup.album, "\u{7}%ALBUM%"); + assert_eq!(page_setup.words, "\u{10}Words by %WORDS%"); + assert_eq!(page_setup.music, "\u{10}Music by %MUSIC%"); + assert_eq!( + page_setup.word_and_music, + "\u{1d}Words & Music by %WORDSMUSIC%" + ); + assert_eq!( + page_setup.copyright, + "\u{15}Copyright %COPYRIGHT%\n5All Rights Reserved - International Copyright Secured" + ); + assert_eq!(page_setup.page_number, "\u{c}Page %N%/%P%"); + + assert_eq!(song.tempo.name, Some("\u{8}Moderate".to_string())); + assert_eq!(song.tempo.value, 90); + assert_eq!(song.hide_tempo, Some(false)); + assert_eq!(song.key_signature, 0); + assert_eq!(song.octave, Some(0)); + + assert_eq!(song.midi_channels.len(), 64); + assert_eq!(song.midi_channels[0].effect_channel_id, 0); + assert_eq!(song.midi_channels[0].instrument, 25); + assert_eq!(song.midi_channels[0].volume, 16); + assert_eq!(song.midi_channels[0].balance, 0); + assert_eq!(song.midi_channels[0].chorus, 0); + assert_eq!(song.midi_channels[0].reverb, 0); + assert_eq!(song.midi_channels[0].phaser, 0); + assert_eq!(song.midi_channels[0].tremolo, 0); + assert_eq!(song.midi_channels[0].bank, 0); + + assert_eq!(song.tracks.len(), 14); + assert_eq!(song.tracks[0].name, "Vocals"); + assert_eq!(song.tracks[1].name, "Acoustic Guitar L"); + assert_eq!(song.tracks[2].name, "Acoustic Guitar R"); + assert_eq!(song.tracks[3].name, "Rythm Guitar L"); + assert_eq!(song.tracks[4].name, "Rythm Guitar R"); + assert_eq!(song.tracks[5].name, "Lead Guitar"); + assert_eq!(song.tracks[6].name, "Bass"); + assert_eq!(song.tracks[7].name, "Piano"); + assert_eq!(song.tracks[8].name, "Organ"); + assert_eq!(song.tracks[9].name, "Synth"); + assert_eq!(song.tracks[10].name, "Strings"); + assert_eq!(song.tracks[11].name, "Drums"); + assert_eq!(song.tracks[12].name, "Timpani"); + assert_eq!(song.tracks[13].name, "Reverse"); + + for t in &song.tracks { + assert_eq!(t.measures.len(), song.measure_headers.len()); + } + + let guitar_track = &song.tracks[2]; + assert_eq!(guitar_track.name, "Acoustic Guitar R"); + assert_eq!(guitar_track.strings.len(), 6); + assert_eq!(guitar_track.strings[0].1, 62); + assert_eq!(guitar_track.strings[1].1, 57); + assert_eq!(guitar_track.strings[2].1, 53); + assert_eq!(guitar_track.strings[3].1, 48); + assert_eq!(guitar_track.strings[4].1, 43); + assert_eq!(guitar_track.strings[5].1, 38); + + assert_eq!(guitar_track.measures.len(), 124); + let measure = &guitar_track.measures[0]; + assert_eq!(measure.time_signature.numerator, 4); + assert_eq!( + measure.time_signature.denominator, + Duration { + value: 4, + dotted: false, + double_dotted: false, + tuplet_enters: 1, + tuplet_times: 1 + } + ); + assert_eq!(measure.key_signature, KeySignature::new(0, false)); + + assert_eq!(measure.voices.len(), 2); + for v in &measure.voices { + assert_eq!(v.beats.len(), 1); + assert_eq!(v.measure_index, 0); + assert!(v.beats[0].notes.is_empty()); + assert!(v.beats[0].empty); + } + let beat = &measure.voices[1].beats[0]; + assert_eq!(beat.start, 960); + assert_eq!(beat.text, ""); + assert_eq!( + beat.duration, + Duration { + value: 4, + dotted: false, + double_dotted: false, + tuplet_enters: 1, + tuplet_times: 1 + } + ); + assert_eq!(beat.notes.len(), 0); + } +} diff --git a/src/ui/application.rs b/src/ui/application.rs new file mode 100644 index 0000000..5107442 --- /dev/null +++ b/src/ui/application.rs @@ -0,0 +1,374 @@ +use iced::widget::{column, horizontal_space, pick_list, row, text}; +use iced::{keyboard, stream, Alignment, Element, Subscription, Task, Theme}; +use std::borrow::Cow; +use std::fmt::Display; + +use crate::audio::midi_player::AudioPlayer; +use crate::parser::song_parser::{parse_gp_data, GpVersion, Song}; +use crate::ui::icons::{open_icon, pause_icon, play_icon, solo_icon, stop_icon}; +use crate::ui::picker::{open_file, PickerError}; +use crate::ui::tablature::Tablature; +use crate::ui::utils::{action_gated, action_toggle, untitled_text_table_box}; +use crate::ApplicationArgs; +use iced::futures::{SinkExt, Stream}; +use iced::keyboard::key::Named::Space; +use iced::widget::scrollable::{scroll_to, AbsoluteOffset, Id}; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::Arc; +use tokio::sync::watch::{Receiver, Sender}; +use tokio::sync::Mutex; + +const ICONS_FONT: &[u8] = include_bytes!("../../resources/icons.ttf"); + +pub struct RuxApplication { + song_info: Option, // parsed song + track_selection: TrackSelection, // selected track + all_tracks: Vec, // all possible tracks + tablature: Option, // loaded tablature + audio_player: Option, // audio player + tab_file_is_loading: bool, // file loading flag in progress + sound_font_file: Option, // sound font file + beat_sender: Arc>, // beat notifier + beat_receiver: Arc>>, // beat receiver +} + +#[derive(Debug)] +struct SongDisplayInfo { + name: String, + artist: String, + gp_version: GpVersion, + file_name: String, +} + +impl SongDisplayInfo { + fn new(song: &Song, file_name: String) -> Self { + Self { + name: song.song_info.name.clone(), + artist: song.song_info.artist.clone(), + gp_version: song.version, + file_name, + } + } +} + +#[derive(Debug, Default, Clone, PartialEq)] +pub struct TrackSelection { + index: usize, + name: String, +} + +impl TrackSelection { + fn new(index: usize, name: String) -> Self { + Self { index, name } + } +} + +impl Display for TrackSelection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} - {}", self.index + 1, self.name) + } +} + +#[derive(Debug, Clone)] +pub enum Message { + OpenFile, // open file dialog + FileOpened(Result<(Vec, String), PickerError>), // file content & file name + TrackSelected(TrackSelection), // track selection + FocusMeasure(usize), // used when clicking on measure in tablature + FocusTick(usize), // focus on a specific tick in the tablature + PlayPause, // toggle play/pause + StopPlayer, // stop playback + ToggleSolo, // toggle solo mode +} + +impl RuxApplication { + fn new(sound_font_file: Option) -> Self { + let (beat_sender, beat_receiver) = tokio::sync::watch::channel(0); + Self { + song_info: None, + track_selection: TrackSelection::default(), + all_tracks: vec![], + tablature: None, + audio_player: None, + tab_file_is_loading: false, + sound_font_file, + beat_receiver: Arc::new(Mutex::new(beat_receiver)), + beat_sender: Arc::new(beat_sender), + } + } + + pub fn start(args: ApplicationArgs) -> iced::Result { + iced::application( + RuxApplication::title, + RuxApplication::update, + RuxApplication::view, + ) + .subscription(RuxApplication::subscription) + .default_font(iced::Font::MONOSPACE) + .theme(RuxApplication::theme) + .font(ICONS_FONT) + .window_size((1150.0, 768.0)) + .centered() + .antialiasing(true) + .run_with(move || { + ( + RuxApplication::new(args.sound_font_bank.clone()), + Task::none(), + ) + }) + } + + fn title(&self) -> String { + match &self.song_info { + Some(song_info) => format!("Ruxguitar - {}", song_info.file_name), + None => String::from("Ruxguitar - untitled"), + } + } + + fn update(&mut self, message: Message) -> Task { + match message { + Message::TrackSelected(selection) => { + if let Some(tablature) = self.tablature.as_mut() { + tablature.update_track(selection.index); + } + self.track_selection = selection; + Task::none() + } + Message::OpenFile => { + if self.tab_file_is_loading { + Task::none() + } else { + self.tab_file_is_loading = true; + Task::perform(open_file(), Message::FileOpened) + } + } + Message::FileOpened(result) => { + self.tab_file_is_loading = false; + match result { + Ok((contents, file_name)) => { + if let Ok(song) = parse_gp_data(&contents) { + // build all tracks selection + let track_selections: Vec<_> = song + .tracks + .iter() + .enumerate() + .map(|(index, track)| { + TrackSelection::new(index, track.name.clone()) + }) + .collect(); + self.all_tracks.clone_from(&track_selections); + self.song_info = Some(SongDisplayInfo::new(&song, file_name)); + // select first track by default + let default_track = 0; + let default_track_selection = track_selections[default_track].clone(); + self.track_selection = default_track_selection; + // share song ownership with tablature and player + let song_rc = Rc::new(song); + let tablature_scroll_id = + Id::new(Cow::Borrowed("tablature-scroll-elements")); + let tablature = Tablature::new( + song_rc.clone(), + default_track, + tablature_scroll_id.clone(), + ); + self.tablature = Some(tablature); + // stop previous audio player if any + if let Some(audio_player) = &mut self.audio_player { + audio_player.stop(); + } + // audio player initialization + let audio_player = AudioPlayer::new( + song_rc.clone(), + song_rc.tempo.value, + self.sound_font_file.clone(), + self.beat_sender.clone(), + ); + self.audio_player = Some(audio_player); + // reset tablature scroll + scroll_to(tablature_scroll_id, AbsoluteOffset::default()) + } else { + log::warn!("Failed to parse GP file"); + // TODO show alert popup + Task::none() + } + } + Err(err) => { + log::warn!("Failed to read GP file: {}", err); + // TODO show alert popup + Task::none() + } + } + } + Message::FocusMeasure(measure_id) => { + // focus measure in tablature + if let Some(tablature) = &mut self.tablature { + tablature.focus_on_measure(measure_id); + } + // focus measure in player + if let Some(audio_player) = &mut self.audio_player { + audio_player.focus_measure(measure_id); + } + Task::none() + } + Message::FocusTick(tick) => { + if let Some(tablature) = &mut self.tablature { + tablature.focus_on_tick(tick); + } + Task::none() + } + Message::PlayPause => { + if let Some(audio_player) = &mut self.audio_player { + audio_player.toggle_play(); + } + Task::none() + } + Message::StopPlayer => { + if let (Some(audio_player), Some(tablature)) = + (&mut self.audio_player, &mut self.tablature) + { + // stop audio player + audio_player.stop(); + // reset tablature focus + tablature.focus_on_measure(0); + // reset tablature scroll + scroll_to(tablature.scroll_id.clone(), AbsoluteOffset::default()) + } else { + Task::none() + } + } + Message::ToggleSolo => { + if let Some(audio_player) = &mut self.audio_player { + let track = self.track_selection.index; + audio_player.toggle_solo_mode(track); + } + Task::none() + } + } + } + + fn view(&self) -> Element { + let open_file = action_gated( + open_icon(), + "Open file", + (!self.tab_file_is_loading).then_some(Message::OpenFile), + ); + + let player_control = if let Some(audio_player) = &self.audio_player { + let (icon, message) = if audio_player.is_playing() { + (pause_icon(), "Pause") + } else { + (play_icon(), "Play") + }; + let play_button = action_gated(icon, message, Some(Message::PlayPause)); + let stop_button = action_gated(stop_icon(), "Stop", Some(Message::StopPlayer)); + row![play_button, stop_button,] + .spacing(10) + .align_y(Alignment::Center) + } else { + row![horizontal_space()] + }; + + let track_control = if self.all_tracks.is_empty() { + row![horizontal_space()] + } else { + let solo_mode = action_toggle( + solo_icon(), + "Solo", + Message::ToggleSolo, + self.audio_player + .as_ref() + .is_some_and(|p| p.solo_track_id().is_some()), + ); + + let track_pick_list = pick_list( + self.all_tracks.as_slice(), + Some(&self.track_selection), + Message::TrackSelected, + ) + .text_size(14) + .padding([5, 10]); + + row![solo_mode, track_pick_list,] + .spacing(10) + .align_y(Alignment::Center) + }; + + let controls = row![ + open_file, + horizontal_space(), + player_control, + horizontal_space(), + track_control, + ] + .spacing(10) + .align_y(Alignment::Center); + + let status = row![ + text(if let Some(song) = &self.song_info { + format!("{} by {}", song.name, song.artist) + } else { + String::new() + }), + horizontal_space(), + text(if let Some(song) = &self.song_info { + format!("{:?}", song.gp_version) + } else { + String::new() + }), + ] + .spacing(10); + + let tablature_view = self + .tablature + .as_ref() + .map_or(untitled_text_table_box().into(), |t| t.view()); + + column![controls, tablature_view, status,] + .spacing(20) + .padding(10) + .into() + } + + fn theme(&self) -> Theme { + Theme::Dark + } + + fn audio_player_beat_subscription(&self) -> impl Stream { + let beat_receiver = self.beat_receiver.clone(); + stream::channel(1, move |mut output| async move { + let mut receiver = beat_receiver.lock().await; + loop { + // get tick from audio player + let tick = *receiver.borrow_and_update(); + // publish to UI + output + .send(Message::FocusTick(tick)) + .await + .expect("send failed"); + // wait for next beat + receiver.changed().await.expect("receiver failed"); + } + }) + } + + fn subscription(&self) -> Subscription { + let mut subscriptions = Vec::with_capacity(2); + + // keyboard event subscription + let keyboard_subscription = keyboard::on_key_press(|key, _modifiers| match key.as_ref() { + keyboard::Key::Named(Space) => Some(Message::PlayPause), + _ => None, + }); + subscriptions.push(keyboard_subscription); + + // next beat notifier subscription + let audio_player_beat_subscription = self.audio_player_beat_subscription(); + subscriptions.push(Subscription::run_with_id( + "audio-player-beat", + audio_player_beat_subscription, + )); + + Subscription::batch(subscriptions) + } +} diff --git a/src/ui/canvas_measure.rs b/src/ui/canvas_measure.rs new file mode 100644 index 0000000..5550cb0 --- /dev/null +++ b/src/ui/canvas_measure.rs @@ -0,0 +1,538 @@ +use crate::parser::song_parser::{Beat, HarmonicType, Note, NoteEffect, NoteType, SlideType, Song}; +use crate::ui::application::Message; +use iced::advanced::mouse; +use iced::advanced::text::Shaping::Advanced; +use iced::alignment::Horizontal::Center; +use iced::event::Status; +use iced::mouse::{Cursor, Interaction}; +use iced::widget::canvas::{Cache, Event, Frame, Geometry, Path, Stroke, Text}; +use iced::widget::{canvas, Canvas}; +use iced::{Color, Element, Length, Point, Rectangle, Renderer, Theme}; +use std::rc::Rc; + +/// Drawing constants + +/// Measure label + marker +const MEASURE_ANNOTATION_Y: f32 = 3.0; + +/// Chord label level +const CHORD_ANNOTATION_Y: f32 = 15.0; + +/// Note effect level +const NOTE_EFFECT_ANNOTATION_Y: f32 = 27.0; + +/// First string level +const FIRST_STRING_Y: f32 = 50.0; + +/// Distance between strings +const STRING_LINE_HEIGHT: f32 = 13.0; + +/// Measure notes padding +const MEASURE_NOTES_PADDING: f32 = 20.0; + +/// Length of a beat +const BEAT_LENGTH: f32 = 25.0; + +/// minimum measure width +const MIN_MEASURE_WIDTH: f32 = 60.0; + +#[derive(Debug)] +pub struct CanvasMeasure { + pub measure_id: usize, + track_id: usize, + song: Rc, + is_focused: bool, + focused_beat: usize, + canvas_cache: Cache, + measure_len: f32, + total_measure_len: f32, +} + +impl CanvasMeasure { + pub fn new(measure_id: usize, track_id: usize, song: Rc, focused: bool) -> Self { + let track = &song.tracks[track_id]; + let measure = &track.measures[measure_id]; + let beat_count = measure.voices[0].beats.len(); + let measure_len = MIN_MEASURE_WIDTH.max(beat_count as f32 * BEAT_LENGTH); + // total length of measure (padding on both sides) + let total_measure_len = measure_len + MEASURE_NOTES_PADDING * 2.0; + Self { + measure_id, + track_id, + song, + is_focused: focused, + focused_beat: 0, + canvas_cache: Cache::default(), + measure_len, + total_measure_len, + } + } + + pub fn view(&self) -> Element { + let string_count = self.song.tracks[self.track_id].strings.len(); + let vertical_measure_height = STRING_LINE_HEIGHT * (string_count - 1) as f32; + let canvas = Canvas::new(self) + .height(vertical_measure_height + FIRST_STRING_Y * 2.0) + .width(Length::Fixed(self.total_measure_len)); + canvas.into() + } + + pub fn toggle_focused(&mut self) { + // reset focus state + self.is_focused = !self.is_focused; + self.focused_beat = 0; + // clear cache + self.canvas_cache.clear(); + } + + pub fn focus_beat(&mut self, beat_id: usize) { + if self.focused_beat != beat_id { + self.focused_beat = beat_id; + self.canvas_cache.clear(); + } + } +} + +#[derive(Debug, Default)] +pub enum MeasureInteraction { + #[default] + None, + Clicked, +} + +impl canvas::Program for CanvasMeasure { + type State = MeasureInteraction; + + fn update( + &self, + state: &mut Self::State, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> (Status, Option) { + if let Event::Mouse(mouse::Event::ButtonPressed(_)) = event { + if let Some(_cursor_position) = cursor.position_in(bounds) { + log::info!("Clicked on measure {:?}", self.measure_id); + *state = MeasureInteraction::Clicked; + return ( + Status::Captured, + Some(Message::FocusMeasure(self.measure_id)), + ); + }; + } + (Status::Ignored, None) + } + + fn draw( + &self, + _state: &Self::State, + renderer: &Renderer, + _theme: &Theme, + bounds: Rectangle, + _cursor: Cursor, + ) -> Vec { + // the cache will not redraw its geometry unless the dimensions of its layer change, or it is explicitly cleared. + let tab = self.canvas_cache.draw(renderer, bounds.size(), |frame| { + log::debug!("Re-drawing measure {}", self.measure_id); + let track = &self.song.tracks[self.track_id]; + let strings = &track.strings; + let string_count = strings.len(); + + // distance between lines of measures + let vertical_measure_height = STRING_LINE_HEIGHT * (string_count - 1) as f32; + + // Positive x-values extend to the right, and positive y-values extend downwards. + let measure_start_x = 0.0; + let measure_start_y = FIRST_STRING_Y; + + // colors + let color_gray = Color::from_rgb8(0x40, 0x44, 0x4B); + let color_dark_red = Color::from_rgb8(200, 50, 50); + + // draw first measure vertical line + // TODO draw only for the first measure because the end measure will be the start of the next measure + draw_measure_vertical_line( + frame, + vertical_measure_height, + measure_start_x, + measure_start_y, + ); + + // draw focused box + if self.is_focused { + draw_focused_box( + frame, + self.total_measure_len, + vertical_measure_height, + measure_start_x, + measure_start_y, + ); + } + + let measure_header = &self.song.measure_headers[self.measure_id]; + let previous_measure_header = if self.measure_id > 0 { + Some(&self.song.measure_headers[self.measure_id - 1]) + } else { + None + }; + + // display time signature (if first measure OR if it changed) + if self.measure_id == 0 + || measure_header.time_signature != previous_measure_header.unwrap().time_signature + { + let numerator = measure_header.time_signature.numerator; + let denominator = measure_header.time_signature.denominator.value; + log::debug!("time signature change {} / {}", numerator, denominator); + // TODO draw time signature + } + + // TODO draw repeat annotations + + // capture tempo label len to adjust next annotations + let mut tempo_label_len = 0; + // display measure tempo (if first measure OR if it changed) + if self.measure_id == 0 + || measure_header.tempo != previous_measure_header.unwrap().tempo + { + // TODO use https://unicodeplus.com/U+1D15F + let tempo_label = format!("bpm={}", measure_header.tempo.value); + tempo_label_len = tempo_label.chars().count() * 10; + let tempo_text = Text { + content: tempo_label, + color: Color::WHITE, + size: 10.0.into(), + position: Point::new(measure_start_x, MEASURE_ANNOTATION_Y), + ..Text::default() + }; + frame.fill_text(tempo_text); + } + + // marker annotation + if let Some(marker) = &measure_header.marker { + // measure marker label + let marker_text = Text { + content: marker.title.clone(), + color: color_dark_red, + size: 10.0.into(), + position: Point::new( + measure_start_x + MEASURE_NOTES_PADDING + tempo_label_len as f32, + MEASURE_ANNOTATION_Y, + ), + ..Text::default() + }; + frame.fill_text(marker_text); + } + + // measure count label + let measure_count_text = Text { + content: format!("{}", self.measure_id + 1), + color: color_dark_red, + size: 10.0.into(), + position: Point::new(measure_start_x, FIRST_STRING_Y - 15.0), + ..Text::default() + }; + frame.fill_text(measure_count_text); + + // draw string lines first + for (string_id, _fret) in strings.iter().enumerate() { + // down position + let local_start_y = string_id as f32 * STRING_LINE_HEIGHT; + // add 1 to x to avoid overlapping with vertical line + let start_point = + Point::new(measure_start_x + 1.0, measure_start_y + local_start_y); + // draw at the same y until end of container + let end_point = Point::new( + measure_start_x + self.total_measure_len, + measure_start_y + local_start_y, + ); + let line = Path::line(start_point, end_point); + let stroke = Stroke::default().with_width(0.8).with_color(color_gray); + frame.stroke(&line, stroke); + } + + // add notes on top of strings + let measure = &track.measures[self.measure_id]; + // TODO draw second voice if present? + let beats = &measure.voices[0].beats; + let beats_len = beats.len(); + log::debug!("{} beats", beats_len); + for (b_id, beat) in beats.iter().enumerate() { + // pick color if beat under focus + let beat_color = if self.is_focused && b_id == self.focused_beat { + color_dark_red + } else { + Color::WHITE + }; + // draw beat + draw_beat( + frame, + self.measure_len, + measure_start_x, + measure_start_y, + beats_len, + b_id, + beat, + beat_color, + ); + } + + // vertical measure end + draw_measure_vertical_line( + frame, + vertical_measure_height, + measure_start_x + self.total_measure_len, // end of measure + measure_start_y, + ); + }); + + vec![tab] + } + + fn mouse_interaction( + &self, + _state: &Self::State, + bounds: Rectangle, + cursor: Cursor, + ) -> Interaction { + match cursor { + Cursor::Available(_point) => { + if let Some(_cursor_position) = cursor.position_in(bounds) { + log::debug!("Mouse over measure {:?}", self.measure_id); + } + } + Cursor::Unavailable => {} + } + Interaction::default() + } +} + +fn draw_focused_box( + frame: &mut Frame, + total_measure_len: f32, + vertical_measure_height: f32, + measure_start_x: f32, + measure_start_y: f32, +) { + let padding = 8.0; + + let focused_box = Rectangle { + x: measure_start_x + padding, + y: measure_start_y - padding, + width: total_measure_len - padding * 2.0, + height: vertical_measure_height + padding * 2.0, + }; + + let Rectangle { + x, + y, + width, + height, + } = focused_box; + + let top_left = Point::new(x, y); + let top_right = Point::new(x + width, y); + let bottom_left = Point::new(x, y + height); + let bottom_right = Point::new(x + width, y + height); + + let stroke = Stroke::default().with_width(1.0).with_color(Color::WHITE); + // TODO use `stroke_rectangle` when available + frame.stroke(&Path::line(top_left, top_right), stroke); + frame.stroke(&Path::line(top_left, bottom_left), stroke); + frame.stroke(&Path::line(top_right, bottom_right), stroke); + frame.stroke(&Path::line(bottom_right, bottom_left), stroke); +} + +fn draw_measure_vertical_line( + frame: &mut Frame, + vertical_measure_height: f32, + measure_start_x: f32, + measure_start_y: f32, +) { + let start_point = Point::new(measure_start_x, measure_start_y); + let end_point = Point::new(measure_start_x, measure_start_y + vertical_measure_height); + let vertical_line = Path::line(start_point, end_point); + let stroke = Stroke::default().with_width(1.5).with_color(Color::WHITE); + frame.stroke(&vertical_line, stroke); +} + +#[allow(clippy::too_many_arguments)] +fn draw_beat( + frame: &mut Frame, + measure_len: f32, + measure_start_x: f32, + measure_start_y: f32, + beats_len: usize, + b_id: usize, + beat: &Beat, + beat_color: Color, +) { + // position to draw beat + let width_per_beat = measure_len / beats_len as f32; + let beat_position_offset = b_id as f32 * width_per_beat; + let beat_position_x = measure_start_x + MEASURE_NOTES_PADDING + beat_position_offset; + + // Annotate chord effect + if let Some(chord) = &beat.effect.chord { + let note_effect_text = Text { + content: chord.name.clone(), + color: Color::WHITE, + size: 8.0.into(), + position: Point::new(beat_position_x + 3.0, CHORD_ANNOTATION_Y), + ..Text::default() + }; + frame.fill_text(note_effect_text); + }; + + // draw notes for beat + for note in &beat.notes { + draw_note( + frame, + measure_start_y, + beat_position_x, + width_per_beat, + note, + beat_color, + ); + } +} + +fn draw_note( + frame: &mut Frame, + measure_start_y: f32, + beat_position_x: f32, + width_per_beat: f32, + note: &Note, + beat_color: Color, +) { + // Annotate note effect above (same position for all notes) + let annotations = above_note_effect_annotation(¬e.effect); + if !annotations.is_empty() { + let merged_annotations = annotations.join("\n"); + let y_position = NOTE_EFFECT_ANNOTATION_Y - 4.0 * (annotations.len() - 1) as f32; + let note_effect_text = Text { + content: merged_annotations, + color: Color::WHITE, + size: 9.0.into(), + position: Point::new(beat_position_x - 3.0, y_position), + ..Text::default() + }; + frame.fill_text(note_effect_text); + } + + // note label (pushed down on the right string) + let note_label = note_value(note); + let local_beat_position_y = (note.string as f32 - 1.0) * STRING_LINE_HEIGHT; + // center the notes with more than one char + let note_position_x = beat_position_x + 3.0 - note_label.chars().count() as f32 / 2.0; + let note_position_y = measure_start_y + local_beat_position_y - 5.0; + let note_text = Text { + content: note_label, + color: beat_color, + size: 10.0.into(), + position: Point::new(note_position_x, note_position_y), + horizontal_alignment: Center, + ..Text::default() + }; + frame.fill_text(note_text); + + // Annotate some effects on the string after the note + let inlined_annotation_width = 10.0; + let inlined_annotation_label = inlined_note_effect_annotation(¬e.effect); + // note_x + half of inter-beat space - half of annotation width + let annotation_position_x = + note_position_x + width_per_beat / 2.0 - inlined_annotation_width / 2.0; + let note_effect_text = Text { + shaping: Advanced, // required for printing unicode + content: inlined_annotation_label, + color: Color::WHITE, + size: inlined_annotation_width.into(), + position: Point::new(annotation_position_x, note_position_y), + ..Text::default() + }; + frame.fill_text(note_effect_text); +} + +/// Similar to https://www.tuxguitar.app/files/1.6.0/desktop/help/edit_effects.html +fn above_note_effect_annotation(note_effect: &NoteEffect) -> Vec { + let mut annotations: Vec = vec![]; + if note_effect.accentuated_note { + annotations.push(">".to_string()); + } + if note_effect.heavy_accentuated_note { + annotations.push("^".to_string()); + } + if note_effect.palm_mute { + annotations.push("P.M".to_string()); + } + if note_effect.let_ring { + annotations.push("L.R".to_string()); + } + if let Some(harmonic) = ¬e_effect.harmonic { + match harmonic.kind { + HarmonicType::Natural => annotations.push("N.H".to_string()), + HarmonicType::Artificial => annotations.push("A.H".to_string()), + HarmonicType::Tapped => annotations.push("T.H".to_string()), + HarmonicType::Pinch => annotations.push("P.H".to_string()), + HarmonicType::Semi => annotations.push("S.H".to_string()), + } + } + // TODO use a nice unicode sign instead + if note_effect.vibrato { + annotations.push("~~~".to_string()); + } + if note_effect.trill.is_some() { + annotations.push("tr".to_string()); + } + if note_effect.tremolo_picking.is_some() { + annotations.push("tp".to_string()); + } + if note_effect.tremolo_bar.is_some() { + annotations.push("tb".to_string()); + } + annotations +} + +fn inlined_note_effect_annotation(note_effect: &NoteEffect) -> String { + let mut annotation = String::new(); + if note_effect.hammer { + // https://unicodeplus.com/U+25E0 + annotation.push(std::char::from_u32(0x25E0).unwrap()); + } + if let Some(slide) = ¬e_effect.slide { + match slide { + SlideType::IntoFromAbove => annotation.push(std::char::from_u32(0x2015).unwrap()), // https://unicodeplus.com/U+2015 + SlideType::IntoFromBelow => annotation.push(std::char::from_u32(0x2015).unwrap()), // https://unicodeplus.com/U+2015 + SlideType::ShiftSlideTo => annotation.push(std::char::from_u32(0x27CD).unwrap()), // https://unicodeplus.com/U+27CD + SlideType::LegatoSlideTo => annotation.push(std::char::from_u32(0x27CB).unwrap()), // https://unicodeplus.com/U+27CB + SlideType::OutDownwards => annotation.push(std::char::from_u32(0x2015).unwrap()), // https://unicodeplus.com/U+2015 + SlideType::OutUpWards => annotation.push(std::char::from_u32(0x27CB).unwrap()), // https://unicodeplus.com/U+27CB + } + } + if let Some(_bend) = ¬e_effect.bend { + // TODO display bend properly + annotation.push(std::char::from_u32(0x2191).unwrap()) // https://unicodeplus.com/U+2191 + } + annotation +} + +fn note_value(note: &Note) -> String { + match note.kind { + NoteType::Rest => { + log::debug!("NoteType Rest"); + String::new() + } + NoteType::Normal => { + if note.effect.ghost_note { + format!("({})", note.value) + } else { + note.value.to_string() + } + } + NoteType::Tie => String::new(), + NoteType::Dead => "x".to_string(), + NoteType::Unknown(i) => { + log::warn!("NoteType Unknown({})", i); + String::new() + } + } +} diff --git a/src/ui/iced_aw/mod.rs b/src/ui/iced_aw/mod.rs new file mode 100644 index 0000000..d425d5e --- /dev/null +++ b/src/ui/iced_aw/mod.rs @@ -0,0 +1 @@ +pub mod wrap; diff --git a/src/ui/iced_aw/wrap.rs b/src/ui/iced_aw/wrap.rs new file mode 100644 index 0000000..7a898e3 --- /dev/null +++ b/src/ui/iced_aw/wrap.rs @@ -0,0 +1,531 @@ +// Copy from https://github.com/iced-rs/iced_aw/blob/main/src/widgets/wrap.rs +// until everything settles on iced 0.13 stable + +//! A widget that displays its children in multiple horizontal or vertical runs. +//! +//! *This API requires the following crate features to be activated: `wrap`* +use iced::{ + advanced::{ + layout::{Limits, Node}, + overlay, renderer, + widget::{Operation, Tree}, + Clipboard, Layout, Shell, Widget, + }, + event, + mouse::{self, Cursor}, + Alignment, Element, Event, Length, Padding, Point, Rectangle, Size, Vector, +}; +use std::marker::PhantomData; + +/// A container that distributes its contents horizontally. +#[allow(missing_debug_implementations)] +pub struct Wrap<'a, Message, Direction, Theme = iced::Theme, Renderer = iced::Renderer> { + /// The elements to distribute. + pub elements: Vec>, + /// The alignment of the [`Wrap`]. + pub alignment: Alignment, + /// The width of the [`Wrap`]. + pub width: Length, + /// The height of the [`Wrap`]. + pub height: Length, + /// The maximum width of the [`Wrap`]. + pub max_width: f32, + /// The maximum height of the [`Wrap`]. + pub max_height: f32, + /// The padding of each element of the [`Wrap`]. + pub padding: f32, + /// The spacing between each element of the [`Wrap`]. + pub spacing: f32, + /// The spacing between each line of the [`Wrap`]. + pub line_spacing: f32, + /// The minimal length of each line of the [`Wrap`]. + pub line_minimal_length: f32, + #[allow(clippy::missing_docs_in_private_items)] + _direction: PhantomData, +} + +impl<'a, Message, Theme, Renderer> Wrap<'a, Message, direction::Horizontal, Theme, Renderer> { + /// Creates an empty horizontal [`Wrap`]. + #[must_use] + pub fn new() -> Self { + Self::with_elements(Vec::new()) + } + + /// Creates a [`Wrap`] with the given elements. + /// + /// It expects: + /// * the vector containing the [`Element`]s for this [`Wrap`]. + #[must_use] + pub fn with_elements(elements: Vec>) -> Self { + Self { + elements, + ..Wrap::default() + } + } +} + +impl<'a, Message, Theme, Renderer> Wrap<'a, Message, direction::Vertical, Theme, Renderer> { + /// Creates an empty vertical [`Wrap`]. + #[must_use] + pub fn new_vertical() -> Self { + Self::with_elements_vertical(Vec::new()) + } + + /// Creates a [`Wrap`] with the given elements. + /// + /// It expects: + /// * the vector containing the [`Element`]s for this [`Wrap`]. + #[must_use] + pub fn with_elements_vertical(elements: Vec>) -> Self { + Self { + elements, + ..Wrap::default() + } + } +} + +impl<'a, Message, Renderer, Direction, Theme> Wrap<'a, Message, Direction, Theme, Renderer> { + /// Sets the spacing of the [`Wrap`]. + #[must_use] + pub const fn spacing(mut self, units: f32) -> Self { + self.spacing = units; + self + } + + /// Sets the spacing of the lines of the [`Wrap`]. + #[must_use] + pub const fn line_spacing(mut self, units: f32) -> Self { + self.line_spacing = units; + self + } + + /// Sets the minimal length of the lines of the [`Wrap`]. + #[must_use] + pub const fn line_minimal_length(mut self, units: f32) -> Self { + self.line_minimal_length = units; + self + } + + /// Sets the padding of the elements in the [`Wrap`]. + #[must_use] + pub const fn padding(mut self, units: f32) -> Self { + self.padding = units; + self + } + + /// Sets the width of the [`Wrap`]. + #[must_use] + pub const fn width_items(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`Wrap`]. + #[must_use] + pub const fn height_items(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the maximum width of the [`Wrap`]. + #[must_use] + pub const fn max_width(mut self, max_width: f32) -> Self { + self.max_width = max_width; + self + } + + /// Sets the maximum height of the [`Wrap`]. + #[must_use] + pub const fn max_height(mut self, max_height: f32) -> Self { + self.max_height = max_height; + self + } + + /// Sets the alignment of the [`Wrap`]. + #[must_use] + pub const fn align_items(mut self, align: Alignment) -> Self { + self.alignment = align; + self + } + + /// Pushes an [`Element`] to the [`Wrap`]. + #[must_use] + pub fn push(mut self, element: E) -> Self + where + E: Into>, + { + self.elements.push(element.into()); + self + } +} + +impl<'a, Message, Renderer, Direction, Theme> Widget + for Wrap<'a, Message, Direction, Theme, Renderer> +where + Self: WrapLayout, + Renderer: renderer::Renderer, +{ + fn children(&self) -> Vec { + self.elements.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.elements); + } + + fn size(&self) -> Size { + Size::new(self.width, self.height) + } + + fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node { + self.inner_layout(tree, renderer, limits) + } + + fn on_event( + &mut self, + state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell, + viewport: &Rectangle, + ) -> event::Status { + self.elements + .iter_mut() + .zip(&mut state.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn overlay<'b>( + &'b mut self, + state: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option> { + self.elements + .iter_mut() + .zip(&mut state.children) + .zip(layout.children()) + .find_map(|((child, state), layout)| { + child + .as_widget_mut() + .overlay(state, layout, renderer, translation) + }) + } + + fn mouse_interaction( + &self, + state: &Tree, + layout: Layout<'_>, + cursor: Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.elements + .iter() + .zip(&state.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child + .as_widget() + .mouse_interaction(state, layout, cursor, viewport, renderer) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + state: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: Cursor, + viewport: &Rectangle, + ) { + for ((child, state), layout) in self + .elements + .iter() + .zip(&state.children) + .zip(layout.children()) + { + child + .as_widget() + .draw(state, renderer, theme, style, layout, cursor, viewport); + } + } + + fn operate( + &self, + state: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation<()>, + ) { + for ((element, state), layout) in self + .elements + .iter() + .zip(&mut state.children) + .zip(layout.children()) + { + element + .as_widget() + .operate(state, layout, renderer, operation); + } + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Renderer: 'a + renderer::Renderer, + Message: 'a, + Theme: 'a, +{ + fn from(wrap: Wrap<'a, Message, direction::Vertical, Theme, Renderer>) -> Self { + Element::new(wrap) + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Renderer: 'a + renderer::Renderer, + Message: 'a, + Theme: 'a, +{ + fn from(wrap: Wrap<'a, Message, direction::Horizontal, Theme, Renderer>) -> Self { + Element::new(wrap) + } +} + +impl<'a, Message, Renderer, Direction, Theme> Default + for Wrap<'a, Message, Direction, Theme, Renderer> +{ + fn default() -> Self { + Self { + elements: vec![], + alignment: Alignment::Start, + width: Length::Shrink, + height: Length::Shrink, + max_width: 4_294_967_295.0, + max_height: 4_294_967_295.0, + padding: 0.0, + spacing: 0.0, + line_spacing: 0.0, + line_minimal_length: 10.0, + _direction: PhantomData, + } + } +} +/// A inner layout of the [`Wrap`]. +pub trait WrapLayout +where + Renderer: renderer::Renderer, +{ + /// A inner layout of the [`Wrap`]. + fn inner_layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node; +} + +impl<'a, Message, Theme, Renderer> WrapLayout + for Wrap<'a, Message, direction::Horizontal, Theme, Renderer> +where + Renderer: renderer::Renderer + 'a, +{ + #[allow(clippy::inline_always)] + #[inline(always)] + fn inner_layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node { + let padding = Padding::from(self.padding); + let spacing = self.spacing; + let line_spacing = self.line_spacing; + #[allow(clippy::cast_precision_loss)] // TODO: possible precision loss + let line_minimal_length = self.line_minimal_length; + let limits = limits + .shrink(padding) + .width(self.width) + .height(self.height) + .max_width(self.max_width) + .max_height(self.max_height); + let max_width = limits.max().width; + + let mut children = tree.children.iter_mut(); + let mut curse = padding.left; + let mut deep_curse = padding.left; + let mut current_line_height = line_minimal_length; + let mut max_main = curse; + let mut align = vec![]; + let mut start = 0; + let mut end = 0; + let mut nodes: Vec = self + .elements + .iter() + .map(|elem| { + let node_limit = Limits::new( + Size::new(limits.min().width, line_minimal_length), + limits.max(), + ); + let mut node = elem.as_widget().layout( + children.next().expect("wrap missing expected child"), + renderer, + &node_limit, + ); + + let size = node.size(); + + let offset_init = size.width + spacing; + let offset = curse + offset_init; + + if offset > max_width { + deep_curse += current_line_height + line_spacing; + align.push((start..end, current_line_height)); + start = end; + end += 1; + current_line_height = line_minimal_length; + node.move_to_mut(Point::new(padding.left, deep_curse)); + curse = offset_init + padding.left; + } else { + node.move_to_mut(Point::new(curse, deep_curse)); + curse = offset; + end += 1; + } + current_line_height = current_line_height.max(size.height); + max_main = max_main.max(curse); + + node + }) + .collect(); + if end != start { + align.push((start..end, current_line_height)); + } + for (range, max_length) in align { + nodes[range].iter_mut().for_each(|node| { + let size = node.size(); + let space = Size::new(size.width, max_length); + node.align_mut(Alignment::Start, self.alignment, space); + }); + } + let (width, height) = ( + max_main - padding.left, + deep_curse - padding.left + current_line_height, + ); + let size = limits.resolve(self.width, self.height, Size::new(width, height)); + + Node::with_children(size.expand(padding), nodes) + } +} + +impl<'a, Message, Theme, Renderer> WrapLayout + for Wrap<'a, Message, direction::Vertical, Theme, Renderer> +where + Renderer: renderer::Renderer + 'a, +{ + #[allow(clippy::inline_always)] + #[inline(always)] + fn inner_layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node { + let padding = Padding::from(self.padding); + let spacing = self.spacing; + let line_spacing = self.line_spacing; + #[allow(clippy::cast_precision_loss)] // TODO: possible precision loss + let line_minimal_length = self.line_minimal_length; + let limits = limits + .shrink(padding) + .width(self.width) + .height(self.height) + .max_width(self.max_width) + .max_height(self.max_height); + let max_height = limits.max().height; + + let mut children = tree.children.iter_mut(); + let mut curse = padding.left; + let mut wide_curse = padding.left; + let mut current_line_width = line_minimal_length; + let mut max_main = curse; + let mut align = vec![]; + let mut start = 0; + let mut end = 0; + let mut nodes: Vec = self + .elements + .iter() + .map(|elem| { + let node_limit = Limits::new( + Size::new(line_minimal_length, limits.min().height), + limits.max(), + ); + let mut node = elem.as_widget().layout( + children.next().expect("wrap missing expected child"), + renderer, + &node_limit, + ); + + let size = node.size(); + + let offset_init = size.height + spacing; + let offset = curse + offset_init; + + if offset > max_height { + wide_curse += current_line_width + line_spacing; + align.push((start..end, current_line_width)); + start = end; + end += 1; + current_line_width = line_minimal_length; + node = node.move_to(Point::new(wide_curse, padding.left)); + curse = offset_init + padding.left; + } else { + node = node.move_to(Point::new(wide_curse, curse)); + end += 1; + curse = offset; + } + current_line_width = current_line_width.max(size.width); + max_main = max_main.max(curse); + + node + }) + .collect(); + if end != start { + align.push((start..end, current_line_width)); + } + + for (range, max_length) in align { + nodes[range].iter_mut().for_each(|node| { + let size = node.size(); + let space = Size::new(max_length, size.height); + node.align_mut(self.alignment, Alignment::Start, space); + }); + } + + let (width, height) = ( + wide_curse - padding.left + current_line_width, + max_main - padding.left, + ); + let size = limits.resolve(self.width, self.height, Size::new(width, height)); + + Node::with_children(size.expand(padding), nodes) + } +} + +/// An optional directional attribute of the [`Wrap`](crate::Wrap). +pub mod direction { + /// An vertical direction of the [`Wrap`](crate::Wrap). + #[derive(Debug)] + pub struct Vertical; + /// An horizontal direction of the [`Wrap`](crate::Wrap). + #[derive(Debug)] + pub struct Horizontal; +} diff --git a/src/ui/icons.rs b/src/ui/icons.rs new file mode 100644 index 0000000..db4eb9b --- /dev/null +++ b/src/ui/icons.rs @@ -0,0 +1,30 @@ +//! Icons coming from + +use iced::widget::text; +use iced::{Element, Font}; + +pub fn open_icon<'a, Message>() -> Element<'a, Message> { + icon('\u{0f115}') +} + +pub fn solo_icon<'a, Message>() -> Element<'a, Message> { + text('S').into() +} + +pub fn pause_icon<'a, Message>() -> Element<'a, Message> { + icon('\u{0e802}') +} + +pub fn play_icon<'a, Message>() -> Element<'a, Message> { + icon('\u{0e800}') +} + +pub fn stop_icon<'a, Message>() -> Element<'a, Message> { + icon('\u{0e801}') +} + +fn icon<'a, Message>(codepoint: char) -> Element<'a, Message> { + const ICON_FONT: Font = Font::with_name("ruxguitar-icons"); + + text(codepoint).font(ICON_FONT).into() +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..572a85c --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,7 @@ +pub mod application; +mod canvas_measure; +pub mod iced_aw; +mod icons; +mod picker; +mod tablature; +mod utils; diff --git a/src/ui/picker.rs b/src/ui/picker.rs new file mode 100644 index 0000000..5dcee37 --- /dev/null +++ b/src/ui/picker.rs @@ -0,0 +1,29 @@ +use std::path::PathBuf; + +#[derive(Debug, Clone, thiserror::Error)] +pub enum PickerError { + #[error("dialog window closed without selecting a file")] + DialogClosed, + #[error("IO error: {0}")] + IoError(String), +} + +pub async fn open_file() -> Result<(Vec, String), PickerError> { + let picked_file = rfd::AsyncFileDialog::new() + .add_filter("Guitar Pro files", &["gp5"]) // only gp5 for now in parser + .set_title("Pick a GP file") + .pick_file() + .await + .ok_or(PickerError::DialogClosed)?; + let file_name = picked_file.file_name(); + log::info!("Loading file: {:?}", file_name); + let content = load_file(picked_file).await?; + Ok((content, file_name)) +} + +async fn load_file(path: impl Into) -> Result, PickerError> { + let path = path.into(); + tokio::fs::read(&path) + .await + .map_err(|error| PickerError::IoError(error.to_string())) +} diff --git a/src/ui/tablature.rs b/src/ui/tablature.rs new file mode 100644 index 0000000..13b28ac --- /dev/null +++ b/src/ui/tablature.rs @@ -0,0 +1,99 @@ +use crate::parser::song_parser::Song; +use crate::ui::application::Message; +use crate::ui::canvas_measure::CanvasMeasure; +use crate::ui::iced_aw::wrap::Wrap; +use iced::widget::scrollable; +use iced::widget::scrollable::Id; +use iced::{Element, Length}; +use std::rc::Rc; + +pub struct Tablature { + pub song: Rc, + pub track_id: usize, + pub canvas_measures: Vec, + pub focuses_measure: usize, + pub scroll_id: Id, +} + +impl Tablature { + pub fn new(song: Rc, track_id: usize, scroll_id: Id) -> Self { + let measure_count = song.measure_headers.len(); + let mut tab = Self { + song, + track_id, + canvas_measures: Vec::with_capacity(measure_count), + focuses_measure: 0, + scroll_id, + }; + tab.load_measures(); + tab + } + + pub fn load_measures(&mut self) { + // clear existing measures + self.canvas_measures.clear(); + + // load new measures + let track = &self.song.tracks[self.track_id]; + let measures = track.measures.len(); + for i in 0..measures { + let focused = self.focuses_measure == i; + let measure = CanvasMeasure::new(i, self.track_id, self.song.clone(), focused); + self.canvas_measures.push(measure); + } + } + + pub fn focus_on_tick(&mut self, tick: usize) { + // TODO autoscroll if necessary + let (new_measure_id, new_beat_id) = + self.song.get_measure_beat_for_tick(self.track_id, tick); + let current_focus_id = self.focuses_measure; + let current_canvas = self.canvas_measures.get_mut(current_focus_id).unwrap(); + if current_focus_id != new_measure_id { + // move to next measure + current_canvas.toggle_focused(); + let next_focus_id = new_measure_id; + if next_focus_id < self.canvas_measures.len() { + self.focuses_measure = next_focus_id; + let next_canvas = self.canvas_measures.get_mut(next_focus_id).unwrap(); + next_canvas.toggle_focused(); + } + } else { + // focus on beat id + current_canvas.focus_beat(new_beat_id); + } + } + + pub fn focus_on_measure(&mut self, new_measure_id: usize) { + let measure_headers = &self.song.measure_headers[new_measure_id]; + let tick = measure_headers.start; + self.focus_on_tick(tick as usize); + } + + pub fn view(&self) -> Element { + let measure_elements = self + .canvas_measures + .iter() + .map(|m| m.view()) + .collect::>>(); + + let column = Wrap::with_elements(measure_elements) + .padding(10.0) + .align_items(iced::Alignment::Center); // TODO does not work?? + + scrollable(column) + .id(self.scroll_id.clone()) + .height(Length::Fill) + .width(Length::Fill) + .direction(scrollable::Direction::default()) + .into() + } + + pub fn update_track(&mut self, track: usize) { + // No op if track is the same + if track != self.track_id { + self.track_id = track; + self.load_measures(); + } + } +} diff --git a/src/ui/utils.rs b/src/ui/utils.rs new file mode 100644 index 0000000..7321d2b --- /dev/null +++ b/src/ui/utils.rs @@ -0,0 +1,56 @@ +use crate::ui::application::Message; +use iced::widget::{button, container, tooltip, Container, Text}; +use iced::{Color, Element, Length}; + +pub fn untitled_text_table_box() -> Container<'static, Message> { + let message = "Tip: use the space bar to play/pause"; + let text = Text::new(message).color(Color::WHITE); + let container = Container::new(text) + .center_x(Length::Fill) + .center_y(Length::Fill) + .padding(20); + container +} + +pub fn action_gated<'a, Message: Clone + 'a>( + content: impl Into>, + label: &'a str, + on_press: Option, +) -> Element<'a, Message> { + let action = button(container(content).center_x(30)); + + if let Some(on_press) = on_press { + tooltip( + action.on_press(on_press), + label, + tooltip::Position::FollowCursor, + ) + .style(container::rounded_box) + .into() + } else { + action.style(button::secondary).into() + } +} + +pub fn action_toggle<'a, Message: Clone + 'a>( + content: impl Into>, + label: &'a str, + on_press: Message, + pressed: bool, +) -> Element<'a, Message> { + let action = button(container(content).center_x(30)); + + let action = if pressed { + action.style(button::secondary) + } else { + action + }; + + tooltip( + action.on_press(on_press), + label, + tooltip::Position::FollowCursor, + ) + .style(container::rounded_box) + .into() +} diff --git a/test-files/Demo v5.gp5 b/test-files/Demo v5.gp5 new file mode 100644 index 0000000..a06ca80 Binary files /dev/null and b/test-files/Demo v5.gp5 differ diff --git a/test-files/Ghost - Cirice.gp5 b/test-files/Ghost - Cirice.gp5 new file mode 100644 index 0000000..a12f1a9 Binary files /dev/null and b/test-files/Ghost - Cirice.gp5 differ