├── .github └── workflows │ ├── check.yml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── asak.gif ├── build.rs └── src ├── cli.rs ├── device.rs ├── main.rs ├── monitor.rs ├── playback.rs └── record.rs /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/RustAudio/cpal/blob/master/.github/workflows/cpal.yml 2 | name: Rust 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | # ubuntu-test: 14 | # runs-on: ubuntu-latest 15 | # steps: 16 | # - uses: actions/checkout@v4 17 | # - name: Update apt 18 | # run: sudo apt update 19 | # - name: Install alsa 20 | # run: sudo apt-get install libasound2-dev 21 | # - name: Install libjack 22 | # run: sudo apt-get install libjack-jackd2-dev libjack-jackd2-0 23 | # - name: Install stable 24 | # uses: dtolnay/rust-toolchain@stable 25 | # - name: Run without features 26 | # run: cargo run -- play 27 | # windows-test: 28 | # strategy: 29 | # matrix: 30 | # version: [x86_64, i686] 31 | # runs-on: windows-latest 32 | # steps: 33 | # - uses: actions/checkout@v4 34 | # - name: Install ASIO SDK 35 | # env: 36 | # LINK: https://www.steinberg.net/asiosdk 37 | # run: | 38 | # curl -L -o asio.zip $env:LINK 39 | # 7z x -oasio asio.zip 40 | # move asio\*\* asio\ 41 | # - name: Install ASIO4ALL 42 | # run: choco install asio4all 43 | # - name: Install llvm and clang 44 | # run: choco install llvm 45 | # - name: Install stable 46 | # uses: dtolnay/rust-toolchain@stable 47 | # with: 48 | # target: ${{ matrix.version }}-pc-windows-msvc 49 | # - name: Run all features 50 | # run: | 51 | # $Env:CPAL_ASIO_DIR = "$Env:GITHUB_WORKSPACE\asio" 52 | # cargo run -- play 53 | 54 | # macos-test: 55 | # runs-on: macOS-latest 56 | # steps: 57 | # - uses: actions/checkout@v4 58 | # - name: Install llvm and clang 59 | # run: brew install llvm 60 | # - name: Install stable 61 | # uses: dtolnay/rust-toolchain@stable 62 | # - name: Play audio 63 | # run: cargo run -- play 64 | clippy-test: 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@v4 68 | - name: Update apt 69 | run: sudo apt update 70 | - name: Install alsa 71 | run: sudo apt-get install libasound2-dev 72 | - name: Install libjack 73 | run: sudo apt-get install libjack-jackd2-dev libjack-jackd2-0 74 | - name: Install stable 75 | uses: dtolnay/rust-toolchain@stable 76 | with: 77 | components: clippy 78 | - name: Run clippy 79 | run: cargo clippy --all --all-features 80 | rustfmt-check: 81 | runs-on: ubuntu-latest 82 | steps: 83 | - uses: actions/checkout@v4 84 | - name: Install stable 85 | uses: dtolnay/rust-toolchain@stable 86 | with: 87 | components: rustfmt 88 | - name: Run rustfmt 89 | run: cargo fmt --all -- --check 90 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2024, axodotdev 2 | # SPDX-License-Identifier: MIT or Apache-2.0 3 | # 4 | # CI that: 5 | # 6 | # * checks for a Git Tag that looks like a release 7 | # * builds artifacts with cargo-dist (archives, installers, hashes) 8 | # * uploads those artifacts to temporary workflow zip 9 | # * on success, uploads the artifacts to a GitHub Release 10 | # 11 | # Note that the GitHub Release will be created with a generated 12 | # title/body based on your changelogs. 13 | 14 | name: Release 15 | 16 | permissions: 17 | contents: write 18 | 19 | # This task will run whenever you push a git tag that looks like a version 20 | # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. 21 | # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where 22 | # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION 23 | # must be a Cargo-style SemVer Version (must have at least major.minor.patch). 24 | # 25 | # If PACKAGE_NAME is specified, then the announcement will be for that 26 | # package (erroring out if it doesn't have the given version or isn't cargo-dist-able). 27 | # 28 | # If PACKAGE_NAME isn't specified, then the announcement will be for all 29 | # (cargo-dist-able) packages in the workspace with that version (this mode is 30 | # intended for workspaces with only one dist-able package, or with all dist-able 31 | # packages versioned/released in lockstep). 32 | # 33 | # If you push multiple tags at once, separate instances of this workflow will 34 | # spin up, creating an independent announcement for each one. However, GitHub 35 | # will hard limit this to 3 tags per commit, as it will assume more tags is a 36 | # mistake. 37 | # 38 | # If there's a prerelease-style suffix to the version, then the release(s) 39 | # will be marked as a prerelease. 40 | on: 41 | pull_request: 42 | push: 43 | tags: 44 | - '**[0-9]+.[0-9]+.[0-9]+*' 45 | 46 | jobs: 47 | # Run 'cargo dist plan' (or host) to determine what tasks we need to do 48 | plan: 49 | runs-on: "ubuntu-20.04" 50 | outputs: 51 | val: ${{ steps.plan.outputs.manifest }} 52 | tag: ${{ !github.event.pull_request && github.ref_name || '' }} 53 | tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} 54 | publishing: ${{ !github.event.pull_request }} 55 | env: 56 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | steps: 58 | - uses: actions/checkout@v4 59 | with: 60 | submodules: recursive 61 | - name: Install cargo-dist 62 | # we specify bash to get pipefail; it guards against the `curl` command 63 | # failing. otherwise `sh` won't catch that `curl` returned non-0 64 | shell: bash 65 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.16.0/cargo-dist-installer.sh | sh" 66 | # sure would be cool if github gave us proper conditionals... 67 | # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible 68 | # functionality based on whether this is a pull_request, and whether it's from a fork. 69 | # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* 70 | # but also really annoying to build CI around when it needs secrets to work right.) 71 | - id: plan 72 | run: | 73 | cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json 74 | echo "cargo dist ran successfully" 75 | cat plan-dist-manifest.json 76 | echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" 77 | - name: "Upload dist-manifest.json" 78 | uses: actions/upload-artifact@v4 79 | with: 80 | name: artifacts-plan-dist-manifest 81 | path: plan-dist-manifest.json 82 | 83 | # Build and packages all the platform-specific things 84 | build-local-artifacts: 85 | name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) 86 | # Let the initial task tell us to not run (currently very blunt) 87 | needs: 88 | - plan 89 | if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} 90 | strategy: 91 | fail-fast: false 92 | # Target platforms/runners are computed by cargo-dist in create-release. 93 | # Each member of the matrix has the following arguments: 94 | # 95 | # - runner: the github runner 96 | # - dist-args: cli flags to pass to cargo dist 97 | # - install-dist: expression to run to install cargo-dist on the runner 98 | # 99 | # Typically there will be: 100 | # - 1 "global" task that builds universal installers 101 | # - N "local" tasks that build each platform's binaries and platform-specific installers 102 | matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} 103 | runs-on: ${{ matrix.runner }} 104 | env: 105 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 106 | BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json 107 | steps: 108 | - name: enable windows longpaths 109 | run: | 110 | git config --global core.longpaths true 111 | - uses: actions/checkout@v4 112 | with: 113 | submodules: recursive 114 | - uses: swatinem/rust-cache@v2 115 | with: 116 | key: ${{ join(matrix.targets, '-') }} 117 | cache-provider: ${{ matrix.cache_provider }} 118 | - name: Install cargo-dist 119 | run: ${{ matrix.install_dist }} 120 | # Get the dist-manifest 121 | - name: Fetch local artifacts 122 | uses: actions/download-artifact@v4 123 | with: 124 | pattern: artifacts-* 125 | path: target/distrib/ 126 | merge-multiple: true 127 | - name: Install dependencies 128 | run: | 129 | ${{ matrix.packages_install }} 130 | - name: Build artifacts 131 | run: | 132 | # Actually do builds and make zips and whatnot 133 | cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json 134 | echo "cargo dist ran successfully" 135 | - id: cargo-dist 136 | name: Post-build 137 | # We force bash here just because github makes it really hard to get values up 138 | # to "real" actions without writing to env-vars, and writing to env-vars has 139 | # inconsistent syntax between shell and powershell. 140 | shell: bash 141 | run: | 142 | # Parse out what we just built and upload it to scratch storage 143 | echo "paths<> "$GITHUB_OUTPUT" 144 | jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" 145 | echo "EOF" >> "$GITHUB_OUTPUT" 146 | 147 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 148 | - name: "Upload artifacts" 149 | uses: actions/upload-artifact@v4 150 | with: 151 | name: artifacts-build-local-${{ join(matrix.targets, '_') }} 152 | path: | 153 | ${{ steps.cargo-dist.outputs.paths }} 154 | ${{ env.BUILD_MANIFEST_NAME }} 155 | 156 | # Build and package all the platform-agnostic(ish) things 157 | build-global-artifacts: 158 | needs: 159 | - plan 160 | - build-local-artifacts 161 | runs-on: "ubuntu-20.04" 162 | env: 163 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 164 | BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json 165 | steps: 166 | - uses: actions/checkout@v4 167 | with: 168 | submodules: recursive 169 | - name: Install cargo-dist 170 | shell: bash 171 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.16.0/cargo-dist-installer.sh | sh" 172 | # Get all the local artifacts for the global tasks to use (for e.g. checksums) 173 | - name: Fetch local artifacts 174 | uses: actions/download-artifact@v4 175 | with: 176 | pattern: artifacts-* 177 | path: target/distrib/ 178 | merge-multiple: true 179 | - id: cargo-dist 180 | shell: bash 181 | run: | 182 | cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json 183 | echo "cargo dist ran successfully" 184 | 185 | # Parse out what we just built and upload it to scratch storage 186 | echo "paths<> "$GITHUB_OUTPUT" 187 | jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" 188 | echo "EOF" >> "$GITHUB_OUTPUT" 189 | 190 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 191 | - name: "Upload artifacts" 192 | uses: actions/upload-artifact@v4 193 | with: 194 | name: artifacts-build-global 195 | path: | 196 | ${{ steps.cargo-dist.outputs.paths }} 197 | ${{ env.BUILD_MANIFEST_NAME }} 198 | # Determines if we should publish/announce 199 | host: 200 | needs: 201 | - plan 202 | - build-local-artifacts 203 | - build-global-artifacts 204 | # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) 205 | if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} 206 | env: 207 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 208 | runs-on: "ubuntu-20.04" 209 | outputs: 210 | val: ${{ steps.host.outputs.manifest }} 211 | steps: 212 | - uses: actions/checkout@v4 213 | with: 214 | submodules: recursive 215 | - name: Install cargo-dist 216 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.16.0/cargo-dist-installer.sh | sh" 217 | # Fetch artifacts from scratch-storage 218 | - name: Fetch artifacts 219 | uses: actions/download-artifact@v4 220 | with: 221 | pattern: artifacts-* 222 | path: target/distrib/ 223 | merge-multiple: true 224 | # This is a harmless no-op for GitHub Releases, hosting for that happens in "announce" 225 | - id: host 226 | shell: bash 227 | run: | 228 | cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json 229 | echo "artifacts uploaded and released successfully" 230 | cat dist-manifest.json 231 | echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" 232 | - name: "Upload dist-manifest.json" 233 | uses: actions/upload-artifact@v4 234 | with: 235 | # Overwrite the previous copy 236 | name: artifacts-dist-manifest 237 | path: dist-manifest.json 238 | 239 | # Create a GitHub Release while uploading all files to it 240 | announce: 241 | needs: 242 | - plan 243 | - host 244 | # use "always() && ..." to allow us to wait for all publish jobs while 245 | # still allowing individual publish jobs to skip themselves (for prereleases). 246 | # "host" however must run to completion, no skipping allowed! 247 | if: ${{ always() && needs.host.result == 'success' }} 248 | runs-on: "ubuntu-20.04" 249 | env: 250 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 251 | steps: 252 | - uses: actions/checkout@v4 253 | with: 254 | submodules: recursive 255 | - name: "Download GitHub Artifacts" 256 | uses: actions/download-artifact@v4 257 | with: 258 | pattern: artifacts-* 259 | path: artifacts 260 | merge-multiple: true 261 | - name: Cleanup 262 | run: | 263 | # Remove the granular manifests 264 | rm -f artifacts/*-dist-manifest.json 265 | - name: Create GitHub Release 266 | env: 267 | PRERELEASE_FLAG: "${{ fromJson(needs.host.outputs.val).announcement_is_prerelease && '--prerelease' || '' }}" 268 | ANNOUNCEMENT_TITLE: "${{ fromJson(needs.host.outputs.val).announcement_title }}" 269 | ANNOUNCEMENT_BODY: "${{ fromJson(needs.host.outputs.val).announcement_github_body }}" 270 | run: | 271 | # Write and read notes from a file to avoid quoting breaking things 272 | echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt 273 | 274 | gh release create "${{ needs.plan.outputs.tag }}" --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" $PRERELEASE_FLAG 275 | gh release upload "${{ needs.plan.outputs.tag }}" artifacts/* 276 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.9" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d713b3834d76b85304d4d525563c1276e2e30dc97cc67bfb4585a4a29fc2c89f" 10 | dependencies = [ 11 | "cfg-if", 12 | "once_cell", 13 | "version_check", 14 | "zerocopy", 15 | ] 16 | 17 | [[package]] 18 | name = "aho-corasick" 19 | version = "1.1.2" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 22 | dependencies = [ 23 | "memchr", 24 | ] 25 | 26 | [[package]] 27 | name = "allocator-api2" 28 | version = "0.2.16" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" 31 | 32 | [[package]] 33 | name = "alsa" 34 | version = "0.7.1" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "e2562ad8dcf0f789f65c6fdaad8a8a9708ed6b488e649da28c01656ad66b8b47" 37 | dependencies = [ 38 | "alsa-sys", 39 | "bitflags 1.3.2", 40 | "libc", 41 | "nix", 42 | ] 43 | 44 | [[package]] 45 | name = "alsa-sys" 46 | version = "0.3.1" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" 49 | dependencies = [ 50 | "libc", 51 | "pkg-config", 52 | ] 53 | 54 | [[package]] 55 | name = "android-tzdata" 56 | version = "0.1.1" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 59 | 60 | [[package]] 61 | name = "android_system_properties" 62 | version = "0.1.5" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 65 | dependencies = [ 66 | "libc", 67 | ] 68 | 69 | [[package]] 70 | name = "anstream" 71 | version = "0.6.12" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "96b09b5178381e0874812a9b157f7fe84982617e48f71f4e3235482775e5b540" 74 | dependencies = [ 75 | "anstyle", 76 | "anstyle-parse", 77 | "anstyle-query", 78 | "anstyle-wincon", 79 | "colorchoice", 80 | "utf8parse", 81 | ] 82 | 83 | [[package]] 84 | name = "anstyle" 85 | version = "1.0.6" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" 88 | 89 | [[package]] 90 | name = "anstyle-parse" 91 | version = "0.2.3" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 94 | dependencies = [ 95 | "utf8parse", 96 | ] 97 | 98 | [[package]] 99 | name = "anstyle-query" 100 | version = "1.0.2" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 103 | dependencies = [ 104 | "windows-sys 0.52.0", 105 | ] 106 | 107 | [[package]] 108 | name = "anstyle-wincon" 109 | version = "3.0.2" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 112 | dependencies = [ 113 | "anstyle", 114 | "windows-sys 0.52.0", 115 | ] 116 | 117 | [[package]] 118 | name = "anyhow" 119 | version = "1.0.80" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" 122 | 123 | [[package]] 124 | name = "asak" 125 | version = "0.3.6" 126 | dependencies = [ 127 | "anyhow", 128 | "chrono", 129 | "clap", 130 | "clap_complete", 131 | "clap_mangen", 132 | "colored", 133 | "cpal", 134 | "crossbeam", 135 | "crossterm 0.29.0", 136 | "dasp_interpolate", 137 | "dasp_ring_buffer", 138 | "dasp_signal", 139 | "hound", 140 | "inquire", 141 | "parking_lot", 142 | "rand", 143 | "ratatui", 144 | "smallvec", 145 | ] 146 | 147 | [[package]] 148 | name = "autocfg" 149 | version = "1.1.0" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 152 | 153 | [[package]] 154 | name = "bindgen" 155 | version = "0.69.4" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" 158 | dependencies = [ 159 | "bitflags 2.9.0", 160 | "cexpr", 161 | "clang-sys", 162 | "itertools 0.12.1", 163 | "lazy_static", 164 | "lazycell", 165 | "proc-macro2", 166 | "quote", 167 | "regex", 168 | "rustc-hash", 169 | "shlex", 170 | "syn 2.0.49", 171 | ] 172 | 173 | [[package]] 174 | name = "bitflags" 175 | version = "1.3.2" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 178 | 179 | [[package]] 180 | name = "bitflags" 181 | version = "2.9.0" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 184 | 185 | [[package]] 186 | name = "bumpalo" 187 | version = "3.15.0" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "d32a994c2b3ca201d9b263612a374263f05e7adde37c4707f693dcd375076d1f" 190 | 191 | [[package]] 192 | name = "byteorder" 193 | version = "1.5.0" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 196 | 197 | [[package]] 198 | name = "bytes" 199 | version = "1.5.0" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 202 | 203 | [[package]] 204 | name = "cassowary" 205 | version = "0.3.0" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 208 | 209 | [[package]] 210 | name = "castaway" 211 | version = "0.2.3" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 214 | dependencies = [ 215 | "rustversion", 216 | ] 217 | 218 | [[package]] 219 | name = "cc" 220 | version = "1.0.83" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 223 | dependencies = [ 224 | "jobserver", 225 | "libc", 226 | ] 227 | 228 | [[package]] 229 | name = "cesu8" 230 | version = "1.1.0" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 233 | 234 | [[package]] 235 | name = "cexpr" 236 | version = "0.6.0" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 239 | dependencies = [ 240 | "nom", 241 | ] 242 | 243 | [[package]] 244 | name = "cfg-if" 245 | version = "1.0.0" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 248 | 249 | [[package]] 250 | name = "chrono" 251 | version = "0.4.35" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" 254 | dependencies = [ 255 | "android-tzdata", 256 | "iana-time-zone", 257 | "js-sys", 258 | "num-traits", 259 | "wasm-bindgen", 260 | "windows-targets 0.52.0", 261 | ] 262 | 263 | [[package]] 264 | name = "clang-sys" 265 | version = "1.7.0" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" 268 | dependencies = [ 269 | "glob", 270 | "libc", 271 | "libloading 0.8.1", 272 | ] 273 | 274 | [[package]] 275 | name = "clap" 276 | version = "4.5.11" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" 279 | dependencies = [ 280 | "clap_builder", 281 | "clap_derive", 282 | ] 283 | 284 | [[package]] 285 | name = "clap_builder" 286 | version = "4.5.11" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" 289 | dependencies = [ 290 | "anstream", 291 | "anstyle", 292 | "clap_lex", 293 | "strsim", 294 | ] 295 | 296 | [[package]] 297 | name = "clap_complete" 298 | version = "4.5.11" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "c6ae69fbb0833c6fcd5a8d4b8609f108c7ad95fc11e248d853ff2c42a90df26a" 301 | dependencies = [ 302 | "clap", 303 | ] 304 | 305 | [[package]] 306 | name = "clap_derive" 307 | version = "4.5.11" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" 310 | dependencies = [ 311 | "heck", 312 | "proc-macro2", 313 | "quote", 314 | "syn 2.0.49", 315 | ] 316 | 317 | [[package]] 318 | name = "clap_lex" 319 | version = "0.7.0" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 322 | 323 | [[package]] 324 | name = "clap_mangen" 325 | version = "0.2.23" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "f17415fd4dfbea46e3274fcd8d368284519b358654772afb700dc2e8d2b24eeb" 328 | dependencies = [ 329 | "clap", 330 | "roff", 331 | ] 332 | 333 | [[package]] 334 | name = "colorchoice" 335 | version = "1.0.0" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 338 | 339 | [[package]] 340 | name = "colored" 341 | version = "3.0.0" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" 344 | dependencies = [ 345 | "windows-sys 0.52.0", 346 | ] 347 | 348 | [[package]] 349 | name = "combine" 350 | version = "4.6.6" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" 353 | dependencies = [ 354 | "bytes", 355 | "memchr", 356 | ] 357 | 358 | [[package]] 359 | name = "compact_str" 360 | version = "0.8.1" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" 363 | dependencies = [ 364 | "castaway", 365 | "cfg-if", 366 | "itoa", 367 | "rustversion", 368 | "ryu", 369 | "static_assertions", 370 | ] 371 | 372 | [[package]] 373 | name = "convert_case" 374 | version = "0.7.1" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" 377 | dependencies = [ 378 | "unicode-segmentation", 379 | ] 380 | 381 | [[package]] 382 | name = "core-foundation-sys" 383 | version = "0.8.6" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 386 | 387 | [[package]] 388 | name = "coreaudio-rs" 389 | version = "0.11.3" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" 392 | dependencies = [ 393 | "bitflags 1.3.2", 394 | "core-foundation-sys", 395 | "coreaudio-sys", 396 | ] 397 | 398 | [[package]] 399 | name = "coreaudio-sys" 400 | version = "0.2.15" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "7f01585027057ff5f0a5bf276174ae4c1594a2c5bde93d5f46a016d76270f5a9" 403 | dependencies = [ 404 | "bindgen", 405 | ] 406 | 407 | [[package]] 408 | name = "cpal" 409 | version = "0.15.2" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c" 412 | dependencies = [ 413 | "alsa", 414 | "core-foundation-sys", 415 | "coreaudio-rs", 416 | "dasp_sample", 417 | "jack", 418 | "jni 0.19.0", 419 | "js-sys", 420 | "libc", 421 | "mach2", 422 | "ndk", 423 | "ndk-context", 424 | "oboe", 425 | "once_cell", 426 | "parking_lot", 427 | "wasm-bindgen", 428 | "wasm-bindgen-futures", 429 | "web-sys", 430 | "windows", 431 | ] 432 | 433 | [[package]] 434 | name = "crossbeam" 435 | version = "0.8.4" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" 438 | dependencies = [ 439 | "crossbeam-channel", 440 | "crossbeam-deque", 441 | "crossbeam-epoch", 442 | "crossbeam-queue", 443 | "crossbeam-utils", 444 | ] 445 | 446 | [[package]] 447 | name = "crossbeam-channel" 448 | version = "0.5.13" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" 451 | dependencies = [ 452 | "crossbeam-utils", 453 | ] 454 | 455 | [[package]] 456 | name = "crossbeam-deque" 457 | version = "0.8.5" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 460 | dependencies = [ 461 | "crossbeam-epoch", 462 | "crossbeam-utils", 463 | ] 464 | 465 | [[package]] 466 | name = "crossbeam-epoch" 467 | version = "0.9.18" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 470 | dependencies = [ 471 | "crossbeam-utils", 472 | ] 473 | 474 | [[package]] 475 | name = "crossbeam-queue" 476 | version = "0.3.11" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" 479 | dependencies = [ 480 | "crossbeam-utils", 481 | ] 482 | 483 | [[package]] 484 | name = "crossbeam-utils" 485 | version = "0.8.20" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 488 | 489 | [[package]] 490 | name = "crossterm" 491 | version = "0.25.0" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" 494 | dependencies = [ 495 | "bitflags 1.3.2", 496 | "crossterm_winapi", 497 | "libc", 498 | "mio 0.8.10", 499 | "parking_lot", 500 | "signal-hook", 501 | "signal-hook-mio", 502 | "winapi", 503 | ] 504 | 505 | [[package]] 506 | name = "crossterm" 507 | version = "0.28.1" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 510 | dependencies = [ 511 | "bitflags 2.9.0", 512 | "crossterm_winapi", 513 | "mio 1.0.3", 514 | "parking_lot", 515 | "rustix 0.38.44", 516 | "signal-hook", 517 | "signal-hook-mio", 518 | "winapi", 519 | ] 520 | 521 | [[package]] 522 | name = "crossterm" 523 | version = "0.29.0" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" 526 | dependencies = [ 527 | "bitflags 2.9.0", 528 | "crossterm_winapi", 529 | "derive_more", 530 | "document-features", 531 | "mio 1.0.3", 532 | "parking_lot", 533 | "rustix 1.0.7", 534 | "signal-hook", 535 | "signal-hook-mio", 536 | "winapi", 537 | ] 538 | 539 | [[package]] 540 | name = "crossterm_winapi" 541 | version = "0.9.1" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 544 | dependencies = [ 545 | "winapi", 546 | ] 547 | 548 | [[package]] 549 | name = "dasp_envelope" 550 | version = "0.11.0" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "8ec617ce7016f101a87fe85ed44180839744265fae73bb4aa43e7ece1b7668b6" 553 | dependencies = [ 554 | "dasp_frame", 555 | "dasp_peak", 556 | "dasp_ring_buffer", 557 | "dasp_rms", 558 | "dasp_sample", 559 | ] 560 | 561 | [[package]] 562 | name = "dasp_frame" 563 | version = "0.11.0" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6" 566 | dependencies = [ 567 | "dasp_sample", 568 | ] 569 | 570 | [[package]] 571 | name = "dasp_interpolate" 572 | version = "0.11.0" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "7fc975a6563bb7ca7ec0a6c784ead49983a21c24835b0bc96eea11ee407c7486" 575 | dependencies = [ 576 | "dasp_frame", 577 | "dasp_ring_buffer", 578 | "dasp_sample", 579 | ] 580 | 581 | [[package]] 582 | name = "dasp_peak" 583 | version = "0.11.0" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "5cf88559d79c21f3d8523d91250c397f9a15b5fc72fbb3f87fdb0a37b79915bf" 586 | dependencies = [ 587 | "dasp_frame", 588 | "dasp_sample", 589 | ] 590 | 591 | [[package]] 592 | name = "dasp_ring_buffer" 593 | version = "0.11.0" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "07d79e19b89618a543c4adec9c5a347fe378a19041699b3278e616e387511ea1" 596 | 597 | [[package]] 598 | name = "dasp_rms" 599 | version = "0.11.0" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "a6c5dcb30b7e5014486e2822537ea2beae50b19722ffe2ed7549ab03774575aa" 602 | dependencies = [ 603 | "dasp_frame", 604 | "dasp_ring_buffer", 605 | "dasp_sample", 606 | ] 607 | 608 | [[package]] 609 | name = "dasp_sample" 610 | version = "0.11.0" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" 613 | 614 | [[package]] 615 | name = "dasp_signal" 616 | version = "0.11.0" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "aa1ab7d01689c6ed4eae3d38fe1cea08cba761573fbd2d592528d55b421077e7" 619 | dependencies = [ 620 | "dasp_envelope", 621 | "dasp_frame", 622 | "dasp_interpolate", 623 | "dasp_peak", 624 | "dasp_ring_buffer", 625 | "dasp_rms", 626 | "dasp_sample", 627 | "dasp_window", 628 | ] 629 | 630 | [[package]] 631 | name = "dasp_window" 632 | version = "0.11.1" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "99ded7b88821d2ce4e8b842c9f1c86ac911891ab89443cc1de750cae764c5076" 635 | dependencies = [ 636 | "dasp_sample", 637 | ] 638 | 639 | [[package]] 640 | name = "derive_more" 641 | version = "2.0.1" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 644 | dependencies = [ 645 | "derive_more-impl", 646 | ] 647 | 648 | [[package]] 649 | name = "derive_more-impl" 650 | version = "2.0.1" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 653 | dependencies = [ 654 | "convert_case", 655 | "proc-macro2", 656 | "quote", 657 | "syn 2.0.49", 658 | ] 659 | 660 | [[package]] 661 | name = "document-features" 662 | version = "0.2.11" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" 665 | dependencies = [ 666 | "litrs", 667 | ] 668 | 669 | [[package]] 670 | name = "dyn-clone" 671 | version = "1.0.17" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" 674 | 675 | [[package]] 676 | name = "either" 677 | version = "1.10.0" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" 680 | 681 | [[package]] 682 | name = "equivalent" 683 | version = "1.0.1" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 686 | 687 | [[package]] 688 | name = "errno" 689 | version = "0.3.11" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 692 | dependencies = [ 693 | "libc", 694 | "windows-sys 0.52.0", 695 | ] 696 | 697 | [[package]] 698 | name = "fuzzy-matcher" 699 | version = "0.3.7" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" 702 | dependencies = [ 703 | "thread_local", 704 | ] 705 | 706 | [[package]] 707 | name = "fxhash" 708 | version = "0.2.1" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 711 | dependencies = [ 712 | "byteorder", 713 | ] 714 | 715 | [[package]] 716 | name = "getrandom" 717 | version = "0.3.2" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 720 | dependencies = [ 721 | "cfg-if", 722 | "libc", 723 | "r-efi", 724 | "wasi 0.14.2+wasi-0.2.4", 725 | ] 726 | 727 | [[package]] 728 | name = "glob" 729 | version = "0.3.1" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 732 | 733 | [[package]] 734 | name = "hashbrown" 735 | version = "0.14.3" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 738 | dependencies = [ 739 | "ahash", 740 | "allocator-api2", 741 | ] 742 | 743 | [[package]] 744 | name = "heck" 745 | version = "0.5.0" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 748 | 749 | [[package]] 750 | name = "hound" 751 | version = "3.5.1" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" 754 | 755 | [[package]] 756 | name = "iana-time-zone" 757 | version = "0.1.60" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" 760 | dependencies = [ 761 | "android_system_properties", 762 | "core-foundation-sys", 763 | "iana-time-zone-haiku", 764 | "js-sys", 765 | "wasm-bindgen", 766 | "windows-core", 767 | ] 768 | 769 | [[package]] 770 | name = "iana-time-zone-haiku" 771 | version = "0.1.2" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 774 | dependencies = [ 775 | "cc", 776 | ] 777 | 778 | [[package]] 779 | name = "indexmap" 780 | version = "2.2.3" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" 783 | dependencies = [ 784 | "equivalent", 785 | "hashbrown", 786 | ] 787 | 788 | [[package]] 789 | name = "indoc" 790 | version = "2.0.4" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" 793 | 794 | [[package]] 795 | name = "inquire" 796 | version = "0.7.4" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "fe95f33091b9b7b517a5849bce4dce1b550b430fc20d58059fcaa319ed895d8b" 799 | dependencies = [ 800 | "bitflags 2.9.0", 801 | "crossterm 0.25.0", 802 | "dyn-clone", 803 | "fuzzy-matcher", 804 | "fxhash", 805 | "newline-converter", 806 | "once_cell", 807 | "unicode-segmentation", 808 | "unicode-width 0.1.11", 809 | ] 810 | 811 | [[package]] 812 | name = "instability" 813 | version = "0.3.2" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" 816 | dependencies = [ 817 | "quote", 818 | "syn 2.0.49", 819 | ] 820 | 821 | [[package]] 822 | name = "itertools" 823 | version = "0.12.1" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 826 | dependencies = [ 827 | "either", 828 | ] 829 | 830 | [[package]] 831 | name = "itertools" 832 | version = "0.13.0" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 835 | dependencies = [ 836 | "either", 837 | ] 838 | 839 | [[package]] 840 | name = "itoa" 841 | version = "1.0.10" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 844 | 845 | [[package]] 846 | name = "jack" 847 | version = "0.11.4" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "0e5a18a3c2aefb354fb77111ade228b20267bdc779de84e7a4ccf7ea96b9a6cd" 850 | dependencies = [ 851 | "bitflags 1.3.2", 852 | "jack-sys", 853 | "lazy_static", 854 | "libc", 855 | "log", 856 | ] 857 | 858 | [[package]] 859 | name = "jack-sys" 860 | version = "0.5.1" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "6013b7619b95a22b576dfb43296faa4ecbe40abbdb97dfd22ead520775fc86ab" 863 | dependencies = [ 864 | "bitflags 1.3.2", 865 | "lazy_static", 866 | "libc", 867 | "libloading 0.7.4", 868 | "log", 869 | "pkg-config", 870 | ] 871 | 872 | [[package]] 873 | name = "jni" 874 | version = "0.19.0" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" 877 | dependencies = [ 878 | "cesu8", 879 | "combine", 880 | "jni-sys", 881 | "log", 882 | "thiserror", 883 | "walkdir", 884 | ] 885 | 886 | [[package]] 887 | name = "jni" 888 | version = "0.20.0" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" 891 | dependencies = [ 892 | "cesu8", 893 | "combine", 894 | "jni-sys", 895 | "log", 896 | "thiserror", 897 | "walkdir", 898 | ] 899 | 900 | [[package]] 901 | name = "jni-sys" 902 | version = "0.3.0" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" 905 | 906 | [[package]] 907 | name = "jobserver" 908 | version = "0.1.28" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" 911 | dependencies = [ 912 | "libc", 913 | ] 914 | 915 | [[package]] 916 | name = "js-sys" 917 | version = "0.3.68" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" 920 | dependencies = [ 921 | "wasm-bindgen", 922 | ] 923 | 924 | [[package]] 925 | name = "lazy_static" 926 | version = "1.4.0" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 929 | 930 | [[package]] 931 | name = "lazycell" 932 | version = "1.3.0" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 935 | 936 | [[package]] 937 | name = "libc" 938 | version = "0.2.172" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 941 | 942 | [[package]] 943 | name = "libloading" 944 | version = "0.7.4" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" 947 | dependencies = [ 948 | "cfg-if", 949 | "winapi", 950 | ] 951 | 952 | [[package]] 953 | name = "libloading" 954 | version = "0.8.1" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" 957 | dependencies = [ 958 | "cfg-if", 959 | "windows-sys 0.48.0", 960 | ] 961 | 962 | [[package]] 963 | name = "linux-raw-sys" 964 | version = "0.4.15" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 967 | 968 | [[package]] 969 | name = "linux-raw-sys" 970 | version = "0.9.4" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 973 | 974 | [[package]] 975 | name = "litrs" 976 | version = "0.4.1" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" 979 | 980 | [[package]] 981 | name = "lock_api" 982 | version = "0.4.11" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 985 | dependencies = [ 986 | "autocfg", 987 | "scopeguard", 988 | ] 989 | 990 | [[package]] 991 | name = "log" 992 | version = "0.4.20" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 995 | 996 | [[package]] 997 | name = "lru" 998 | version = "0.12.2" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "db2c024b41519440580066ba82aab04092b333e09066a5eb86c7c4890df31f22" 1001 | dependencies = [ 1002 | "hashbrown", 1003 | ] 1004 | 1005 | [[package]] 1006 | name = "mach2" 1007 | version = "0.4.2" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" 1010 | dependencies = [ 1011 | "libc", 1012 | ] 1013 | 1014 | [[package]] 1015 | name = "memchr" 1016 | version = "2.7.1" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 1019 | 1020 | [[package]] 1021 | name = "minimal-lexical" 1022 | version = "0.2.1" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 1025 | 1026 | [[package]] 1027 | name = "mio" 1028 | version = "0.8.10" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" 1031 | dependencies = [ 1032 | "libc", 1033 | "log", 1034 | "wasi 0.11.0+wasi-snapshot-preview1", 1035 | "windows-sys 0.48.0", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "mio" 1040 | version = "1.0.3" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 1043 | dependencies = [ 1044 | "libc", 1045 | "log", 1046 | "wasi 0.11.0+wasi-snapshot-preview1", 1047 | "windows-sys 0.52.0", 1048 | ] 1049 | 1050 | [[package]] 1051 | name = "ndk" 1052 | version = "0.7.0" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" 1055 | dependencies = [ 1056 | "bitflags 1.3.2", 1057 | "jni-sys", 1058 | "ndk-sys", 1059 | "num_enum", 1060 | "raw-window-handle", 1061 | "thiserror", 1062 | ] 1063 | 1064 | [[package]] 1065 | name = "ndk-context" 1066 | version = "0.1.1" 1067 | source = "registry+https://github.com/rust-lang/crates.io-index" 1068 | checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" 1069 | 1070 | [[package]] 1071 | name = "ndk-sys" 1072 | version = "0.4.1+23.1.7779620" 1073 | source = "registry+https://github.com/rust-lang/crates.io-index" 1074 | checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" 1075 | dependencies = [ 1076 | "jni-sys", 1077 | ] 1078 | 1079 | [[package]] 1080 | name = "newline-converter" 1081 | version = "0.3.0" 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" 1083 | checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" 1084 | dependencies = [ 1085 | "unicode-segmentation", 1086 | ] 1087 | 1088 | [[package]] 1089 | name = "nix" 1090 | version = "0.24.3" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" 1093 | dependencies = [ 1094 | "bitflags 1.3.2", 1095 | "cfg-if", 1096 | "libc", 1097 | ] 1098 | 1099 | [[package]] 1100 | name = "nom" 1101 | version = "7.1.3" 1102 | source = "registry+https://github.com/rust-lang/crates.io-index" 1103 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 1104 | dependencies = [ 1105 | "memchr", 1106 | "minimal-lexical", 1107 | ] 1108 | 1109 | [[package]] 1110 | name = "num-derive" 1111 | version = "0.3.3" 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" 1113 | checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" 1114 | dependencies = [ 1115 | "proc-macro2", 1116 | "quote", 1117 | "syn 1.0.109", 1118 | ] 1119 | 1120 | [[package]] 1121 | name = "num-traits" 1122 | version = "0.2.18" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" 1125 | dependencies = [ 1126 | "autocfg", 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "num_enum" 1131 | version = "0.5.11" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" 1134 | dependencies = [ 1135 | "num_enum_derive", 1136 | ] 1137 | 1138 | [[package]] 1139 | name = "num_enum_derive" 1140 | version = "0.5.11" 1141 | source = "registry+https://github.com/rust-lang/crates.io-index" 1142 | checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" 1143 | dependencies = [ 1144 | "proc-macro-crate", 1145 | "proc-macro2", 1146 | "quote", 1147 | "syn 1.0.109", 1148 | ] 1149 | 1150 | [[package]] 1151 | name = "oboe" 1152 | version = "0.5.0" 1153 | source = "registry+https://github.com/rust-lang/crates.io-index" 1154 | checksum = "8868cc237ee02e2d9618539a23a8d228b9bb3fc2e7a5b11eed3831de77c395d0" 1155 | dependencies = [ 1156 | "jni 0.20.0", 1157 | "ndk", 1158 | "ndk-context", 1159 | "num-derive", 1160 | "num-traits", 1161 | "oboe-sys", 1162 | ] 1163 | 1164 | [[package]] 1165 | name = "oboe-sys" 1166 | version = "0.5.0" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "7f44155e7fb718d3cfddcf70690b2b51ac4412f347cd9e4fbe511abe9cd7b5f2" 1169 | dependencies = [ 1170 | "cc", 1171 | ] 1172 | 1173 | [[package]] 1174 | name = "once_cell" 1175 | version = "1.19.0" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 1178 | 1179 | [[package]] 1180 | name = "parking_lot" 1181 | version = "0.12.1" 1182 | source = "registry+https://github.com/rust-lang/crates.io-index" 1183 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 1184 | dependencies = [ 1185 | "lock_api", 1186 | "parking_lot_core", 1187 | ] 1188 | 1189 | [[package]] 1190 | name = "parking_lot_core" 1191 | version = "0.9.9" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 1194 | dependencies = [ 1195 | "cfg-if", 1196 | "libc", 1197 | "redox_syscall", 1198 | "smallvec", 1199 | "windows-targets 0.48.5", 1200 | ] 1201 | 1202 | [[package]] 1203 | name = "paste" 1204 | version = "1.0.14" 1205 | source = "registry+https://github.com/rust-lang/crates.io-index" 1206 | checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" 1207 | 1208 | [[package]] 1209 | name = "pkg-config" 1210 | version = "0.3.30" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 1213 | 1214 | [[package]] 1215 | name = "ppv-lite86" 1216 | version = "0.2.17" 1217 | source = "registry+https://github.com/rust-lang/crates.io-index" 1218 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 1219 | 1220 | [[package]] 1221 | name = "proc-macro-crate" 1222 | version = "1.3.1" 1223 | source = "registry+https://github.com/rust-lang/crates.io-index" 1224 | checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" 1225 | dependencies = [ 1226 | "once_cell", 1227 | "toml_edit", 1228 | ] 1229 | 1230 | [[package]] 1231 | name = "proc-macro2" 1232 | version = "1.0.78" 1233 | source = "registry+https://github.com/rust-lang/crates.io-index" 1234 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 1235 | dependencies = [ 1236 | "unicode-ident", 1237 | ] 1238 | 1239 | [[package]] 1240 | name = "quote" 1241 | version = "1.0.35" 1242 | source = "registry+https://github.com/rust-lang/crates.io-index" 1243 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 1244 | dependencies = [ 1245 | "proc-macro2", 1246 | ] 1247 | 1248 | [[package]] 1249 | name = "r-efi" 1250 | version = "5.2.0" 1251 | source = "registry+https://github.com/rust-lang/crates.io-index" 1252 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 1253 | 1254 | [[package]] 1255 | name = "rand" 1256 | version = "0.9.1" 1257 | source = "registry+https://github.com/rust-lang/crates.io-index" 1258 | checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" 1259 | dependencies = [ 1260 | "rand_chacha", 1261 | "rand_core", 1262 | ] 1263 | 1264 | [[package]] 1265 | name = "rand_chacha" 1266 | version = "0.9.0" 1267 | source = "registry+https://github.com/rust-lang/crates.io-index" 1268 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 1269 | dependencies = [ 1270 | "ppv-lite86", 1271 | "rand_core", 1272 | ] 1273 | 1274 | [[package]] 1275 | name = "rand_core" 1276 | version = "0.9.3" 1277 | source = "registry+https://github.com/rust-lang/crates.io-index" 1278 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 1279 | dependencies = [ 1280 | "getrandom", 1281 | ] 1282 | 1283 | [[package]] 1284 | name = "ratatui" 1285 | version = "0.29.0" 1286 | source = "registry+https://github.com/rust-lang/crates.io-index" 1287 | checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" 1288 | dependencies = [ 1289 | "bitflags 2.9.0", 1290 | "cassowary", 1291 | "compact_str", 1292 | "crossterm 0.28.1", 1293 | "indoc", 1294 | "instability", 1295 | "itertools 0.13.0", 1296 | "lru", 1297 | "paste", 1298 | "strum", 1299 | "unicode-segmentation", 1300 | "unicode-truncate", 1301 | "unicode-width 0.2.0", 1302 | ] 1303 | 1304 | [[package]] 1305 | name = "raw-window-handle" 1306 | version = "0.5.2" 1307 | source = "registry+https://github.com/rust-lang/crates.io-index" 1308 | checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" 1309 | 1310 | [[package]] 1311 | name = "redox_syscall" 1312 | version = "0.4.1" 1313 | source = "registry+https://github.com/rust-lang/crates.io-index" 1314 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 1315 | dependencies = [ 1316 | "bitflags 1.3.2", 1317 | ] 1318 | 1319 | [[package]] 1320 | name = "regex" 1321 | version = "1.10.3" 1322 | source = "registry+https://github.com/rust-lang/crates.io-index" 1323 | checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" 1324 | dependencies = [ 1325 | "aho-corasick", 1326 | "memchr", 1327 | "regex-automata", 1328 | "regex-syntax", 1329 | ] 1330 | 1331 | [[package]] 1332 | name = "regex-automata" 1333 | version = "0.4.5" 1334 | source = "registry+https://github.com/rust-lang/crates.io-index" 1335 | checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" 1336 | dependencies = [ 1337 | "aho-corasick", 1338 | "memchr", 1339 | "regex-syntax", 1340 | ] 1341 | 1342 | [[package]] 1343 | name = "regex-syntax" 1344 | version = "0.8.2" 1345 | source = "registry+https://github.com/rust-lang/crates.io-index" 1346 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 1347 | 1348 | [[package]] 1349 | name = "roff" 1350 | version = "0.2.2" 1351 | source = "registry+https://github.com/rust-lang/crates.io-index" 1352 | checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" 1353 | 1354 | [[package]] 1355 | name = "rustc-hash" 1356 | version = "1.1.0" 1357 | source = "registry+https://github.com/rust-lang/crates.io-index" 1358 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 1359 | 1360 | [[package]] 1361 | name = "rustix" 1362 | version = "0.38.44" 1363 | source = "registry+https://github.com/rust-lang/crates.io-index" 1364 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 1365 | dependencies = [ 1366 | "bitflags 2.9.0", 1367 | "errno", 1368 | "libc", 1369 | "linux-raw-sys 0.4.15", 1370 | "windows-sys 0.52.0", 1371 | ] 1372 | 1373 | [[package]] 1374 | name = "rustix" 1375 | version = "1.0.7" 1376 | source = "registry+https://github.com/rust-lang/crates.io-index" 1377 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 1378 | dependencies = [ 1379 | "bitflags 2.9.0", 1380 | "errno", 1381 | "libc", 1382 | "linux-raw-sys 0.9.4", 1383 | "windows-sys 0.52.0", 1384 | ] 1385 | 1386 | [[package]] 1387 | name = "rustversion" 1388 | version = "1.0.14" 1389 | source = "registry+https://github.com/rust-lang/crates.io-index" 1390 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 1391 | 1392 | [[package]] 1393 | name = "ryu" 1394 | version = "1.0.17" 1395 | source = "registry+https://github.com/rust-lang/crates.io-index" 1396 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 1397 | 1398 | [[package]] 1399 | name = "same-file" 1400 | version = "1.0.6" 1401 | source = "registry+https://github.com/rust-lang/crates.io-index" 1402 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1403 | dependencies = [ 1404 | "winapi-util", 1405 | ] 1406 | 1407 | [[package]] 1408 | name = "scopeguard" 1409 | version = "1.2.0" 1410 | source = "registry+https://github.com/rust-lang/crates.io-index" 1411 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1412 | 1413 | [[package]] 1414 | name = "shlex" 1415 | version = "1.3.0" 1416 | source = "registry+https://github.com/rust-lang/crates.io-index" 1417 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1418 | 1419 | [[package]] 1420 | name = "signal-hook" 1421 | version = "0.3.17" 1422 | source = "registry+https://github.com/rust-lang/crates.io-index" 1423 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 1424 | dependencies = [ 1425 | "libc", 1426 | "signal-hook-registry", 1427 | ] 1428 | 1429 | [[package]] 1430 | name = "signal-hook-mio" 1431 | version = "0.2.4" 1432 | source = "registry+https://github.com/rust-lang/crates.io-index" 1433 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 1434 | dependencies = [ 1435 | "libc", 1436 | "mio 0.8.10", 1437 | "mio 1.0.3", 1438 | "signal-hook", 1439 | ] 1440 | 1441 | [[package]] 1442 | name = "signal-hook-registry" 1443 | version = "1.4.1" 1444 | source = "registry+https://github.com/rust-lang/crates.io-index" 1445 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 1446 | dependencies = [ 1447 | "libc", 1448 | ] 1449 | 1450 | [[package]] 1451 | name = "smallvec" 1452 | version = "1.13.1" 1453 | source = "registry+https://github.com/rust-lang/crates.io-index" 1454 | checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" 1455 | 1456 | [[package]] 1457 | name = "static_assertions" 1458 | version = "1.1.0" 1459 | source = "registry+https://github.com/rust-lang/crates.io-index" 1460 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1461 | 1462 | [[package]] 1463 | name = "strsim" 1464 | version = "0.11.0" 1465 | source = "registry+https://github.com/rust-lang/crates.io-index" 1466 | checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" 1467 | 1468 | [[package]] 1469 | name = "strum" 1470 | version = "0.26.3" 1471 | source = "registry+https://github.com/rust-lang/crates.io-index" 1472 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 1473 | dependencies = [ 1474 | "strum_macros", 1475 | ] 1476 | 1477 | [[package]] 1478 | name = "strum_macros" 1479 | version = "0.26.4" 1480 | source = "registry+https://github.com/rust-lang/crates.io-index" 1481 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 1482 | dependencies = [ 1483 | "heck", 1484 | "proc-macro2", 1485 | "quote", 1486 | "rustversion", 1487 | "syn 2.0.49", 1488 | ] 1489 | 1490 | [[package]] 1491 | name = "syn" 1492 | version = "1.0.109" 1493 | source = "registry+https://github.com/rust-lang/crates.io-index" 1494 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1495 | dependencies = [ 1496 | "proc-macro2", 1497 | "quote", 1498 | "unicode-ident", 1499 | ] 1500 | 1501 | [[package]] 1502 | name = "syn" 1503 | version = "2.0.49" 1504 | source = "registry+https://github.com/rust-lang/crates.io-index" 1505 | checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" 1506 | dependencies = [ 1507 | "proc-macro2", 1508 | "quote", 1509 | "unicode-ident", 1510 | ] 1511 | 1512 | [[package]] 1513 | name = "thiserror" 1514 | version = "1.0.57" 1515 | source = "registry+https://github.com/rust-lang/crates.io-index" 1516 | checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" 1517 | dependencies = [ 1518 | "thiserror-impl", 1519 | ] 1520 | 1521 | [[package]] 1522 | name = "thiserror-impl" 1523 | version = "1.0.57" 1524 | source = "registry+https://github.com/rust-lang/crates.io-index" 1525 | checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" 1526 | dependencies = [ 1527 | "proc-macro2", 1528 | "quote", 1529 | "syn 2.0.49", 1530 | ] 1531 | 1532 | [[package]] 1533 | name = "thread_local" 1534 | version = "1.1.8" 1535 | source = "registry+https://github.com/rust-lang/crates.io-index" 1536 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 1537 | dependencies = [ 1538 | "cfg-if", 1539 | "once_cell", 1540 | ] 1541 | 1542 | [[package]] 1543 | name = "toml_datetime" 1544 | version = "0.6.5" 1545 | source = "registry+https://github.com/rust-lang/crates.io-index" 1546 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 1547 | 1548 | [[package]] 1549 | name = "toml_edit" 1550 | version = "0.19.15" 1551 | source = "registry+https://github.com/rust-lang/crates.io-index" 1552 | checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" 1553 | dependencies = [ 1554 | "indexmap", 1555 | "toml_datetime", 1556 | "winnow", 1557 | ] 1558 | 1559 | [[package]] 1560 | name = "unicode-ident" 1561 | version = "1.0.12" 1562 | source = "registry+https://github.com/rust-lang/crates.io-index" 1563 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1564 | 1565 | [[package]] 1566 | name = "unicode-segmentation" 1567 | version = "1.11.0" 1568 | source = "registry+https://github.com/rust-lang/crates.io-index" 1569 | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 1570 | 1571 | [[package]] 1572 | name = "unicode-truncate" 1573 | version = "1.1.0" 1574 | source = "registry+https://github.com/rust-lang/crates.io-index" 1575 | checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" 1576 | dependencies = [ 1577 | "itertools 0.13.0", 1578 | "unicode-segmentation", 1579 | "unicode-width 0.1.11", 1580 | ] 1581 | 1582 | [[package]] 1583 | name = "unicode-width" 1584 | version = "0.1.11" 1585 | source = "registry+https://github.com/rust-lang/crates.io-index" 1586 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 1587 | 1588 | [[package]] 1589 | name = "unicode-width" 1590 | version = "0.2.0" 1591 | source = "registry+https://github.com/rust-lang/crates.io-index" 1592 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1593 | 1594 | [[package]] 1595 | name = "utf8parse" 1596 | version = "0.2.1" 1597 | source = "registry+https://github.com/rust-lang/crates.io-index" 1598 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1599 | 1600 | [[package]] 1601 | name = "version_check" 1602 | version = "0.9.4" 1603 | source = "registry+https://github.com/rust-lang/crates.io-index" 1604 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1605 | 1606 | [[package]] 1607 | name = "walkdir" 1608 | version = "2.4.0" 1609 | source = "registry+https://github.com/rust-lang/crates.io-index" 1610 | checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" 1611 | dependencies = [ 1612 | "same-file", 1613 | "winapi-util", 1614 | ] 1615 | 1616 | [[package]] 1617 | name = "wasi" 1618 | version = "0.11.0+wasi-snapshot-preview1" 1619 | source = "registry+https://github.com/rust-lang/crates.io-index" 1620 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1621 | 1622 | [[package]] 1623 | name = "wasi" 1624 | version = "0.14.2+wasi-0.2.4" 1625 | source = "registry+https://github.com/rust-lang/crates.io-index" 1626 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1627 | dependencies = [ 1628 | "wit-bindgen-rt", 1629 | ] 1630 | 1631 | [[package]] 1632 | name = "wasm-bindgen" 1633 | version = "0.2.91" 1634 | source = "registry+https://github.com/rust-lang/crates.io-index" 1635 | checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" 1636 | dependencies = [ 1637 | "cfg-if", 1638 | "wasm-bindgen-macro", 1639 | ] 1640 | 1641 | [[package]] 1642 | name = "wasm-bindgen-backend" 1643 | version = "0.2.91" 1644 | source = "registry+https://github.com/rust-lang/crates.io-index" 1645 | checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" 1646 | dependencies = [ 1647 | "bumpalo", 1648 | "log", 1649 | "once_cell", 1650 | "proc-macro2", 1651 | "quote", 1652 | "syn 2.0.49", 1653 | "wasm-bindgen-shared", 1654 | ] 1655 | 1656 | [[package]] 1657 | name = "wasm-bindgen-futures" 1658 | version = "0.4.41" 1659 | source = "registry+https://github.com/rust-lang/crates.io-index" 1660 | checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" 1661 | dependencies = [ 1662 | "cfg-if", 1663 | "js-sys", 1664 | "wasm-bindgen", 1665 | "web-sys", 1666 | ] 1667 | 1668 | [[package]] 1669 | name = "wasm-bindgen-macro" 1670 | version = "0.2.91" 1671 | source = "registry+https://github.com/rust-lang/crates.io-index" 1672 | checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" 1673 | dependencies = [ 1674 | "quote", 1675 | "wasm-bindgen-macro-support", 1676 | ] 1677 | 1678 | [[package]] 1679 | name = "wasm-bindgen-macro-support" 1680 | version = "0.2.91" 1681 | source = "registry+https://github.com/rust-lang/crates.io-index" 1682 | checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" 1683 | dependencies = [ 1684 | "proc-macro2", 1685 | "quote", 1686 | "syn 2.0.49", 1687 | "wasm-bindgen-backend", 1688 | "wasm-bindgen-shared", 1689 | ] 1690 | 1691 | [[package]] 1692 | name = "wasm-bindgen-shared" 1693 | version = "0.2.91" 1694 | source = "registry+https://github.com/rust-lang/crates.io-index" 1695 | checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" 1696 | 1697 | [[package]] 1698 | name = "web-sys" 1699 | version = "0.3.68" 1700 | source = "registry+https://github.com/rust-lang/crates.io-index" 1701 | checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" 1702 | dependencies = [ 1703 | "js-sys", 1704 | "wasm-bindgen", 1705 | ] 1706 | 1707 | [[package]] 1708 | name = "winapi" 1709 | version = "0.3.9" 1710 | source = "registry+https://github.com/rust-lang/crates.io-index" 1711 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1712 | dependencies = [ 1713 | "winapi-i686-pc-windows-gnu", 1714 | "winapi-x86_64-pc-windows-gnu", 1715 | ] 1716 | 1717 | [[package]] 1718 | name = "winapi-i686-pc-windows-gnu" 1719 | version = "0.4.0" 1720 | source = "registry+https://github.com/rust-lang/crates.io-index" 1721 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1722 | 1723 | [[package]] 1724 | name = "winapi-util" 1725 | version = "0.1.6" 1726 | source = "registry+https://github.com/rust-lang/crates.io-index" 1727 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 1728 | dependencies = [ 1729 | "winapi", 1730 | ] 1731 | 1732 | [[package]] 1733 | name = "winapi-x86_64-pc-windows-gnu" 1734 | version = "0.4.0" 1735 | source = "registry+https://github.com/rust-lang/crates.io-index" 1736 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1737 | 1738 | [[package]] 1739 | name = "windows" 1740 | version = "0.46.0" 1741 | source = "registry+https://github.com/rust-lang/crates.io-index" 1742 | checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25" 1743 | dependencies = [ 1744 | "windows-targets 0.42.2", 1745 | ] 1746 | 1747 | [[package]] 1748 | name = "windows-core" 1749 | version = "0.52.0" 1750 | source = "registry+https://github.com/rust-lang/crates.io-index" 1751 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1752 | dependencies = [ 1753 | "windows-targets 0.52.0", 1754 | ] 1755 | 1756 | [[package]] 1757 | name = "windows-sys" 1758 | version = "0.48.0" 1759 | source = "registry+https://github.com/rust-lang/crates.io-index" 1760 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1761 | dependencies = [ 1762 | "windows-targets 0.48.5", 1763 | ] 1764 | 1765 | [[package]] 1766 | name = "windows-sys" 1767 | version = "0.52.0" 1768 | source = "registry+https://github.com/rust-lang/crates.io-index" 1769 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1770 | dependencies = [ 1771 | "windows-targets 0.52.0", 1772 | ] 1773 | 1774 | [[package]] 1775 | name = "windows-targets" 1776 | version = "0.42.2" 1777 | source = "registry+https://github.com/rust-lang/crates.io-index" 1778 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 1779 | dependencies = [ 1780 | "windows_aarch64_gnullvm 0.42.2", 1781 | "windows_aarch64_msvc 0.42.2", 1782 | "windows_i686_gnu 0.42.2", 1783 | "windows_i686_msvc 0.42.2", 1784 | "windows_x86_64_gnu 0.42.2", 1785 | "windows_x86_64_gnullvm 0.42.2", 1786 | "windows_x86_64_msvc 0.42.2", 1787 | ] 1788 | 1789 | [[package]] 1790 | name = "windows-targets" 1791 | version = "0.48.5" 1792 | source = "registry+https://github.com/rust-lang/crates.io-index" 1793 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1794 | dependencies = [ 1795 | "windows_aarch64_gnullvm 0.48.5", 1796 | "windows_aarch64_msvc 0.48.5", 1797 | "windows_i686_gnu 0.48.5", 1798 | "windows_i686_msvc 0.48.5", 1799 | "windows_x86_64_gnu 0.48.5", 1800 | "windows_x86_64_gnullvm 0.48.5", 1801 | "windows_x86_64_msvc 0.48.5", 1802 | ] 1803 | 1804 | [[package]] 1805 | name = "windows-targets" 1806 | version = "0.52.0" 1807 | source = "registry+https://github.com/rust-lang/crates.io-index" 1808 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 1809 | dependencies = [ 1810 | "windows_aarch64_gnullvm 0.52.0", 1811 | "windows_aarch64_msvc 0.52.0", 1812 | "windows_i686_gnu 0.52.0", 1813 | "windows_i686_msvc 0.52.0", 1814 | "windows_x86_64_gnu 0.52.0", 1815 | "windows_x86_64_gnullvm 0.52.0", 1816 | "windows_x86_64_msvc 0.52.0", 1817 | ] 1818 | 1819 | [[package]] 1820 | name = "windows_aarch64_gnullvm" 1821 | version = "0.42.2" 1822 | source = "registry+https://github.com/rust-lang/crates.io-index" 1823 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 1824 | 1825 | [[package]] 1826 | name = "windows_aarch64_gnullvm" 1827 | version = "0.48.5" 1828 | source = "registry+https://github.com/rust-lang/crates.io-index" 1829 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1830 | 1831 | [[package]] 1832 | name = "windows_aarch64_gnullvm" 1833 | version = "0.52.0" 1834 | source = "registry+https://github.com/rust-lang/crates.io-index" 1835 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 1836 | 1837 | [[package]] 1838 | name = "windows_aarch64_msvc" 1839 | version = "0.42.2" 1840 | source = "registry+https://github.com/rust-lang/crates.io-index" 1841 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 1842 | 1843 | [[package]] 1844 | name = "windows_aarch64_msvc" 1845 | version = "0.48.5" 1846 | source = "registry+https://github.com/rust-lang/crates.io-index" 1847 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1848 | 1849 | [[package]] 1850 | name = "windows_aarch64_msvc" 1851 | version = "0.52.0" 1852 | source = "registry+https://github.com/rust-lang/crates.io-index" 1853 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 1854 | 1855 | [[package]] 1856 | name = "windows_i686_gnu" 1857 | version = "0.42.2" 1858 | source = "registry+https://github.com/rust-lang/crates.io-index" 1859 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 1860 | 1861 | [[package]] 1862 | name = "windows_i686_gnu" 1863 | version = "0.48.5" 1864 | source = "registry+https://github.com/rust-lang/crates.io-index" 1865 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1866 | 1867 | [[package]] 1868 | name = "windows_i686_gnu" 1869 | version = "0.52.0" 1870 | source = "registry+https://github.com/rust-lang/crates.io-index" 1871 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 1872 | 1873 | [[package]] 1874 | name = "windows_i686_msvc" 1875 | version = "0.42.2" 1876 | source = "registry+https://github.com/rust-lang/crates.io-index" 1877 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 1878 | 1879 | [[package]] 1880 | name = "windows_i686_msvc" 1881 | version = "0.48.5" 1882 | source = "registry+https://github.com/rust-lang/crates.io-index" 1883 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1884 | 1885 | [[package]] 1886 | name = "windows_i686_msvc" 1887 | version = "0.52.0" 1888 | source = "registry+https://github.com/rust-lang/crates.io-index" 1889 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 1890 | 1891 | [[package]] 1892 | name = "windows_x86_64_gnu" 1893 | version = "0.42.2" 1894 | source = "registry+https://github.com/rust-lang/crates.io-index" 1895 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 1896 | 1897 | [[package]] 1898 | name = "windows_x86_64_gnu" 1899 | version = "0.48.5" 1900 | source = "registry+https://github.com/rust-lang/crates.io-index" 1901 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1902 | 1903 | [[package]] 1904 | name = "windows_x86_64_gnu" 1905 | version = "0.52.0" 1906 | source = "registry+https://github.com/rust-lang/crates.io-index" 1907 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 1908 | 1909 | [[package]] 1910 | name = "windows_x86_64_gnullvm" 1911 | version = "0.42.2" 1912 | source = "registry+https://github.com/rust-lang/crates.io-index" 1913 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 1914 | 1915 | [[package]] 1916 | name = "windows_x86_64_gnullvm" 1917 | version = "0.48.5" 1918 | source = "registry+https://github.com/rust-lang/crates.io-index" 1919 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1920 | 1921 | [[package]] 1922 | name = "windows_x86_64_gnullvm" 1923 | version = "0.52.0" 1924 | source = "registry+https://github.com/rust-lang/crates.io-index" 1925 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 1926 | 1927 | [[package]] 1928 | name = "windows_x86_64_msvc" 1929 | version = "0.42.2" 1930 | source = "registry+https://github.com/rust-lang/crates.io-index" 1931 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 1932 | 1933 | [[package]] 1934 | name = "windows_x86_64_msvc" 1935 | version = "0.48.5" 1936 | source = "registry+https://github.com/rust-lang/crates.io-index" 1937 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1938 | 1939 | [[package]] 1940 | name = "windows_x86_64_msvc" 1941 | version = "0.52.0" 1942 | source = "registry+https://github.com/rust-lang/crates.io-index" 1943 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 1944 | 1945 | [[package]] 1946 | name = "winnow" 1947 | version = "0.5.40" 1948 | source = "registry+https://github.com/rust-lang/crates.io-index" 1949 | checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" 1950 | dependencies = [ 1951 | "memchr", 1952 | ] 1953 | 1954 | [[package]] 1955 | name = "wit-bindgen-rt" 1956 | version = "0.39.0" 1957 | source = "registry+https://github.com/rust-lang/crates.io-index" 1958 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1959 | dependencies = [ 1960 | "bitflags 2.9.0", 1961 | ] 1962 | 1963 | [[package]] 1964 | name = "zerocopy" 1965 | version = "0.7.32" 1966 | source = "registry+https://github.com/rust-lang/crates.io-index" 1967 | checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" 1968 | dependencies = [ 1969 | "zerocopy-derive", 1970 | ] 1971 | 1972 | [[package]] 1973 | name = "zerocopy-derive" 1974 | version = "0.7.32" 1975 | source = "registry+https://github.com/rust-lang/crates.io-index" 1976 | checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" 1977 | dependencies = [ 1978 | "proc-macro2", 1979 | "quote", 1980 | "syn 2.0.49", 1981 | ] 1982 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "asak" 3 | version = "0.3.6" 4 | edition = "2021" 5 | keywords = ["audio", "music", "DSP", "synth", "synthesizer"] 6 | readme = "README.md" 7 | license-file = "LICENSE" 8 | description = "A cross-platform audio recording/playback CLI tool with TUI" 9 | authors = ["Qichao Lan "] 10 | repository = "https://github.com/chaosprint/asak.git" 11 | 12 | [dependencies] 13 | anyhow = "1.0.80" 14 | chrono = "0.4.35" 15 | clap = { version = "4.5.1", features = ["derive"] } 16 | colored = "3.0.0" 17 | cpal = { version = "0.15.2", features = ["jack"], optional = true } 18 | crossbeam = "0.8.4" 19 | crossterm = "0.29.0" 20 | dasp_interpolate = { version = "0.11.0", features = ["linear"] } 21 | dasp_ring_buffer = "0.11.0" 22 | dasp_signal = "0.11.0" 23 | hound = "3.5.1" 24 | inquire = "0.7.4" 25 | parking_lot = "0.12.1" 26 | rand = "0.9.1" 27 | ratatui = "0.29.0" 28 | smallvec = "1.13.1" 29 | 30 | [build-dependencies] 31 | clap = { version = "4.5.4", features = ["derive"] } 32 | clap_complete = "4.5.2" 33 | clap_mangen = "0.2.20" 34 | 35 | [features] 36 | default = ["cpal"] 37 | jack = ["cpal/jack"] 38 | 39 | # The profile that 'cargo dist' will build with 40 | [profile.dist] 41 | inherits = "release" 42 | lto = "thin" 43 | 44 | # Config for 'cargo dist' 45 | [workspace.metadata.dist] 46 | # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) 47 | cargo-dist-version = "0.16.0" 48 | # CI backends to support 49 | ci = "github" 50 | # The installers to generate for each app 51 | installers = ["shell"] 52 | # Target platforms to build apps for (Rust target-triple syntax) 53 | targets = [ 54 | "aarch64-apple-darwin", 55 | "x86_64-apple-darwin", 56 | "x86_64-unknown-linux-gnu", 57 | "x86_64-pc-windows-msvc" 58 | ] 59 | # Publish jobs to run in CI 60 | pr-run-mode = "plan" 61 | # Whether to install an updater program 62 | install-updater = false 63 | 64 | [package.metadata.dist.dependencies.apt] 65 | libasound2-dev = '*' 66 | libjack-dev = '*' 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024-present Qichao Lan (chaopsrint) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # asak 2 | 3 | A cross-platform audio recording/playback CLI tool with TUI, written in Rust. The goal is to be an audio Swiss Army Knife (asak), like SoX but more interactive and fun. 4 | 5 | ![Asak](./asak.gif) 6 | 7 | ## install 8 | 9 | > You need to have `cargo` installed, see [here](https://doc.rust-lang.org/cargo/getting-started/installation.html). 10 | 11 | ### step 1 12 | 13 | ```sh 14 | cargo install asak 15 | ``` 16 | 17 | Note: Make sure the [JACK Audio Connection Kit](https://jackaudio.org) is installed on your machine prior to installing `asak`. For instance, on Ubuntu/Mint, if nothing is returned when running `sudo dpkg -l | grep libjack`, you will need to `sudo apt install libjack-dev`. 18 | 19 | ### step 2 20 | 21 | ```sh 22 | asak --help 23 | ``` 24 | 25 | ## usage 26 | 27 | ### record 28 | 29 | ```sh 30 | asak rec hello 31 | ``` 32 | 33 | > If no output name is provided, a prompt will come for you to input output file name. UTC format such as `2024-04-14T09:17:40Z.wav` will be provided as initial file name. 34 | 35 | ### playback 36 | 37 | ```sh 38 | asak play hello.wav 39 | ``` 40 | 41 | > If no input name is provided, it will search current directory for `.wav` files and open an interactive menu. 42 | 43 | ### monitor 44 | 45 | ```sh 46 | asak monitor 47 | ``` 48 | 49 | > Reminder: ⚠️ Watch your volume when play the video below❗️ 50 | 51 | https://github.com/chaosprint/asak/assets/35621141/f0876503-4dc7-4c92-b324-c36ec5b747d0 52 | 53 | 54 | 55 | > Known issue: you need to select the same output device as the one in your current system settings. 56 | 57 | ## roadmap? 58 | 59 | - [x] record audio 60 | - [x] basic audio playback 61 | - [x] monitoring an input device with an output device 62 | - [ ] rec device, dur, sr, ch, fmt 63 | - [ ] play device, dur, sr, ch, fmt 64 | - [ ] playback live pos control 65 | - [ ] live amp + fx (reverb, delay, etc) 66 | - [ ] passthru + live fx 67 | 68 | ## contribution 69 | 70 | Just open an issue or PR, I'm happy to discuss and collaborate. 71 | -------------------------------------------------------------------------------- /asak.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaosprint/asak/70b3cc7a5dfcc1d1b30223f1e8463b593aa723a8/asak.gif -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | include!("src/cli.rs"); 2 | 3 | use clap::Command; 4 | use clap::CommandFactory; 5 | use clap_complete::generate_to; 6 | use clap_complete::Shell::{Bash, Fish, Zsh}; 7 | use clap_mangen::Man; 8 | use std::fs; 9 | use std::path::PathBuf; 10 | 11 | static NAME: &str = "asak"; 12 | 13 | fn generate_man_pages(cmd: Command) { 14 | let man_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/man"); 15 | let mut buffer = Vec::default(); 16 | 17 | Man::new(cmd.clone()).render(&mut buffer).unwrap(); 18 | fs::create_dir_all(&man_dir).unwrap(); 19 | fs::write(man_dir.join(NAME.to_owned() + ".1"), buffer).unwrap(); 20 | 21 | for subcommand in cmd.get_subcommands() { 22 | let mut buffer = Vec::default(); 23 | 24 | Man::new(subcommand.clone()).render(&mut buffer).unwrap(); 25 | fs::write( 26 | man_dir.join(NAME.to_owned() + "-" + subcommand.get_name() + ".1"), 27 | buffer, 28 | ) 29 | .unwrap(); 30 | } 31 | } 32 | 33 | fn generate_shell_completions(mut cmd: Command) { 34 | let comp_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/completions"); 35 | 36 | fs::create_dir_all(&comp_dir).unwrap(); 37 | 38 | for shell in [Bash, Fish, Zsh] { 39 | generate_to(shell, &mut cmd, NAME, &comp_dir).unwrap(); 40 | } 41 | } 42 | 43 | fn main() { 44 | let mut cmd = Cli::command(); 45 | cmd.set_bin_name(NAME); 46 | 47 | generate_man_pages(cmd.clone()); 48 | generate_shell_completions(cmd); 49 | } 50 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args, Parser, Subcommand}; 2 | 3 | /// Audio Swiss Army knife written in Rust. Like Sox but interactive with TUI. 4 | #[derive(Parser, Debug)] 5 | #[command(author, version, about, long_about = None)] 6 | pub struct Cli { 7 | #[command(subcommand)] 8 | pub command: Commands, 9 | 10 | /// Use the JACK host 11 | #[cfg(all( 12 | any( 13 | target_os = "linux", 14 | target_os = "dragonfly", 15 | target_os = "freebsd", 16 | target_os = "netbsd" 17 | ), 18 | feature = "jack" 19 | ))] 20 | #[arg(short, long)] 21 | #[allow(dead_code)] 22 | pub jack: bool, 23 | } 24 | 25 | #[derive(Debug, Subcommand)] 26 | pub enum Commands { 27 | /// Record an audio file 28 | Rec(RecArgs), 29 | /// Play an audio file 30 | Play(PlayArgs), 31 | /// Monitor audio input with scopes 32 | Monitor(MonitorArgs), 33 | /// List available audio devices 34 | List, 35 | } 36 | 37 | /// Arguments used for the `rec` command 38 | #[derive(Args, Debug)] 39 | pub struct RecArgs { 40 | /// Path for the output audio file, e.g. `output` 41 | #[arg(required = false)] 42 | pub output: Option, 43 | /// The audio device index to use 44 | #[arg(required = false, short, long)] 45 | pub device: Option, 46 | } 47 | 48 | /// Arguments used for the `play` command 49 | #[derive(Args, Debug)] 50 | pub struct PlayArgs { 51 | /// Path to the audio file to play; must be wav format for now, e.g. `input.wav` 52 | #[arg(required = false)] 53 | pub input: Option, 54 | /// The audio device index to use 55 | #[arg(required = false, short, long)] 56 | pub device: Option, 57 | } 58 | 59 | /// Arguments used for the `monitor` command 60 | #[derive(Args, Debug)] 61 | pub struct MonitorArgs { 62 | /// Buffer size for the audio input monitoring, defaults to 1024, the higher the value the more latency 63 | #[arg(required = false, short, long)] 64 | pub buffer_size: Option, 65 | } 66 | -------------------------------------------------------------------------------- /src/device.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaosprint/asak/70b3cc7a5dfcc1d1b30223f1e8463b593aa723a8/src/device.rs -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use colored::*; 3 | 4 | mod record; 5 | use cpal::traits::{DeviceTrait, HostTrait}; 6 | use inquire::{InquireError, Select, Text}; 7 | use record::record_audio; 8 | 9 | mod playback; 10 | use playback::play_audio; 11 | 12 | mod monitor; 13 | use monitor::start_monitoring; 14 | 15 | mod cli; 16 | use cli::{Cli, Commands}; 17 | 18 | fn main() { 19 | let cli = Cli::parse(); 20 | 21 | match &cli.command { 22 | Commands::Rec(args) => { 23 | // Pass the respective JACK usage flag to play_audio based on compile-time detection 24 | #[cfg(all( 25 | any( 26 | target_os = "linux", 27 | target_os = "dragonfly", 28 | target_os = "freebsd", 29 | target_os = "netbsd" 30 | ), 31 | feature = "jack" 32 | ))] 33 | { 34 | // If we're on the right platform and JACK is enabled, pass true to use JACK for playback 35 | 36 | match &args.output { 37 | Some(output) => { 38 | record_audio(output.clone(), args.device, false).unwrap(); 39 | } 40 | None => { 41 | let now = chrono::Utc::now(); 42 | let name = format!( 43 | "{}.wav", 44 | now.to_rfc3339_opts(chrono::SecondsFormat::Secs, true) 45 | ); 46 | // let output = Text::new("What is your name?").placeholder(name).prompt(); 47 | let output = Text { 48 | initial_value: Some(&name), 49 | ..Text::new("Please enter the output wav file name:") 50 | } 51 | .prompt(); 52 | match output { 53 | Ok(output) => record_audio(output, args.device, cli.jack).unwrap(), 54 | Err(_) => println!("Recording cancelled."), 55 | } 56 | } 57 | }; 58 | } 59 | #[cfg(not(all( 60 | any( 61 | target_os = "linux", 62 | target_os = "dragonfly", 63 | target_os = "freebsd", 64 | target_os = "netbsd" 65 | ), 66 | feature = "jack" 67 | )))] 68 | { 69 | // If JACK is not available or the platform is unsupported, pass false to not use JACK 70 | match &args.output { 71 | Some(output) => { 72 | record_audio(output.clone(), args.device, false).unwrap(); 73 | } 74 | None => { 75 | let now = chrono::Utc::now(); 76 | let name = format!( 77 | "{}.wav", 78 | now.to_rfc3339_opts(chrono::SecondsFormat::Secs, true) 79 | ); 80 | // let output = Text::new("What is your name?").placeholder(name).prompt(); 81 | let output = Text { 82 | initial_value: Some(&name), 83 | ..Text::new("Please enter the output wav file name:") 84 | } 85 | .prompt(); 86 | match output { 87 | Ok(output) => record_audio(output, args.device, false).unwrap(), 88 | Err(_) => println!("Recording cancelled."), 89 | } 90 | } 91 | }; 92 | } 93 | } 94 | Commands::Play(args) => { 95 | // Pass the respective JACK usage flag to play_audio based on compile-time detection 96 | #[cfg(all( 97 | any( 98 | target_os = "linux", 99 | target_os = "dragonfly", 100 | target_os = "freebsd", 101 | target_os = "netbsd" 102 | ), 103 | feature = "jack" 104 | ))] 105 | { 106 | // If we're on the right platform and JACK is enabled, pass true to use JACK for playback 107 | match &args.input { 108 | Some(input) => play_audio(input, args.device, false).unwrap(), 109 | None => { 110 | let mut options: Vec = vec![]; 111 | // check current directory for wav files 112 | let files = std::fs::read_dir(".").unwrap(); 113 | for file in files { 114 | let file = file.unwrap(); 115 | let path = file.path().clone(); 116 | let path = path.to_str().unwrap(); 117 | if path.ends_with(".wav") { 118 | options.push(path.into()); 119 | } 120 | } 121 | if options.is_empty() { 122 | println!("No wav files found in current directory"); 123 | } else { 124 | let ans: Result = 125 | Select::new("Select a wav file to play", options).prompt(); 126 | match ans { 127 | Ok(input) => play_audio(&input, args.device, cli.jack).unwrap(), 128 | Err(_) => println!("Playback cancelled."), 129 | } 130 | } 131 | } 132 | } 133 | } 134 | #[cfg(not(all( 135 | any( 136 | target_os = "linux", 137 | target_os = "dragonfly", 138 | target_os = "freebsd", 139 | target_os = "netbsd" 140 | ), 141 | feature = "jack" 142 | )))] 143 | { 144 | // If JACK is not available or the platform is unsupported, pass false to not use JACK 145 | match &args.input { 146 | Some(input) => play_audio(input, args.device, false).unwrap(), 147 | None => { 148 | let mut options: Vec = vec![]; 149 | // check current directory for wav files 150 | let files = std::fs::read_dir(".").unwrap(); 151 | for file in files { 152 | let file = file.unwrap(); 153 | let path = file.path().clone(); 154 | let path = path.to_str().unwrap(); 155 | if path.ends_with(".wav") { 156 | options.push(path.into()); 157 | } 158 | } 159 | if options.is_empty() { 160 | println!("No wav files found in current directory"); 161 | } else { 162 | let ans: Result = 163 | Select::new("Select a wav file to play", options).prompt(); 164 | match ans { 165 | Ok(input) => play_audio(&input, args.device, false).unwrap(), 166 | Err(_) => println!("Playback cancelled."), 167 | } 168 | } 169 | } 170 | } 171 | } 172 | } 173 | Commands::Monitor(args) => { 174 | let buffer_size = args.buffer_size.unwrap_or(1024); 175 | start_monitoring(buffer_size).unwrap(); 176 | } 177 | Commands::List => { 178 | let host = cpal::default_host(); 179 | let in_devices = host.input_devices().unwrap(); 180 | let out_devices = host.output_devices().unwrap(); 181 | 182 | println!("\n{}", "Available Audio Devices".bold().underline()); 183 | println!("\n{}", "Usage:".yellow()); 184 | println!( 185 | " Recording: {} {}", 186 | "asak rec --device".bright_black(), 187 | "".cyan() 188 | ); 189 | println!( 190 | " Playback: {} {}", 191 | "asak play --device".bright_black(), 192 | "".cyan() 193 | ); 194 | 195 | println!("\n{}", "=== Input Devices ===".green().bold()); 196 | for (index, device) in in_devices.enumerate() { 197 | println!("#{}: {}", index.to_string().cyan(), device.name().unwrap()); 198 | } 199 | 200 | println!("\n{}", "=== Output Devices ===".blue().bold()); 201 | for (index, device) in out_devices.enumerate() { 202 | println!("#{}: {}", index.to_string().cyan(), device.name().unwrap()); 203 | } 204 | 205 | println!( 206 | "\n{}", 207 | "Note: If no device is specified, the system default will be used.".italic() 208 | ); 209 | println!(); 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/monitor.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{stdout, Stdout}, 3 | sync::{ 4 | atomic::{AtomicBool, Ordering}, 5 | Arc, 6 | }, 7 | time::Duration, 8 | }; 9 | 10 | use anyhow::Result; 11 | use cpal::{ 12 | traits::{DeviceTrait, HostTrait, StreamTrait}, 13 | SizedSample, SupportedStreamConfig, 14 | }; 15 | 16 | use crossbeam::channel::{bounded, unbounded, Receiver, Sender}; 17 | use inquire::Select; 18 | 19 | use crossterm::event::{self, KeyCode}; 20 | use crossterm::execute; 21 | use crossterm::terminal::{ 22 | disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, 23 | }; 24 | use ratatui::{ 25 | layout::{Constraint, Direction, Layout}, 26 | prelude::{CrosstermBackend, Terminal, *}, 27 | style::{Color, Style}, 28 | text::{Span, Text}, 29 | widgets::{Block, Borders, Gauge, LineGauge, Paragraph}, 30 | }; 31 | 32 | use ratatui::style::Modifier; 33 | 34 | pub fn start_monitoring(buffer_length: usize) -> Result<()> { 35 | // let rb = HeapRb::::new(buffer_length); 36 | let (ui_tx, ui_rx) = unbounded(); 37 | // let shared_waveform_data = Arc::new(Mutex::new(rb)); 38 | // let shared_waveform_data_for_audio_thread = shared_waveform_data.clone(); 39 | let is_monitoring = Arc::new(AtomicBool::new(true)); 40 | 41 | let host = cpal::default_host(); 42 | let devices = host.devices()?; 43 | let mut device_options = vec![]; 44 | for device in devices { 45 | device_options.push(device.name().unwrap()); 46 | } 47 | let selected_input = Select::new("Select an input device:", device_options.clone()).prompt()?; 48 | let selected_output = Select::new("Select an output device:", device_options).prompt()?; 49 | 50 | let input_device = host 51 | .devices()? 52 | .find(|device| device.name().unwrap() == selected_input) 53 | .unwrap(); 54 | let output_device = host 55 | .devices()? 56 | .find(|device| device.name().unwrap() == selected_output) 57 | .unwrap(); 58 | 59 | // todo: selected_output has to be the default output device manually, which is a bug 60 | // let output_device = host.default_output_device().unwrap(); 61 | // let selected_output = output_device.name().unwrap(); 62 | 63 | let input_config = input_device.default_input_config()?; 64 | let output_config = output_device.default_output_config()?; 65 | 66 | if input_config.sample_rate() != output_config.sample_rate() { 67 | return Err(anyhow::anyhow!( 68 | "Sample rates of input and output devices do not match." 69 | )); 70 | } 71 | 72 | let config = SupportedStreamConfig::new( 73 | 2, 74 | input_config.sample_rate(), 75 | input_config.buffer_size().clone(), 76 | input_config.sample_format(), 77 | ); 78 | 79 | let stream_format = input_config.sample_format(); 80 | 81 | match stream_format { 82 | cpal::SampleFormat::F32 => build_stream::( 83 | &input_device, 84 | &config.clone().into(), 85 | &output_device, 86 | &config.clone().into(), 87 | Arc::clone(&is_monitoring), 88 | ui_tx, 89 | // shared_waveform_data_for_audio_thread, 90 | buffer_length, 91 | )?, 92 | cpal::SampleFormat::I16 => build_stream::( 93 | &input_device, 94 | &config.clone().into(), 95 | &output_device, 96 | &config.clone().into(), 97 | Arc::clone(&is_monitoring), 98 | ui_tx, 99 | // shared_waveform_data_for_audio_thread, 100 | buffer_length, 101 | )?, 102 | cpal::SampleFormat::U16 => build_stream::( 103 | &input_device, 104 | &config.clone().into(), 105 | &output_device, 106 | &config.clone().into(), 107 | Arc::clone(&is_monitoring), 108 | ui_tx, 109 | // shared_waveform_data_for_audio_thread, 110 | buffer_length, 111 | )?, 112 | _ => return Err(anyhow::anyhow!("Unsupported sample format")), 113 | }; 114 | 115 | record_tui(ui_rx, is_monitoring, &selected_input, &selected_output)?; 116 | Ok(()) 117 | } 118 | 119 | fn build_stream( 120 | input_device: &cpal::Device, 121 | input_config: &cpal::StreamConfig, 122 | output_device: &cpal::Device, 123 | output_config: &cpal::StreamConfig, 124 | is_monitoring: Arc, 125 | ui_tx: Sender>, 126 | // shared_waveform_data: Arc>>>, 127 | buffer_length: usize, 128 | ) -> Result<(), anyhow::Error> 129 | where 130 | T: cpal::Sample + Send + 'static + Default + SizedSample + Into, 131 | { 132 | let (tx, rx) = bounded::(buffer_length); 133 | // let is_monitoring_clone = Arc::clone(&is_monitoring); 134 | let input_stream = input_device.build_input_stream( 135 | input_config, 136 | move |data: &[T], _: &cpal::InputCallbackInfo| { 137 | if is_monitoring.load(Ordering::SeqCst) { 138 | // let mut waveform = shared_waveform_data.lock(); 139 | 140 | let waveform: Vec = data 141 | .iter() 142 | .map(|&sample| { 143 | let s: f32 = sample.into(); 144 | s 145 | }) 146 | .collect(); 147 | ui_tx.send(waveform).ok(); 148 | 149 | for &sample in data.iter() { 150 | // let sample_f32: f32 = sample.into(); 151 | // (*waveform).push_overwrite(sample_f32); 152 | if tx.send(sample).is_err() { 153 | eprintln!("Buffer overflow, dropping sample"); 154 | } 155 | } 156 | } 157 | }, 158 | move |err| { 159 | eprintln!("Error occurred on input stream: {}", err); 160 | }, 161 | None, 162 | )?; 163 | 164 | let output_stream = output_device.build_output_stream( 165 | output_config, 166 | move |data: &mut [T], _: &cpal::OutputCallbackInfo| { 167 | for sample in data.iter_mut() { 168 | *sample = rx.recv().unwrap_or_default(); 169 | } 170 | }, 171 | move |err| { 172 | eprintln!("Error occurred on output stream: {}", err); 173 | }, 174 | None, 175 | )?; 176 | 177 | input_stream.play()?; 178 | output_stream.play()?; 179 | 180 | Ok(()) 181 | } 182 | 183 | fn record_tui( 184 | ui_rx: Receiver>, 185 | is_monitoring: Arc, 186 | selected_input: &str, 187 | selected_output: &str, 188 | ) -> Result<()> { 189 | enable_raw_mode()?; 190 | execute!(stdout(), EnterAlternateScreen)?; 191 | let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; 192 | terminal.clear()?; 193 | 194 | loop { 195 | let mut waveform_data = Vec::new(); 196 | while let Ok(data) = ui_rx.try_recv() { 197 | waveform_data = data; 198 | } 199 | draw_rec_waveform( 200 | &mut terminal, 201 | waveform_data, 202 | selected_input, 203 | selected_output, 204 | )?; 205 | let refresh_interval = Duration::from_millis(100); 206 | if event::poll(refresh_interval)? { 207 | if let event::Event::Key(event) = event::read()? { 208 | if event.code == KeyCode::Enter { 209 | is_monitoring.store(false, Ordering::SeqCst); 210 | break; 211 | } 212 | } 213 | } 214 | } 215 | 216 | execute!(stdout(), LeaveAlternateScreen)?; 217 | disable_raw_mode()?; 218 | Ok(()) 219 | } 220 | 221 | fn draw_rec_waveform( 222 | terminal: &mut Terminal>, 223 | shared_waveform_data: Vec, 224 | selected_input: &str, 225 | selected_output: &str, 226 | ) -> Result<()> { 227 | terminal.draw(|f| { 228 | let waveform: Vec = shared_waveform_data.to_vec(); 229 | 230 | let vertical = Layout::default() 231 | .direction(Direction::Vertical) 232 | .constraints( 233 | [ 234 | Constraint::Length(2), 235 | // Constraint::Length(1), 236 | Constraint::Length(1), 237 | // Constraint::Length(4), 238 | // Constraint::Length(4), 239 | Constraint::Length(3), 240 | Constraint::Length(3), 241 | Constraint::Min(3), 242 | ] 243 | .as_ref(), 244 | ); 245 | 246 | let [title, indicator, rect_left, rect_right, help] = vertical.areas(f.area()); 247 | 248 | let devices = Paragraph::new(Text::raw(format!( 249 | "INPUT: {};\t OUTPUT: {};", 250 | selected_input, selected_output 251 | ))) 252 | .style( 253 | Style::default() 254 | .fg(Color::Blue) 255 | .add_modifier(Modifier::BOLD), 256 | ); 257 | f.render_widget(devices, title); 258 | 259 | let label = Span::styled( 260 | "Press ENTER to exit TUI and stop monitoring...", 261 | Style::default() 262 | .fg(Color::Yellow) 263 | .add_modifier(Modifier::ITALIC | Modifier::BOLD), 264 | ); 265 | 266 | f.render_widget(Paragraph::new(label), help); 267 | 268 | let level = calculate_level(&waveform); 269 | 270 | if level.len() < 2 { 271 | return; 272 | } 273 | let left = (level[0].0 * 90.) as u64; 274 | let right = (level[1].0 * 90.) as u64; 275 | 276 | let db_left = (20. * level[0].0.log10()) as i32; 277 | let db_right = (20. * level[1].0.log10()) as i32; 278 | // audio clip indicator 279 | let color = if left > 90 || right > 90 { 280 | Color::Red 281 | } else { 282 | Color::Green 283 | }; 284 | 285 | let line_gauge_test = LineGauge::default() 286 | // .block(Block::bordered().title("Progress")) 287 | .filled_style( 288 | Style::default() 289 | .fg(Color::Green) 290 | .bg(Color::Red) 291 | .add_modifier(Modifier::BOLD), 292 | ) 293 | .label("") 294 | .line_set(symbols::line::THICK) 295 | .ratio(0.9); 296 | 297 | f.render_widget(line_gauge_test, indicator); 298 | 299 | let g = Gauge::default() 300 | .block(Block::new().title("Left dB SPL").borders(Borders::ALL)) 301 | .gauge_style(color) 302 | .label(Span::styled( 303 | format!( 304 | "{} db", 305 | match db_left { 306 | x if x < -90 => "-inf".to_string(), 307 | x => x.to_string(), 308 | } 309 | ), 310 | Style::new().italic().bold().fg(Color::White), 311 | )) 312 | .ratio(level[0].0 as f64); 313 | f.render_widget(g, rect_left); 314 | 315 | let g = Gauge::default() 316 | .block(Block::new().title("Right dB SPL").borders(Borders::ALL)) 317 | .gauge_style(color) 318 | .label(Span::styled( 319 | format!( 320 | "{} db", 321 | match db_right { 322 | x if x < -90 => "-inf".to_string(), 323 | x => x.to_string(), 324 | } 325 | ), 326 | Style::new().italic().bold().fg(Color::White), 327 | )) 328 | .ratio(level[1].0 as f64); 329 | f.render_widget(g, rect_right); 330 | })?; 331 | Ok(()) 332 | } 333 | 334 | fn calculate_level(samples: &[f32]) -> Vec<(f32, f32)> { 335 | let mut v = vec![]; 336 | for frame in samples.chunks(2) { 337 | let square_sum: f32 = frame.iter().map(|&sample| (sample).powi(2)).sum(); 338 | let mean: f32 = square_sum / frame.len() as f32; 339 | let rms = mean.sqrt(); 340 | 341 | let peak = frame 342 | .iter() 343 | .map(|&sample| sample.abs()) 344 | .max_by(|a, b| a.partial_cmp(b).unwrap()); 345 | v.push((rms, peak.unwrap())); 346 | } 347 | v 348 | } 349 | -------------------------------------------------------------------------------- /src/playback.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; 3 | use crossterm::event::{self, KeyCode}; 4 | use crossterm::execute; 5 | use crossterm::terminal::{ 6 | disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, 7 | }; 8 | use hound::WavReader; 9 | use ratatui::style::Modifier; 10 | use ratatui::symbols; 11 | use ratatui::text::Span; 12 | use ratatui::widgets::canvas::{Canvas, Circle, Line}; 13 | use ratatui::widgets::{Axis, Block, Borders, Chart, Dataset, Gauge, GraphType, Paragraph}; 14 | use ratatui::{ 15 | layout::{Constraint, Direction, Layout}, 16 | prelude::{CrosstermBackend, Terminal}, 17 | style::{Color, Style}, 18 | }; 19 | 20 | use dasp_interpolate::linear::Linear; 21 | use dasp_signal::Signal; 22 | use std::f64::consts::PI; 23 | use std::io::stdout; 24 | use std::sync::atomic::{AtomicBool, AtomicUsize}; 25 | use std::sync::Arc; 26 | use std::time::{Duration, Instant}; 27 | 28 | #[allow(unused_variables)] 29 | pub fn play_audio(file_path: &str, device: Option, jack: bool) -> Result<()> { 30 | // Conditionally compile with jack if the feature is specified. 31 | #[cfg(all( 32 | any( 33 | target_os = "linux", 34 | target_os = "dragonfly", 35 | target_os = "freebsd", 36 | target_os = "netbsd" 37 | ), 38 | feature = "jack" 39 | ))] 40 | let host = if jack { 41 | cpal::host_from_id(cpal::available_hosts() 42 | .into_iter() 43 | .find(|id| *id == cpal::HostId::Jack) 44 | .expect( 45 | "make sure --features jack is specified. only works on OSes where jack is available", 46 | )).expect("jack host unavailable") 47 | } else { 48 | cpal::default_host() 49 | }; 50 | 51 | #[cfg(any( 52 | not(any( 53 | target_os = "linux", 54 | target_os = "dragonfly", 55 | target_os = "freebsd", 56 | target_os = "netbsd" 57 | )), 58 | not(feature = "jack") 59 | ))] 60 | let host = cpal::default_host(); 61 | 62 | let device = if device.is_none() { 63 | host.default_output_device() 64 | } else if let Some(index) = device { 65 | host.output_devices()?.nth(index as usize) 66 | } else { 67 | panic!("failed to find output device"); 68 | } 69 | .expect("failed to find output device"); 70 | 71 | let config = device.default_output_config().unwrap(); 72 | 73 | let sys_chan = config.channels() as usize; 74 | let sys_sr = config.sample_rate().0 as f64; 75 | let mut reader = WavReader::open(file_path).expect("failed to open wav file"); 76 | let spec = reader.spec(); 77 | let source_sr = spec.sample_rate as f64; 78 | 79 | let num_channels = spec.channels as usize; 80 | let bit = spec.bits_per_sample as usize; 81 | let mut file_data: Vec> = vec![]; 82 | 83 | for _ in 0..num_channels { 84 | file_data.push(Vec::new()); 85 | } 86 | 87 | let mut sample_count = 0; 88 | 89 | match spec.sample_format { 90 | hound::SampleFormat::Int => match spec.bits_per_sample { 91 | 16 => { 92 | for result in reader.samples::() { 93 | let sample = result? as f32 / i16::MAX as f32; 94 | let channel = sample_count % num_channels; 95 | file_data[channel].push(sample); 96 | sample_count += 1; 97 | } 98 | } 99 | 100 | 24 => { 101 | for result in reader.samples::() { 102 | let sample = result?; 103 | let sample = if sample & (1 << 23) != 0 { 104 | (sample | !0xff_ffff) as f32 105 | } else { 106 | sample as f32 107 | }; 108 | let sample = sample / (1 << 23) as f32; 109 | let channel = sample_count % num_channels; 110 | file_data[channel].push(sample); 111 | sample_count += 1; 112 | } 113 | } 114 | 115 | 32 => { 116 | for result in reader.samples::() { 117 | let sample = result? as f32 / i32::MAX as f32; 118 | let channel = sample_count % num_channels; 119 | file_data[channel].push(sample); 120 | sample_count += 1; 121 | } 122 | } 123 | _ => panic!("unsupported bit depth"), 124 | }, 125 | hound::SampleFormat::Float => { 126 | for result in reader.samples::() { 127 | let sample = result?; 128 | let channel = sample_count % num_channels; 129 | file_data[channel].push(sample); 130 | sample_count += 1; 131 | } 132 | } 133 | } 134 | 135 | // TODO: should be able to play any chan file in any chan system 136 | for i in num_channels..sys_chan { 137 | file_data.push(file_data[0].clone()); 138 | } 139 | 140 | let file_data_clone = file_data.clone(); 141 | 142 | let mut resampled_data: Vec> = vec![vec![]; sys_chan]; 143 | 144 | for i in 0..sys_chan { 145 | let mut source = dasp_signal::from_iter(file_data[i].iter().cloned()); 146 | let a = source.next(); 147 | let b = source.next(); 148 | let interp = Linear::new(a, b); 149 | let resampled_sig = source 150 | .from_hz_to_hz(interp, source_sr, sys_sr) 151 | .until_exhausted(); 152 | 153 | resampled_data[i] = resampled_sig.collect(); 154 | } 155 | let length = resampled_data[0].len(); 156 | 157 | let sample_format = config.sample_format(); 158 | let pointer = Arc::new(AtomicUsize::new(0)); 159 | let is_paused = Arc::new(AtomicBool::new(false)); 160 | 161 | let err_fn = |err| eprintln!("an error occurred on the output stream: {}", err); 162 | 163 | let is_paused_clone = is_paused.clone(); 164 | let stream = match sample_format { 165 | cpal::SampleFormat::F32 => device.build_output_stream( 166 | &config.into(), 167 | move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { 168 | let channels = sys_chan as usize; 169 | for i in (0..data.len()).step_by(sys_chan) { 170 | let p = pointer.load(std::sync::atomic::Ordering::Relaxed); 171 | 172 | for j in 0..channels { 173 | if i + j < data.len() && j < resampled_data.len() && p < length { 174 | data[i + j] = resampled_data[j][p]; 175 | } 176 | } 177 | 178 | if !is_paused_clone.load(std::sync::atomic::Ordering::Relaxed) { 179 | let next = if p + 1 < length { p + 1 } else { 0 }; // Loop at the end 180 | pointer.store(next, std::sync::atomic::Ordering::Relaxed); 181 | } 182 | } 183 | }, 184 | err_fn, 185 | None, 186 | )?, 187 | cpal::SampleFormat::I16 => device.build_output_stream( 188 | &config.into(), 189 | move |data: &mut [i16], _: &cpal::OutputCallbackInfo| { 190 | let channels = sys_chan as usize; 191 | for i in (0..data.len()).step_by(sys_chan) { 192 | let p = pointer.load(std::sync::atomic::Ordering::Relaxed); 193 | 194 | for j in 0..channels { 195 | if i + j < data.len() && j < resampled_data.len() && p < length { 196 | data[i + j] = (resampled_data[j][p] * i16::MAX as f32) as i16; 197 | } 198 | } 199 | 200 | if !is_paused_clone.load(std::sync::atomic::Ordering::Relaxed) { 201 | let next = if p + 1 < length { p + 1 } else { 0 }; // Loop at the end 202 | pointer.store(next, std::sync::atomic::Ordering::Relaxed); 203 | } 204 | } 205 | }, 206 | err_fn, 207 | None, 208 | )?, 209 | cpal::SampleFormat::U16 => device.build_output_stream( 210 | &config.into(), 211 | move |data: &mut [u16], _: &cpal::OutputCallbackInfo| { 212 | let channels = sys_chan as usize; 213 | for i in (0..data.len()).step_by(sys_chan) { 214 | let p = pointer.load(std::sync::atomic::Ordering::Relaxed); 215 | 216 | for j in 0..channels { 217 | if i + j < data.len() && j < resampled_data.len() && p < length { 218 | data[i + j] = 219 | ((resampled_data[j][p] * u16::MAX as f32) + u16::MAX as f32) as u16; 220 | } 221 | } 222 | 223 | if !is_paused_clone.load(std::sync::atomic::Ordering::Relaxed) { 224 | let next = if p + 1 < length { p + 1 } else { 0 }; // Loop at the end 225 | pointer.store(next, std::sync::atomic::Ordering::Relaxed); 226 | } 227 | } 228 | }, 229 | err_fn, 230 | None, 231 | )?, 232 | 233 | cpal::SampleFormat::I32 => device.build_output_stream( 234 | &config.into(), 235 | move |data: &mut [i32], _: &cpal::OutputCallbackInfo| { 236 | let channels = sys_chan as usize; 237 | for i in (0..data.len()).step_by(sys_chan) { 238 | let p = pointer.load(std::sync::atomic::Ordering::Relaxed); 239 | 240 | for j in 0..channels { 241 | if i + j < data.len() && j < resampled_data.len() && p < length { 242 | data[i + j] = (resampled_data[j][p] * i32::MAX as f32) as i32; 243 | } 244 | } 245 | 246 | if !is_paused_clone.load(std::sync::atomic::Ordering::Relaxed) { 247 | let next = if p + 1 < length { p + 1 } else { 0 }; // Loop at the end 248 | pointer.store(next, std::sync::atomic::Ordering::Relaxed); 249 | } 250 | } 251 | }, 252 | err_fn, 253 | None, 254 | )?, 255 | cpal::SampleFormat::U32 => device.build_output_stream( 256 | &config.into(), 257 | move |data: &mut [u32], _: &cpal::OutputCallbackInfo| { 258 | let channels = sys_chan as usize; 259 | for i in (0..data.len()).step_by(sys_chan) { 260 | let p = pointer.load(std::sync::atomic::Ordering::Relaxed); 261 | 262 | for j in 0..channels { 263 | if i + j < data.len() && j < resampled_data.len() && p < length { 264 | data[i + j] = 265 | ((resampled_data[j][p] * u32::MAX as f32) + u32::MAX as f32) as u32; 266 | } 267 | } 268 | 269 | if !is_paused_clone.load(std::sync::atomic::Ordering::Relaxed) { 270 | let next = if p + 1 < length { p + 1 } else { 0 }; // Loop at the end 271 | pointer.store(next, std::sync::atomic::Ordering::Relaxed); 272 | } 273 | } 274 | }, 275 | err_fn, 276 | None, 277 | )?, 278 | _ => panic!("unsupported sample format"), 279 | }; 280 | stream.play()?; 281 | 282 | enable_raw_mode()?; 283 | execute!(stdout(), EnterAlternateScreen)?; 284 | let backend = CrosstermBackend::new(stdout()); 285 | let mut terminal = Terminal::new(backend)?; 286 | terminal.hide_cursor()?; 287 | 288 | let start_time = Instant::now(); 289 | let file_duration = WavReader::open(file_path)?.duration() as f32 / spec.sample_rate as f32; 290 | 291 | // Initialize angles for canvas animation 292 | let mut angle1 = 0.0; 293 | let mut angle2 = 0.0; 294 | 295 | // For tracking playback time 296 | let mut elapsed = 0.0; 297 | let mut last_update = Instant::now(); 298 | 299 | loop { 300 | if event::poll(Duration::from_millis(100))? { 301 | if let event::Event::Key(event) = event::read()? { 302 | match event.code { 303 | KeyCode::Esc => { 304 | break; 305 | } 306 | KeyCode::Char(' ') => { 307 | let current_pause_state = 308 | is_paused.load(std::sync::atomic::Ordering::Relaxed); 309 | is_paused.store(!current_pause_state, std::sync::atomic::Ordering::Relaxed); 310 | } 311 | _ => {} 312 | } 313 | } 314 | } 315 | 316 | // Update elapsed time only when not paused 317 | if !is_paused.load(std::sync::atomic::Ordering::Relaxed) { 318 | let now = Instant::now(); 319 | elapsed += now.duration_since(last_update).as_secs_f32(); 320 | last_update = now; 321 | 322 | // Update angles for rotation only when not paused 323 | angle1 = (angle1 + 0.1) % (2.0 * PI); 324 | angle2 = (angle2 + 0.15) % (2.0 * PI); 325 | } else { 326 | // Still update last_update to avoid jumps when unpausing 327 | last_update = Instant::now(); 328 | } 329 | 330 | // Calculate progress based on actual playback progress 331 | let progress = (elapsed % file_duration) / file_duration; 332 | 333 | // With looping, we'll display the current position within the loop 334 | let display_time = elapsed % file_duration; 335 | 336 | terminal.draw(|f| { 337 | let size = f.area(); 338 | let width = size.width as usize; 339 | 340 | // data vec is calculated here, pick width samples from the file data 341 | let mut data_vec: Vec<(f64, f64)> = vec![]; 342 | for i in 0..width { 343 | let index = (i as f32 / width as f32 * length as f32) as usize; 344 | let rms = file_data_clone[0][index]; 345 | data_vec.push((i as f64, rms as f64)); 346 | } 347 | 348 | let chunks = Layout::default() 349 | .direction(Direction::Vertical) 350 | .constraints( 351 | [ 352 | Constraint::Length(3), // Progress bar 353 | Constraint::Length(8), // Waveform (reduced height) 354 | Constraint::Percentage(70), // Canvas animation 355 | Constraint::Min(3), // Help text 356 | ] 357 | .as_ref(), 358 | ) 359 | .split(size); 360 | 361 | let title_text = if is_paused.load(std::sync::atomic::Ordering::Relaxed) { 362 | format!( 363 | "PLAYBACK (PAUSED) {:.2}s/{:.2}s", 364 | display_time, file_duration 365 | ) 366 | } else { 367 | format!("PLAYBACK {:.2}s/{:.2}s", display_time, file_duration) 368 | }; 369 | 370 | let gauge = Gauge::default() 371 | .block(Block::default().title(title_text).borders(Borders::ALL)) 372 | .gauge_style(Style::default().fg(Color::Blue).bg(Color::Black)) 373 | .percent((progress * 100.0) as u16); 374 | 375 | f.render_widget(gauge, chunks[0]); 376 | 377 | let datasets = vec![Dataset::default() 378 | .marker(symbols::Marker::Braille) 379 | .graph_type(GraphType::Line) 380 | .style(Style::default().fg(Color::Red)) 381 | .data(&data_vec)]; 382 | 383 | let chart = Chart::new(datasets) 384 | .x_axis( 385 | Axis::default() 386 | .style(Style::default().fg(Color::Gray)) 387 | .bounds([0., width as f64]), 388 | ) 389 | .y_axis( 390 | Axis::default() 391 | .style(Style::default().fg(Color::Gray)) 392 | .bounds([-1.0, 1.]), 393 | ) 394 | .block(Block::default().borders(Borders::ALL).title("Waveform")); 395 | 396 | f.render_widget(chart, chunks[1]); 397 | 398 | // Canvas animation (rotating discs) 399 | let canvas_rect = chunks[2]; 400 | 401 | // Aspect ratio correction 402 | let canvas_width_chars = canvas_rect.width as f64; 403 | let canvas_height_chars = canvas_rect.height as f64; 404 | let char_aspect_ratio = 2.0; // Assume terminal char height is approx 2x width 405 | 406 | // Calculate the aspect ratio needed for world coordinates to make circles appear round 407 | let canvas_aspect_ratio = canvas_height_chars / canvas_width_chars; 408 | let world_aspect_ratio = char_aspect_ratio * canvas_aspect_ratio; 409 | 410 | // Define the fixed horizontal range for world coordinates 411 | let x_world_range = 100.0; 412 | let x_bounds = [-x_world_range / 2.0, x_world_range / 2.0]; // [-50.0, 50.0] 413 | 414 | // Calculate the corresponding vertical range based on the desired world aspect ratio 415 | let y_world_range = x_world_range * world_aspect_ratio; 416 | let y_bounds = [-y_world_range / 2.0, y_world_range / 2.0]; 417 | 418 | // Define the circle parameters in world coordinates 419 | let circle_radius = 15.0; 420 | let center1_x = -20.0; 421 | let center1_y = 0.0; 422 | let center2_x = 20.0; 423 | let center2_y = 0.0; 424 | 425 | let canvas = Canvas::default() 426 | .block(Block::default().borders(Borders::ALL).title("Playback")) 427 | .x_bounds(x_bounds) 428 | .y_bounds(y_bounds) 429 | .paint(move |ctx| { 430 | // Draw the disc outlines 431 | ctx.draw(&Circle { 432 | x: center1_x, 433 | y: center1_y, 434 | radius: circle_radius, 435 | color: Color::White, 436 | }); 437 | 438 | ctx.draw(&Circle { 439 | x: center2_x, 440 | y: center2_y, 441 | radius: circle_radius, 442 | color: Color::White, 443 | }); 444 | 445 | // Draw inner circles 446 | ctx.draw(&Circle { 447 | x: center1_x, 448 | y: center1_y, 449 | radius: circle_radius * 0.2, 450 | color: Color::White, 451 | }); 452 | 453 | ctx.draw(&Circle { 454 | x: center2_x, 455 | y: center2_y, 456 | radius: circle_radius * 0.2, 457 | color: Color::White, 458 | }); 459 | 460 | // Draw rotating lines (left disc) 461 | for i in 0..4 { 462 | let line_angle = angle1 + (i as f64 * PI / 2.0); 463 | let end_x = center1_x + circle_radius * line_angle.cos(); 464 | let end_y = center1_y + circle_radius * line_angle.sin(); 465 | 466 | ctx.draw(&Line { 467 | x1: center1_x, 468 | y1: center1_y, 469 | x2: end_x, 470 | y2: end_y, 471 | color: Color::White, 472 | }); 473 | } 474 | 475 | // Draw rotating lines (right disc) 476 | for i in 0..4 { 477 | let line_angle = angle2 + (i as f64 * PI / 2.0); 478 | let end_x = center2_x + circle_radius * line_angle.cos(); 479 | let end_y = center2_y + circle_radius * line_angle.sin(); 480 | 481 | ctx.draw(&Line { 482 | x1: center2_x, 483 | y1: center2_y, 484 | x2: end_x, 485 | y2: end_y, 486 | color: Color::White, 487 | }); 488 | } 489 | 490 | // Draw grooves 491 | for r in 1..5 { 492 | let groove_radius = circle_radius * (0.3 + r as f64 * 0.15); 493 | 494 | ctx.draw(&Circle { 495 | x: center1_x, 496 | y: center1_y, 497 | radius: groove_radius, 498 | color: Color::Gray, 499 | }); 500 | 501 | ctx.draw(&Circle { 502 | x: center2_x, 503 | y: center2_y, 504 | radius: groove_radius, 505 | color: Color::Gray, 506 | }); 507 | } 508 | }); 509 | 510 | f.render_widget(canvas, chunks[2]); 511 | 512 | let label = Span::styled( 513 | "[SPACE] -> PAUSE/RESUME | [ESC] -> QUIT", 514 | Style::default() 515 | .fg(Color::Yellow) 516 | .add_modifier(Modifier::ITALIC | Modifier::BOLD), 517 | ); 518 | 519 | f.render_widget(Paragraph::new(label), chunks[3]); 520 | })?; 521 | } 522 | 523 | disable_raw_mode()?; 524 | execute!(stdout(), LeaveAlternateScreen)?; 525 | Ok(()) 526 | } 527 | -------------------------------------------------------------------------------- /src/record.rs: -------------------------------------------------------------------------------- 1 | use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; 2 | use cpal::{Sample, SampleFormat, SupportedStreamConfig}; 3 | use crossbeam::channel::{unbounded, Receiver}; 4 | use crossterm::event::{self, KeyCode}; 5 | use crossterm::execute; 6 | use crossterm::terminal::{ 7 | disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, 8 | }; 9 | use hound::{WavSpec, WavWriter}; 10 | use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; 11 | use ratatui::style::Modifier; 12 | use ratatui::widgets::canvas::{Canvas, Circle, Line}; 13 | use ratatui::{ 14 | prelude::{CrosstermBackend, Terminal}, 15 | style::{Color, Style}, 16 | text::Span, 17 | widgets::{Block, Borders, Gauge, Paragraph}, 18 | }; 19 | use std::f64::consts::PI; 20 | use std::io::{stdout, Stdout}; 21 | use std::sync::atomic::{AtomicBool, Ordering}; 22 | use std::sync::Arc; 23 | use std::time::Duration; 24 | use std::time::Instant; 25 | 26 | fn calculate_level(samples: &[f32]) -> Vec<(f32, f32)> { 27 | let mut v = vec![]; 28 | for frame in samples.chunks(2) { 29 | let square_sum: f32 = frame.iter().map(|&sample| (sample).powi(2)).sum(); 30 | let mean: f32 = square_sum / frame.len() as f32; 31 | let rms = mean.sqrt(); 32 | 33 | let peak = frame 34 | .iter() 35 | .map(|&sample| sample.abs()) 36 | .max_by(|a, b| a.partial_cmp(b).unwrap()); 37 | v.push((rms, peak.unwrap_or(0.0))); 38 | } 39 | v 40 | } 41 | 42 | fn record_tui(ui_rx: Receiver>, is_recording: Arc) -> anyhow::Result<()> { 43 | let start_time = Instant::now(); 44 | let refresh_interval = Duration::from_millis(100); 45 | 46 | enable_raw_mode()?; 47 | execute!(stdout(), EnterAlternateScreen)?; 48 | let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; 49 | terminal.clear()?; 50 | 51 | let mut angle1 = 0.0; 52 | let mut angle2 = 0.0; 53 | let mut last_audio_data = Vec::new(); 54 | 55 | loop { 56 | let now = Instant::now(); 57 | let duration = now.duration_since(start_time); 58 | let secs = duration.as_secs_f32(); 59 | 60 | // Update angles for rotation 61 | angle1 = (angle1 + 0.1) % (2.0 * PI); 62 | angle2 = (angle2 + 0.15) % (2.0 * PI); 63 | 64 | // Process audio data for visualization 65 | while let Ok(data) = ui_rx.try_recv() { 66 | last_audio_data = data; 67 | } 68 | 69 | draw_rotating_discs(&mut terminal, secs, angle1, angle2, &last_audio_data)?; 70 | 71 | if event::poll(refresh_interval)? { 72 | if let event::Event::Key(event) = event::read()? { 73 | if event.code == KeyCode::Enter { 74 | is_recording.store(false, Ordering::SeqCst); 75 | break; 76 | } 77 | } 78 | } 79 | } 80 | 81 | execute!(stdout(), LeaveAlternateScreen)?; 82 | disable_raw_mode()?; 83 | Ok(()) 84 | } 85 | 86 | fn draw_rotating_discs( 87 | terminal: &mut Terminal>, 88 | secs: f32, 89 | angle1: f64, 90 | angle2: f64, 91 | audio_data: &[f32], 92 | ) -> anyhow::Result<()> { 93 | // Check for zero area before drawing 94 | let size = terminal.size()?; 95 | let chunks = Layout::default() 96 | .direction(Direction::Vertical) 97 | .constraints( 98 | [ 99 | Constraint::Length(1), 100 | Constraint::Min(10), 101 | Constraint::Length(3), 102 | Constraint::Length(3), 103 | ] 104 | .as_ref(), 105 | ) 106 | .split(Rect::new(0, 0, size.width, size.height)); 107 | 108 | let canvas_rect = chunks[1]; 109 | if canvas_rect.width == 0 || canvas_rect.height == 0 { 110 | // If the canvas area is zero, we can skip the terminal.draw call 111 | // or handle it gracefully, maybe just drawing the top/bottom parts 112 | // For now, let's just skip the entire draw operation for this frame 113 | return Ok(()); 114 | } 115 | 116 | terminal.draw(|f| { 117 | // Recalculate chunks inside the closure as `f.size()` might differ slightly? 118 | // Or just use the previously calculated chunks. Let's reuse chunks. 119 | let size = f.area(); // Get size specific to this frame draw context 120 | let chunks = Layout::default() // Re-split based on frame size 121 | .direction(Direction::Vertical) 122 | .constraints( 123 | [ 124 | Constraint::Length(1), 125 | Constraint::Min(10), 126 | Constraint::Length(3), 127 | Constraint::Length(3), 128 | ] 129 | .as_ref(), 130 | ) 131 | .split(size); 132 | 133 | // Top row with help text and time 134 | let top_row = Layout::default() 135 | .direction(Direction::Horizontal) 136 | .constraints([Constraint::Percentage(80), Constraint::Percentage(20)]) 137 | .split(chunks[0]); 138 | 139 | // Help text on the left (yellow) 140 | let help_text = Paragraph::new(Span::styled( 141 | "press ENTER to stop and quit recorder", 142 | Style::default() 143 | .fg(Color::Yellow) 144 | .add_modifier(Modifier::ITALIC | Modifier::BOLD), 145 | )) 146 | .alignment(Alignment::Left); 147 | f.render_widget(help_text, top_row[0]); 148 | 149 | // Time on the right (red) 150 | let time_text = format!("{:.1}", secs); 151 | let time_display = Paragraph::new(time_text) 152 | .style(Style::default().fg(Color::Red)) 153 | .alignment(Alignment::Right); 154 | f.render_widget(time_display, top_row[1]); 155 | 156 | // --- Aspect Ratio Correction --- START 157 | let canvas_rect = chunks[1]; // Use chunks calculated inside closure 158 | // No need to check for zero size again, handled outside 159 | 160 | let canvas_width_chars = canvas_rect.width as f64; 161 | let canvas_height_chars = canvas_rect.height as f64; 162 | let char_aspect_ratio = 2.0; // Assume terminal char height is approx 2x width 163 | 164 | // Calculate the aspect ratio needed for the world coordinates to make circles appear round 165 | let canvas_aspect_ratio = canvas_height_chars / canvas_width_chars; 166 | let world_aspect_ratio = char_aspect_ratio * canvas_aspect_ratio; 167 | 168 | // Define the fixed horizontal range for world coordinates 169 | let x_world_range = 100.0; 170 | let x_bounds = [-x_world_range / 2.0, x_world_range / 2.0]; // [-50.0, 50.0] 171 | 172 | // Calculate the corresponding vertical range based on the desired world aspect ratio 173 | let y_world_range = x_world_range * world_aspect_ratio; 174 | let y_bounds = [-y_world_range / 2.0, y_world_range / 2.0]; 175 | // --- Aspect Ratio Correction --- END 176 | 177 | // Define the circle parameters in world coordinates (using a fixed radius) 178 | let circle_radius = 15.0; 179 | let center1_x = -20.0; 180 | let center1_y = 0.0; 181 | let center2_x = 20.0; 182 | let center2_y = 0.0; 183 | 184 | // Create canvas with dynamically adjusted bounds 185 | let canvas = Canvas::default() 186 | .block( 187 | Block::default().borders(Borders::ALL), // .title("asak recorder"), 188 | ) 189 | .x_bounds(x_bounds) // Use fixed x_bounds 190 | .y_bounds(y_bounds) // Use dynamically calculated y_bounds for aspect ratio correction 191 | .paint(move |ctx| { 192 | // Draw shapes using world coordinates; aspect ratio is handled by bounds 193 | 194 | // Draw the disc outlines 195 | ctx.draw(&Circle { 196 | x: center1_x, 197 | y: center1_y, 198 | radius: circle_radius, 199 | color: Color::White, 200 | }); 201 | 202 | ctx.draw(&Circle { 203 | x: center2_x, 204 | y: center2_y, 205 | radius: circle_radius, 206 | color: Color::White, 207 | }); 208 | 209 | // Draw inner circles 210 | ctx.draw(&Circle { 211 | x: center1_x, 212 | y: center1_y, 213 | radius: circle_radius * 0.2, 214 | color: Color::White, 215 | }); 216 | 217 | ctx.draw(&Circle { 218 | x: center2_x, 219 | y: center2_y, 220 | radius: circle_radius * 0.2, 221 | color: Color::White, 222 | }); 223 | 224 | // Draw rotating lines (use world radius directly) 225 | for i in 0..4 { 226 | let line_angle = angle1 + (i as f64 * PI / 2.0); 227 | let end_x = center1_x + circle_radius * line_angle.cos(); 228 | let end_y = center1_y + circle_radius * line_angle.sin(); 229 | 230 | ctx.draw(&Line { 231 | x1: center1_x, 232 | y1: center1_y, 233 | x2: end_x, 234 | y2: end_y, 235 | color: Color::White, 236 | }); 237 | } 238 | 239 | // Draw rotating lines for right disc 240 | for i in 0..4 { 241 | let line_angle = angle2 + (i as f64 * PI / 2.0); 242 | let end_x = center2_x + circle_radius * line_angle.cos(); 243 | let end_y = center2_y + circle_radius * line_angle.sin(); 244 | 245 | ctx.draw(&Line { 246 | x1: center2_x, 247 | y1: center2_y, 248 | x2: end_x, 249 | y2: end_y, 250 | color: Color::White, 251 | }); 252 | } 253 | 254 | // Draw grooves (use world radius) 255 | for r in 1..5 { 256 | let groove_radius1 = circle_radius * (0.3 + r as f64 * 0.15); 257 | let groove_radius2 = circle_radius * (0.3 + r as f64 * 0.15); 258 | 259 | ctx.draw(&Circle { 260 | x: center1_x, 261 | y: center1_y, 262 | radius: groove_radius1, 263 | color: Color::Gray, 264 | }); 265 | 266 | ctx.draw(&Circle { 267 | x: center2_x, 268 | y: center2_y, 269 | radius: groove_radius2, 270 | color: Color::Gray, 271 | }); 272 | } 273 | }); 274 | 275 | f.render_widget(canvas, chunks[1]); 276 | 277 | // Add level meters 278 | let levels = calculate_level(audio_data); 279 | 280 | if !levels.is_empty() { 281 | // Ensure we have at least 2 channels (stereo) 282 | let left = if levels.len() > 0 { levels[0].0 } else { 0.0 }; 283 | let right = if levels.len() > 1 { levels[1].0 } else { 0.0 }; 284 | 285 | let db_left = if left > 0.0 { 286 | (20.0 * left.log10()) as i32 287 | } else { 288 | -90 289 | }; 290 | let db_right = if right > 0.0 { 291 | (20.0 * right.log10()) as i32 292 | } else { 293 | -90 294 | }; 295 | 296 | // Determine color based on level (red for clipping) 297 | let left_color = if left > 0.9 { Color::Red } else { Color::Green }; 298 | let right_color = if right > 0.9 { 299 | Color::Red 300 | } else { 301 | Color::Green 302 | }; 303 | 304 | let left_gauge = Gauge::default() 305 | .block(Block::new().title("Left dB").borders(Borders::ALL)) 306 | .gauge_style(Style::default().fg(left_color)) 307 | .label(Span::styled( 308 | format!( 309 | "{} dB", 310 | match db_left { 311 | x if x < -90 => "-inf".to_string(), 312 | x => x.to_string(), 313 | } 314 | ), 315 | Style::default() 316 | .add_modifier(Modifier::ITALIC | Modifier::BOLD) 317 | .fg(Color::White), 318 | )) 319 | .ratio(left as f64); 320 | 321 | let right_gauge = Gauge::default() 322 | .block(Block::new().title("Right dB").borders(Borders::ALL)) 323 | .gauge_style(Style::default().fg(right_color)) 324 | .label(Span::styled( 325 | format!( 326 | "{} dB", 327 | match db_right { 328 | x if x < -90 => "-inf".to_string(), 329 | x => x.to_string(), 330 | } 331 | ), 332 | Style::default() 333 | .add_modifier(Modifier::ITALIC | Modifier::BOLD) 334 | .fg(Color::White), 335 | )) 336 | .ratio(right as f64); 337 | 338 | f.render_widget(left_gauge, chunks[2]); 339 | f.render_widget(right_gauge, chunks[3]); 340 | } 341 | })?; 342 | Ok(()) 343 | } 344 | 345 | pub fn record_audio(output: String, device: Option, jack: bool) -> anyhow::Result<()> { 346 | let output = format!("{}.wav", output.replace(".wav", "")); 347 | let (ui_tx, ui_rx) = unbounded(); 348 | let (writer_tx, writer_rx) = unbounded(); 349 | let is_recording = Arc::new(AtomicBool::new(true)); 350 | let is_recording_for_thread = is_recording.clone(); 351 | 352 | #[cfg(all( 353 | any( 354 | target_os = "linux", 355 | target_os = "dragonfly", 356 | target_os = "freebsd", 357 | target_os = "netbsd" 358 | ), 359 | feature = "jack" 360 | ))] 361 | let host = if jack { 362 | cpal::host_from_id(cpal::available_hosts() 363 | .into_iter() 364 | .find(|id| *id == cpal::HostId::Jack) 365 | .expect( 366 | "make sure --features jack is specified. only works on OSes where jack is available", 367 | )).expect("jack host unavailable") 368 | } else { 369 | cpal::default_host() 370 | }; 371 | 372 | #[cfg(any( 373 | not(any( 374 | target_os = "linux", 375 | target_os = "dragonfly", 376 | target_os = "freebsd", 377 | target_os = "netbsd" 378 | )), 379 | not(feature = "jack") 380 | ))] 381 | assert!( 382 | !jack, 383 | "jack is only supported on linux, dragonfly, freebsd, and netbsd" 384 | ); 385 | let host = cpal::default_host(); 386 | 387 | let device = if device.is_none() { 388 | host.default_input_device() 389 | } else if let Some(index) = device { 390 | host.input_devices()?.nth(index as usize) 391 | } else { 392 | panic!("failed to find output device") 393 | } 394 | .expect("failed to find output device"); 395 | 396 | let config = device.default_input_config().unwrap(); 397 | let o = output.to_owned(); 398 | let spec = wav_spec_from_config(&device.default_input_config().unwrap()); 399 | 400 | let recording_thread = std::thread::spawn(move || { 401 | let err_fn = move |err| eprintln!("an error occurred on stream: {}", err); 402 | let stream = match config.sample_format() { 403 | cpal::SampleFormat::I8 => device.build_input_stream( 404 | &config.into(), 405 | move |data: &[i8], _: &_| { 406 | let float_data: Vec = data 407 | .iter() 408 | .map(|&sample| sample.to_float_sample()) 409 | .collect(); 410 | ui_tx.send(float_data.clone()).ok(); 411 | writer_tx.send(float_data).ok(); 412 | }, 413 | err_fn, 414 | None, 415 | )?, 416 | cpal::SampleFormat::I16 => device.build_input_stream( 417 | &config.into(), 418 | move |data: &[i16], _: &_| { 419 | let float_data: Vec = data 420 | .iter() 421 | .map(|&sample| sample.to_float_sample()) 422 | .collect(); 423 | ui_tx.send(float_data.clone()).ok(); 424 | writer_tx.send(float_data).ok(); 425 | }, 426 | err_fn, 427 | None, 428 | )?, 429 | cpal::SampleFormat::I32 => device.build_input_stream( 430 | &config.into(), 431 | move |data: &[i32], _: &_| { 432 | let float_data: Vec = data 433 | .iter() 434 | .map(|&sample| sample.to_float_sample()) 435 | .collect(); 436 | ui_tx.send(float_data.clone()).ok(); 437 | writer_tx.send(float_data).ok(); 438 | }, 439 | err_fn, 440 | None, 441 | )?, 442 | cpal::SampleFormat::F32 => device.build_input_stream( 443 | &config.into(), 444 | move |data: &[f32], _: &_| { 445 | let float_data: Vec = data.to_vec(); 446 | ui_tx.send(float_data.clone()).ok(); 447 | writer_tx.send(float_data).ok(); 448 | }, 449 | err_fn, 450 | None, 451 | )?, 452 | sample_format => { 453 | return Err(anyhow::Error::msg(format!( 454 | "Unsupported sample format '{sample_format}'" 455 | ))) 456 | } 457 | }; 458 | stream.play()?; 459 | 460 | while is_recording_for_thread.load(Ordering::SeqCst) { 461 | std::thread::sleep(std::time::Duration::from_millis(100)); 462 | } 463 | 464 | stream.pause()?; 465 | Ok(()) 466 | }); 467 | 468 | let writer_thread = std::thread::spawn(move || -> anyhow::Result<()> { 469 | let path = std::path::Path::new(&o); 470 | 471 | let spec2 = WavSpec { 472 | channels: spec.channels, 473 | sample_rate: spec.sample_rate, 474 | bits_per_sample: spec.bits_per_sample, 475 | sample_format: hound::SampleFormat::Float, 476 | }; 477 | 478 | let mut writer = WavWriter::create(path, spec2).unwrap(); 479 | 480 | while let Ok(data) = writer_rx.recv() { 481 | for sample in data { 482 | writer.write_sample(sample).ok(); 483 | } 484 | } 485 | 486 | writer.finalize().unwrap(); 487 | Ok(()) 488 | }); 489 | 490 | record_tui(ui_rx, is_recording.clone())?; 491 | is_recording.store(false, Ordering::SeqCst); 492 | recording_thread.join().unwrap()?; 493 | writer_thread.join().unwrap()?; 494 | 495 | Ok(()) 496 | } 497 | 498 | fn wav_spec_from_config(config: &SupportedStreamConfig) -> WavSpec { 499 | WavSpec { 500 | channels: config.channels() as _, 501 | sample_rate: config.sample_rate().0 as _, 502 | bits_per_sample: (config.sample_format().sample_size() * 8) as _, 503 | sample_format: if config.sample_format() == SampleFormat::F32 { 504 | hound::SampleFormat::Float 505 | } else { 506 | hound::SampleFormat::Int 507 | }, 508 | } 509 | } 510 | --------------------------------------------------------------------------------