├── .editorconfig ├── .envrc ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── example_static_png_waveform.rs ├── live_visualize_biquad_lowpass_filter.rs ├── live_visualize_lowpass_filter.rs ├── live_visualize_signal_power.rs └── live_visualize_spectrum.rs ├── plotters_spectrum_example.png ├── png_waveform_example.png ├── res ├── live_demo_lowpass_filter_green_day_holiday.gif ├── live_demo_lowpass_filter_green_day_holiday.webm ├── live_demo_spectrum_green_day_holiday.gif └── live_demo_spectrum_green_day_holiday.webm ├── shell.nix ├── src ├── bin │ └── test_cpal_audio_input.rs ├── dynamic │ ├── live_input.rs │ ├── mod.rs │ └── window_top_btm │ │ ├── mod.rs │ │ ├── pixel_buf.rs │ │ └── visualize_minifb.rs ├── lib.rs ├── spectrum │ ├── mod.rs │ ├── plotters_png_file.rs │ └── png_file.rs ├── tests │ ├── mod.rs │ ├── testutil │ │ ├── mod.rs │ │ └── sine.rs │ ├── visualize_sine_10hz.rs │ └── visualize_sine_50hz_plus_250hz.rs ├── util │ ├── mod.rs │ └── png.rs └── waveform │ ├── mod.rs │ ├── plotters_png_file.rs │ └── png_file.rs └── test └── samples └── sample_1.mp3 /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 4 11 | trim_trailing_whitespace = true 12 | max_line_length = 80 13 | 14 | [Makefile] 15 | indent_style = tab 16 | 17 | [*.yml] 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use nix 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: phip1611 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: "*" 10 | update-types: [ "version-update:semver-patch" ] 11 | - package-ecosystem: github-actions 12 | directory: "/" 13 | schedule: 14 | interval: daily 15 | open-pull-requests-limit: 10 16 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | # Triggers the workflow on push or pull request events (for any branch in a repository) 4 | on: [ push, pull_request, merge_group ] 5 | 6 | env: 7 | CARGO_TERM_COLOR: always 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | rust: 15 | - stable 16 | - nightly 17 | - 1.81.0 # MSRV 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Setup Rust toolchain 21 | uses: dtolnay/rust-toolchain@stable 22 | with: 23 | toolchain: ${{ matrix.rust }} 24 | - uses: Swatinem/rust-cache@v2 25 | - run: sudo apt update && sudo apt install pkg-config libfontconfig-dev libasound2-dev libxkbcommon-dev -y 26 | - run: cargo build --all-targets 27 | - run: cargo test 28 | 29 | style_checks: 30 | runs-on: ubuntu-latest 31 | strategy: 32 | matrix: 33 | rust: 34 | - stable 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Setup Rust toolchain 38 | uses: dtolnay/rust-toolchain@stable 39 | with: 40 | toolchain: ${{ matrix.rust }} 41 | components: clippy, rustfmt 42 | - uses: Swatinem/rust-cache@v2 43 | - run: sudo apt update && sudo apt install pkg-config libfontconfig-dev libasound2-dev libxkbcommon-dev -y 44 | - name: Rustfmt (checks all source code/all features) 45 | run: cargo fmt -- --check 46 | - name: Clippy 47 | run: cargo clippy --all-targets 48 | - name: Rustdoc (default feature) 49 | run: cargo doc 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/vscode,rust,clion+all,windows,macos,linux 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=vscode,rust,clion+all,windows,macos,linux 4 | 5 | ### CLion+all ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/artifacts 37 | # .idea/compiler.xml 38 | # .idea/jarRepositories.xml 39 | # .idea/modules.xml 40 | # .idea/*.iml 41 | # .idea/modules 42 | # *.iml 43 | # *.ipr 44 | 45 | # CMake 46 | cmake-build-*/ 47 | 48 | # Mongo Explorer plugin 49 | .idea/**/mongoSettings.xml 50 | 51 | # File-based project format 52 | *.iws 53 | 54 | # IntelliJ 55 | 56 | 57 | # mpeltonen/sbt-idea plugin 58 | .idea_modules/ 59 | 60 | # JIRA plugin 61 | atlassian-ide-plugin.xml 62 | 63 | # Cursive Clojure plugin 64 | .idea/replstate.xml 65 | 66 | # Crashlytics plugin (for Android Studio and IntelliJ) 67 | com_crashlytics_export_strings.xml 68 | crashlytics.properties 69 | crashlytics-build.properties 70 | fabric.properties 71 | 72 | # Editor-based Rest Client 73 | .idea/httpRequests 74 | 75 | # Android studio 3.1+ serialized cache file 76 | .idea/caches/build_file_checksums.ser 77 | 78 | ### CLion+all Patch ### 79 | # Ignores the whole .idea folder and all .iml files 80 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 81 | 82 | .idea/ 83 | 84 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 85 | 86 | *.iml 87 | modules.xml 88 | .idea/misc.xml 89 | *.ipr 90 | 91 | # Sonarlint plugin 92 | .idea/sonarlint 93 | 94 | ### Linux ### 95 | *~ 96 | 97 | # temporary files which can be created if a process still has a handle open of a deleted file 98 | .fuse_hidden* 99 | 100 | # KDE directory preferences 101 | .directory 102 | 103 | # Linux trash folder which might appear on any partition or disk 104 | .Trash-* 105 | 106 | # .nfs files are created when an open file is removed but is still being accessed 107 | .nfs* 108 | 109 | ### macOS ### 110 | # General 111 | .DS_Store 112 | .AppleDouble 113 | .LSOverride 114 | 115 | # Icon must end with two \r 116 | Icon 117 | 118 | 119 | # Thumbnails 120 | ._* 121 | 122 | # Files that might appear in the root of a volume 123 | .DocumentRevisions-V100 124 | .fseventsd 125 | .Spotlight-V100 126 | .TemporaryItems 127 | .Trashes 128 | .VolumeIcon.icns 129 | .com.apple.timemachine.donotpresent 130 | 131 | # Directories potentially created on remote AFP share 132 | .AppleDB 133 | .AppleDesktop 134 | Network Trash Folder 135 | Temporary Items 136 | .apdisk 137 | 138 | ### Rust ### 139 | # Generated by Cargo 140 | # will have compiled files and executables 141 | /target/ 142 | 143 | ### vscode ### 144 | .vscode/* 145 | !.vscode/settings.json 146 | !.vscode/tasks.json 147 | !.vscode/launch.json 148 | !.vscode/extensions.json 149 | *.code-workspace 150 | 151 | ### Windows ### 152 | # Windows thumbnail cache files 153 | Thumbs.db 154 | Thumbs.db:encryptable 155 | ehthumbs.db 156 | ehthumbs_vista.db 157 | 158 | # Dump file 159 | *.stackdump 160 | 161 | # Folder config file 162 | [Dd]esktop.ini 163 | 164 | # Recycle Bin used on file shares 165 | $RECYCLE.BIN/ 166 | 167 | # Windows Installer files 168 | *.cab 169 | *.msi 170 | *.msix 171 | *.msm 172 | *.msp 173 | 174 | # Windows shortcuts 175 | *.lnk 176 | 177 | # End of https://www.toptal.com/developers/gitignore/api/vscode,rust,clion+all,windows,macos,linux 178 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | # v0.5.0 (2025-05-11) 4 | - **BREAKING** MSRV is 1.81.0 5 | - (slightly) modernized crate and dependencies 6 | - updated dependencies 7 | 8 | # v0.4.0 (2023-09-21) 9 | - **BREAKING** MSRV is 1.63.0 10 | - build fix 11 | - dependency bumps 12 | 13 | # v0.3.1 (2021-11-16) 14 | - removed accidentally public export of internal module 15 | 16 | # v0.3.0 (2021-11-13) 17 | - MSRV is 1.56.1 stable (because of Rust edition 2021) 18 | - breaking changes: changed module paths 19 | - new functionality: live audio + GUI + customized view! see example: \ 20 | **Real-time audio + lowpass filter (6.9MB GIF)** \ 21 | ![Example visualization of real-time audio + lowpass filter](res/live_demo_lowpass_filter_green_day_holiday.gif "Example visualization of real-time audio + lowpass filter") \ 22 | On the top you see the original waveform of the song Holiday by Green Day. On the bottom you see the data after a 23 | lowpass filter was applied. The beats are visible. 24 | - internal code improvements 25 | -------------------------------------------------------------------------------- /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 = "adler2" 7 | version = "2.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 10 | 11 | [[package]] 12 | name = "aho-corasick" 13 | version = "1.1.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 16 | dependencies = [ 17 | "memchr", 18 | ] 19 | 20 | [[package]] 21 | name = "alsa" 22 | version = "0.9.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" 25 | dependencies = [ 26 | "alsa-sys", 27 | "bitflags 2.9.0", 28 | "cfg-if", 29 | "libc", 30 | ] 31 | 32 | [[package]] 33 | name = "alsa-sys" 34 | version = "0.3.1" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" 37 | dependencies = [ 38 | "libc", 39 | "pkg-config", 40 | ] 41 | 42 | [[package]] 43 | name = "android-tzdata" 44 | version = "0.1.1" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 47 | 48 | [[package]] 49 | name = "android_system_properties" 50 | version = "0.1.5" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 53 | dependencies = [ 54 | "libc", 55 | ] 56 | 57 | [[package]] 58 | name = "arrayvec" 59 | version = "0.7.6" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 62 | 63 | [[package]] 64 | name = "audio-visualizer" 65 | version = "0.5.0" 66 | dependencies = [ 67 | "biquad", 68 | "cpal", 69 | "lowpass-filter", 70 | "minifb", 71 | "plotters", 72 | "plotters-bitmap", 73 | "png", 74 | "ringbuffer", 75 | "spectrum-analyzer", 76 | "symphonia", 77 | ] 78 | 79 | [[package]] 80 | name = "autocfg" 81 | version = "1.4.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 84 | 85 | [[package]] 86 | name = "bindgen" 87 | version = "0.70.1" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" 90 | dependencies = [ 91 | "bitflags 2.9.0", 92 | "cexpr", 93 | "clang-sys", 94 | "itertools", 95 | "proc-macro2", 96 | "quote", 97 | "regex", 98 | "rustc-hash", 99 | "shlex", 100 | "syn", 101 | ] 102 | 103 | [[package]] 104 | name = "biquad" 105 | version = "0.5.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "8580925877eb35ab50f987a1c2c193eb46000c8827c871bc3425070f086408cc" 108 | dependencies = [ 109 | "num-traits", 110 | ] 111 | 112 | [[package]] 113 | name = "bitflags" 114 | version = "1.3.2" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 117 | 118 | [[package]] 119 | name = "bitflags" 120 | version = "2.9.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 123 | 124 | [[package]] 125 | name = "bumpalo" 126 | version = "3.17.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 129 | 130 | [[package]] 131 | name = "bytemuck" 132 | version = "1.23.0" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" 135 | 136 | [[package]] 137 | name = "byteorder" 138 | version = "1.5.0" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 141 | 142 | [[package]] 143 | name = "bytes" 144 | version = "1.10.1" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 147 | 148 | [[package]] 149 | name = "cc" 150 | version = "1.2.22" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" 153 | dependencies = [ 154 | "jobserver", 155 | "libc", 156 | "shlex", 157 | ] 158 | 159 | [[package]] 160 | name = "cesu8" 161 | version = "1.1.0" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 164 | 165 | [[package]] 166 | name = "cexpr" 167 | version = "0.6.0" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 170 | dependencies = [ 171 | "nom", 172 | ] 173 | 174 | [[package]] 175 | name = "cfg-if" 176 | version = "1.0.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 179 | 180 | [[package]] 181 | name = "chrono" 182 | version = "0.4.41" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 185 | dependencies = [ 186 | "android-tzdata", 187 | "iana-time-zone", 188 | "js-sys", 189 | "num-traits", 190 | "wasm-bindgen", 191 | "windows-link", 192 | ] 193 | 194 | [[package]] 195 | name = "clang-sys" 196 | version = "1.8.1" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" 199 | dependencies = [ 200 | "glob", 201 | "libc", 202 | "libloading", 203 | ] 204 | 205 | [[package]] 206 | name = "color_quant" 207 | version = "1.1.0" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 210 | 211 | [[package]] 212 | name = "combine" 213 | version = "4.6.7" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" 216 | dependencies = [ 217 | "bytes", 218 | "memchr", 219 | ] 220 | 221 | [[package]] 222 | name = "console_error_panic_hook" 223 | version = "0.1.7" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" 226 | dependencies = [ 227 | "cfg-if", 228 | "wasm-bindgen", 229 | ] 230 | 231 | [[package]] 232 | name = "core-foundation" 233 | version = "0.9.4" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 236 | dependencies = [ 237 | "core-foundation-sys", 238 | "libc", 239 | ] 240 | 241 | [[package]] 242 | name = "core-foundation-sys" 243 | version = "0.8.7" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 246 | 247 | [[package]] 248 | name = "core-graphics" 249 | version = "0.23.2" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" 252 | dependencies = [ 253 | "bitflags 1.3.2", 254 | "core-foundation", 255 | "core-graphics-types", 256 | "foreign-types", 257 | "libc", 258 | ] 259 | 260 | [[package]] 261 | name = "core-graphics-types" 262 | version = "0.1.3" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" 265 | dependencies = [ 266 | "bitflags 1.3.2", 267 | "core-foundation", 268 | "libc", 269 | ] 270 | 271 | [[package]] 272 | name = "core-text" 273 | version = "20.1.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" 276 | dependencies = [ 277 | "core-foundation", 278 | "core-graphics", 279 | "foreign-types", 280 | "libc", 281 | ] 282 | 283 | [[package]] 284 | name = "coreaudio-rs" 285 | version = "0.11.3" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" 288 | dependencies = [ 289 | "bitflags 1.3.2", 290 | "core-foundation-sys", 291 | "coreaudio-sys", 292 | ] 293 | 294 | [[package]] 295 | name = "coreaudio-sys" 296 | version = "0.2.16" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "2ce857aa0b77d77287acc1ac3e37a05a8c95a2af3647d23b15f263bdaeb7562b" 299 | dependencies = [ 300 | "bindgen", 301 | ] 302 | 303 | [[package]] 304 | name = "cpal" 305 | version = "0.15.3" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" 308 | dependencies = [ 309 | "alsa", 310 | "core-foundation-sys", 311 | "coreaudio-rs", 312 | "dasp_sample", 313 | "jni", 314 | "js-sys", 315 | "libc", 316 | "mach2", 317 | "ndk", 318 | "ndk-context", 319 | "oboe", 320 | "wasm-bindgen", 321 | "wasm-bindgen-futures", 322 | "web-sys", 323 | "windows", 324 | ] 325 | 326 | [[package]] 327 | name = "crc32fast" 328 | version = "1.4.2" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 331 | dependencies = [ 332 | "cfg-if", 333 | ] 334 | 335 | [[package]] 336 | name = "dasp_sample" 337 | version = "0.11.0" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" 340 | 341 | [[package]] 342 | name = "dirs" 343 | version = "5.0.1" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" 346 | dependencies = [ 347 | "dirs-sys", 348 | ] 349 | 350 | [[package]] 351 | name = "dirs-sys" 352 | version = "0.4.1" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 355 | dependencies = [ 356 | "libc", 357 | "option-ext", 358 | "redox_users", 359 | "windows-sys 0.48.0", 360 | ] 361 | 362 | [[package]] 363 | name = "dlib" 364 | version = "0.5.2" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" 367 | dependencies = [ 368 | "libloading", 369 | ] 370 | 371 | [[package]] 372 | name = "downcast-rs" 373 | version = "1.2.1" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" 376 | 377 | [[package]] 378 | name = "dwrote" 379 | version = "0.11.3" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "bfe1f192fcce01590bd8d839aca53ce0d11d803bf291b2a6c4ad925a8f0024be" 382 | dependencies = [ 383 | "lazy_static", 384 | "libc", 385 | "winapi", 386 | "wio", 387 | ] 388 | 389 | [[package]] 390 | name = "either" 391 | version = "1.15.0" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 394 | 395 | [[package]] 396 | name = "encoding_rs" 397 | version = "0.8.35" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 400 | dependencies = [ 401 | "cfg-if", 402 | ] 403 | 404 | [[package]] 405 | name = "equivalent" 406 | version = "1.0.2" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 409 | 410 | [[package]] 411 | name = "errno" 412 | version = "0.3.11" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 415 | dependencies = [ 416 | "libc", 417 | "windows-sys 0.59.0", 418 | ] 419 | 420 | [[package]] 421 | name = "fastrand" 422 | version = "2.3.0" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 425 | 426 | [[package]] 427 | name = "fdeflate" 428 | version = "0.3.7" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" 431 | dependencies = [ 432 | "simd-adler32", 433 | ] 434 | 435 | [[package]] 436 | name = "flate2" 437 | version = "1.1.1" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" 440 | dependencies = [ 441 | "crc32fast", 442 | "miniz_oxide", 443 | ] 444 | 445 | [[package]] 446 | name = "float-cmp" 447 | version = "0.10.0" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" 450 | dependencies = [ 451 | "num-traits", 452 | ] 453 | 454 | [[package]] 455 | name = "float-ord" 456 | version = "0.3.2" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" 459 | 460 | [[package]] 461 | name = "font-kit" 462 | version = "0.14.2" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "b64b34f4efd515f905952d91bc185039863705592c0c53ae6d979805dd154520" 465 | dependencies = [ 466 | "bitflags 2.9.0", 467 | "byteorder", 468 | "core-foundation", 469 | "core-graphics", 470 | "core-text", 471 | "dirs", 472 | "dwrote", 473 | "float-ord", 474 | "freetype-sys", 475 | "lazy_static", 476 | "libc", 477 | "log", 478 | "pathfinder_geometry", 479 | "pathfinder_simd", 480 | "walkdir", 481 | "winapi", 482 | "yeslogic-fontconfig-sys", 483 | ] 484 | 485 | [[package]] 486 | name = "foreign-types" 487 | version = "0.5.0" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" 490 | dependencies = [ 491 | "foreign-types-macros", 492 | "foreign-types-shared", 493 | ] 494 | 495 | [[package]] 496 | name = "foreign-types-macros" 497 | version = "0.2.3" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" 500 | dependencies = [ 501 | "proc-macro2", 502 | "quote", 503 | "syn", 504 | ] 505 | 506 | [[package]] 507 | name = "foreign-types-shared" 508 | version = "0.3.1" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" 511 | 512 | [[package]] 513 | name = "freetype-sys" 514 | version = "0.20.1" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" 517 | dependencies = [ 518 | "cc", 519 | "libc", 520 | "pkg-config", 521 | ] 522 | 523 | [[package]] 524 | name = "futures" 525 | version = "0.3.31" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 528 | dependencies = [ 529 | "futures-channel", 530 | "futures-core", 531 | "futures-executor", 532 | "futures-io", 533 | "futures-sink", 534 | "futures-task", 535 | "futures-util", 536 | ] 537 | 538 | [[package]] 539 | name = "futures-channel" 540 | version = "0.3.31" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 543 | dependencies = [ 544 | "futures-core", 545 | "futures-sink", 546 | ] 547 | 548 | [[package]] 549 | name = "futures-core" 550 | version = "0.3.31" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 553 | 554 | [[package]] 555 | name = "futures-executor" 556 | version = "0.3.31" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 559 | dependencies = [ 560 | "futures-core", 561 | "futures-task", 562 | "futures-util", 563 | ] 564 | 565 | [[package]] 566 | name = "futures-io" 567 | version = "0.3.31" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 570 | 571 | [[package]] 572 | name = "futures-macro" 573 | version = "0.3.31" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 576 | dependencies = [ 577 | "proc-macro2", 578 | "quote", 579 | "syn", 580 | ] 581 | 582 | [[package]] 583 | name = "futures-sink" 584 | version = "0.3.31" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 587 | 588 | [[package]] 589 | name = "futures-task" 590 | version = "0.3.31" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 593 | 594 | [[package]] 595 | name = "futures-util" 596 | version = "0.3.31" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 599 | dependencies = [ 600 | "futures-channel", 601 | "futures-core", 602 | "futures-io", 603 | "futures-macro", 604 | "futures-sink", 605 | "futures-task", 606 | "memchr", 607 | "pin-project-lite", 608 | "pin-utils", 609 | "slab", 610 | ] 611 | 612 | [[package]] 613 | name = "getrandom" 614 | version = "0.2.16" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 617 | dependencies = [ 618 | "cfg-if", 619 | "libc", 620 | "wasi 0.11.0+wasi-snapshot-preview1", 621 | ] 622 | 623 | [[package]] 624 | name = "getrandom" 625 | version = "0.3.3" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 628 | dependencies = [ 629 | "cfg-if", 630 | "libc", 631 | "r-efi", 632 | "wasi 0.14.2+wasi-0.2.4", 633 | ] 634 | 635 | [[package]] 636 | name = "gif" 637 | version = "0.12.0" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" 640 | dependencies = [ 641 | "color_quant", 642 | "weezl", 643 | ] 644 | 645 | [[package]] 646 | name = "glob" 647 | version = "0.3.2" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 650 | 651 | [[package]] 652 | name = "hashbrown" 653 | version = "0.15.3" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" 656 | 657 | [[package]] 658 | name = "iana-time-zone" 659 | version = "0.1.63" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 662 | dependencies = [ 663 | "android_system_properties", 664 | "core-foundation-sys", 665 | "iana-time-zone-haiku", 666 | "js-sys", 667 | "log", 668 | "wasm-bindgen", 669 | "windows-core 0.61.0", 670 | ] 671 | 672 | [[package]] 673 | name = "iana-time-zone-haiku" 674 | version = "0.1.2" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 677 | dependencies = [ 678 | "cc", 679 | ] 680 | 681 | [[package]] 682 | name = "image" 683 | version = "0.24.9" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" 686 | dependencies = [ 687 | "bytemuck", 688 | "byteorder", 689 | "color_quant", 690 | "jpeg-decoder", 691 | "num-traits", 692 | "png", 693 | ] 694 | 695 | [[package]] 696 | name = "indexmap" 697 | version = "2.9.0" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 700 | dependencies = [ 701 | "equivalent", 702 | "hashbrown", 703 | ] 704 | 705 | [[package]] 706 | name = "instant" 707 | version = "0.1.13" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" 710 | dependencies = [ 711 | "cfg-if", 712 | "js-sys", 713 | "wasm-bindgen", 714 | "web-sys", 715 | ] 716 | 717 | [[package]] 718 | name = "itertools" 719 | version = "0.13.0" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 722 | dependencies = [ 723 | "either", 724 | ] 725 | 726 | [[package]] 727 | name = "itoa" 728 | version = "1.0.15" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 731 | 732 | [[package]] 733 | name = "jni" 734 | version = "0.21.1" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" 737 | dependencies = [ 738 | "cesu8", 739 | "cfg-if", 740 | "combine", 741 | "jni-sys", 742 | "log", 743 | "thiserror", 744 | "walkdir", 745 | "windows-sys 0.45.0", 746 | ] 747 | 748 | [[package]] 749 | name = "jni-sys" 750 | version = "0.3.0" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" 753 | 754 | [[package]] 755 | name = "jobserver" 756 | version = "0.1.33" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" 759 | dependencies = [ 760 | "getrandom 0.3.3", 761 | "libc", 762 | ] 763 | 764 | [[package]] 765 | name = "jpeg-decoder" 766 | version = "0.3.1" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" 769 | 770 | [[package]] 771 | name = "js-sys" 772 | version = "0.3.77" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 775 | dependencies = [ 776 | "once_cell", 777 | "wasm-bindgen", 778 | ] 779 | 780 | [[package]] 781 | name = "lazy_static" 782 | version = "1.5.0" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 785 | 786 | [[package]] 787 | name = "libc" 788 | version = "0.2.172" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 791 | 792 | [[package]] 793 | name = "libloading" 794 | version = "0.8.7" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" 797 | dependencies = [ 798 | "cfg-if", 799 | "windows-targets 0.53.0", 800 | ] 801 | 802 | [[package]] 803 | name = "libm" 804 | version = "0.2.15" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" 807 | 808 | [[package]] 809 | name = "libredox" 810 | version = "0.1.3" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 813 | dependencies = [ 814 | "bitflags 2.9.0", 815 | "libc", 816 | "redox_syscall", 817 | ] 818 | 819 | [[package]] 820 | name = "linux-raw-sys" 821 | version = "0.9.4" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 824 | 825 | [[package]] 826 | name = "log" 827 | version = "0.4.27" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 830 | 831 | [[package]] 832 | name = "lowpass-filter" 833 | version = "0.3.2" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "dac6061fee09145b2f2be9a0abca11730341b95ae8b47ae0c80be785a91bc06d" 836 | 837 | [[package]] 838 | name = "mach2" 839 | version = "0.4.2" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" 842 | dependencies = [ 843 | "libc", 844 | ] 845 | 846 | [[package]] 847 | name = "memchr" 848 | version = "2.7.4" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 851 | 852 | [[package]] 853 | name = "memoffset" 854 | version = "0.6.5" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 857 | dependencies = [ 858 | "autocfg", 859 | ] 860 | 861 | [[package]] 862 | name = "microfft" 863 | version = "0.6.0" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "f2b6673eb0cc536241d6734c2ca45abfdbf90e9e7791c66a36a7ba3c315b76cf" 866 | dependencies = [ 867 | "cfg-if", 868 | "num-complex", 869 | "static_assertions", 870 | ] 871 | 872 | [[package]] 873 | name = "minifb" 874 | version = "0.28.0" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "d1a093126f2ed9012fc0b146934c97eb0273e54983680a8bf5309b6b4a365b32" 877 | dependencies = [ 878 | "cc", 879 | "console_error_panic_hook", 880 | "dlib", 881 | "futures", 882 | "instant", 883 | "js-sys", 884 | "lazy_static", 885 | "libc", 886 | "orbclient", 887 | "raw-window-handle", 888 | "serde", 889 | "serde_derive", 890 | "tempfile", 891 | "wasm-bindgen", 892 | "wasm-bindgen-futures", 893 | "wayland-client", 894 | "wayland-cursor", 895 | "wayland-protocols", 896 | "web-sys", 897 | "winapi", 898 | "x11-dl", 899 | ] 900 | 901 | [[package]] 902 | name = "minimal-lexical" 903 | version = "0.2.1" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 906 | 907 | [[package]] 908 | name = "miniz_oxide" 909 | version = "0.8.8" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 912 | dependencies = [ 913 | "adler2", 914 | "simd-adler32", 915 | ] 916 | 917 | [[package]] 918 | name = "ndk" 919 | version = "0.8.0" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" 922 | dependencies = [ 923 | "bitflags 2.9.0", 924 | "jni-sys", 925 | "log", 926 | "ndk-sys", 927 | "num_enum", 928 | "thiserror", 929 | ] 930 | 931 | [[package]] 932 | name = "ndk-context" 933 | version = "0.1.1" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" 936 | 937 | [[package]] 938 | name = "ndk-sys" 939 | version = "0.5.0+25.2.9519653" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" 942 | dependencies = [ 943 | "jni-sys", 944 | ] 945 | 946 | [[package]] 947 | name = "nix" 948 | version = "0.24.3" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" 951 | dependencies = [ 952 | "bitflags 1.3.2", 953 | "cfg-if", 954 | "libc", 955 | "memoffset", 956 | ] 957 | 958 | [[package]] 959 | name = "nom" 960 | version = "7.1.3" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 963 | dependencies = [ 964 | "memchr", 965 | "minimal-lexical", 966 | ] 967 | 968 | [[package]] 969 | name = "num-complex" 970 | version = "0.4.6" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" 973 | dependencies = [ 974 | "num-traits", 975 | ] 976 | 977 | [[package]] 978 | name = "num-derive" 979 | version = "0.4.2" 980 | source = "registry+https://github.com/rust-lang/crates.io-index" 981 | checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" 982 | dependencies = [ 983 | "proc-macro2", 984 | "quote", 985 | "syn", 986 | ] 987 | 988 | [[package]] 989 | name = "num-traits" 990 | version = "0.2.19" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 993 | dependencies = [ 994 | "autocfg", 995 | "libm", 996 | ] 997 | 998 | [[package]] 999 | name = "num_enum" 1000 | version = "0.7.3" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" 1003 | dependencies = [ 1004 | "num_enum_derive", 1005 | ] 1006 | 1007 | [[package]] 1008 | name = "num_enum_derive" 1009 | version = "0.7.3" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" 1012 | dependencies = [ 1013 | "proc-macro-crate", 1014 | "proc-macro2", 1015 | "quote", 1016 | "syn", 1017 | ] 1018 | 1019 | [[package]] 1020 | name = "oboe" 1021 | version = "0.6.1" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" 1024 | dependencies = [ 1025 | "jni", 1026 | "ndk", 1027 | "ndk-context", 1028 | "num-derive", 1029 | "num-traits", 1030 | "oboe-sys", 1031 | ] 1032 | 1033 | [[package]] 1034 | name = "oboe-sys" 1035 | version = "0.6.1" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" 1038 | dependencies = [ 1039 | "cc", 1040 | ] 1041 | 1042 | [[package]] 1043 | name = "once_cell" 1044 | version = "1.21.3" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 1047 | 1048 | [[package]] 1049 | name = "option-ext" 1050 | version = "0.2.0" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 1053 | 1054 | [[package]] 1055 | name = "orbclient" 1056 | version = "0.3.48" 1057 | source = "registry+https://github.com/rust-lang/crates.io-index" 1058 | checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" 1059 | dependencies = [ 1060 | "libc", 1061 | "libredox", 1062 | "sdl2", 1063 | "sdl2-sys", 1064 | ] 1065 | 1066 | [[package]] 1067 | name = "paste" 1068 | version = "1.0.15" 1069 | source = "registry+https://github.com/rust-lang/crates.io-index" 1070 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 1071 | 1072 | [[package]] 1073 | name = "pathfinder_geometry" 1074 | version = "0.5.1" 1075 | source = "registry+https://github.com/rust-lang/crates.io-index" 1076 | checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" 1077 | dependencies = [ 1078 | "log", 1079 | "pathfinder_simd", 1080 | ] 1081 | 1082 | [[package]] 1083 | name = "pathfinder_simd" 1084 | version = "0.5.4" 1085 | source = "registry+https://github.com/rust-lang/crates.io-index" 1086 | checksum = "5cf07ef4804cfa9aea3b04a7bbdd5a40031dbb6b4f2cbaf2b011666c80c5b4f2" 1087 | dependencies = [ 1088 | "rustc_version", 1089 | ] 1090 | 1091 | [[package]] 1092 | name = "pin-project-lite" 1093 | version = "0.2.16" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1096 | 1097 | [[package]] 1098 | name = "pin-utils" 1099 | version = "0.1.0" 1100 | source = "registry+https://github.com/rust-lang/crates.io-index" 1101 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1102 | 1103 | [[package]] 1104 | name = "pkg-config" 1105 | version = "0.3.32" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1108 | 1109 | [[package]] 1110 | name = "plotters" 1111 | version = "0.3.7" 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" 1113 | checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" 1114 | dependencies = [ 1115 | "chrono", 1116 | "font-kit", 1117 | "image", 1118 | "lazy_static", 1119 | "num-traits", 1120 | "pathfinder_geometry", 1121 | "plotters-backend", 1122 | "plotters-bitmap", 1123 | "plotters-svg", 1124 | "ttf-parser", 1125 | "wasm-bindgen", 1126 | "web-sys", 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "plotters-backend" 1131 | version = "0.3.7" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" 1134 | 1135 | [[package]] 1136 | name = "plotters-bitmap" 1137 | version = "0.3.7" 1138 | source = "registry+https://github.com/rust-lang/crates.io-index" 1139 | checksum = "72ce181e3f6bf82d6c1dc569103ca7b1bd964c60ba03d7e6cdfbb3e3eb7f7405" 1140 | dependencies = [ 1141 | "gif", 1142 | "image", 1143 | "plotters-backend", 1144 | ] 1145 | 1146 | [[package]] 1147 | name = "plotters-svg" 1148 | version = "0.3.7" 1149 | source = "registry+https://github.com/rust-lang/crates.io-index" 1150 | checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" 1151 | dependencies = [ 1152 | "plotters-backend", 1153 | ] 1154 | 1155 | [[package]] 1156 | name = "png" 1157 | version = "0.17.16" 1158 | source = "registry+https://github.com/rust-lang/crates.io-index" 1159 | checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" 1160 | dependencies = [ 1161 | "bitflags 1.3.2", 1162 | "crc32fast", 1163 | "fdeflate", 1164 | "flate2", 1165 | "miniz_oxide", 1166 | ] 1167 | 1168 | [[package]] 1169 | name = "proc-macro-crate" 1170 | version = "3.3.0" 1171 | source = "registry+https://github.com/rust-lang/crates.io-index" 1172 | checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" 1173 | dependencies = [ 1174 | "toml_edit", 1175 | ] 1176 | 1177 | [[package]] 1178 | name = "proc-macro2" 1179 | version = "1.0.95" 1180 | source = "registry+https://github.com/rust-lang/crates.io-index" 1181 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 1182 | dependencies = [ 1183 | "unicode-ident", 1184 | ] 1185 | 1186 | [[package]] 1187 | name = "quote" 1188 | version = "1.0.40" 1189 | source = "registry+https://github.com/rust-lang/crates.io-index" 1190 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 1191 | dependencies = [ 1192 | "proc-macro2", 1193 | ] 1194 | 1195 | [[package]] 1196 | name = "r-efi" 1197 | version = "5.2.0" 1198 | source = "registry+https://github.com/rust-lang/crates.io-index" 1199 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 1200 | 1201 | [[package]] 1202 | name = "raw-window-handle" 1203 | version = "0.6.2" 1204 | source = "registry+https://github.com/rust-lang/crates.io-index" 1205 | checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" 1206 | 1207 | [[package]] 1208 | name = "redox_syscall" 1209 | version = "0.5.12" 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" 1211 | checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" 1212 | dependencies = [ 1213 | "bitflags 2.9.0", 1214 | ] 1215 | 1216 | [[package]] 1217 | name = "redox_users" 1218 | version = "0.4.6" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" 1221 | dependencies = [ 1222 | "getrandom 0.2.16", 1223 | "libredox", 1224 | "thiserror", 1225 | ] 1226 | 1227 | [[package]] 1228 | name = "regex" 1229 | version = "1.11.1" 1230 | source = "registry+https://github.com/rust-lang/crates.io-index" 1231 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1232 | dependencies = [ 1233 | "aho-corasick", 1234 | "memchr", 1235 | "regex-automata", 1236 | "regex-syntax", 1237 | ] 1238 | 1239 | [[package]] 1240 | name = "regex-automata" 1241 | version = "0.4.9" 1242 | source = "registry+https://github.com/rust-lang/crates.io-index" 1243 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1244 | dependencies = [ 1245 | "aho-corasick", 1246 | "memchr", 1247 | "regex-syntax", 1248 | ] 1249 | 1250 | [[package]] 1251 | name = "regex-syntax" 1252 | version = "0.8.5" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1255 | 1256 | [[package]] 1257 | name = "ringbuffer" 1258 | version = "0.15.0" 1259 | source = "registry+https://github.com/rust-lang/crates.io-index" 1260 | checksum = "3df6368f71f205ff9c33c076d170dd56ebf68e8161c733c0caa07a7a5509ed53" 1261 | 1262 | [[package]] 1263 | name = "rustc-hash" 1264 | version = "1.1.0" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 1267 | 1268 | [[package]] 1269 | name = "rustc_version" 1270 | version = "0.4.1" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 1273 | dependencies = [ 1274 | "semver", 1275 | ] 1276 | 1277 | [[package]] 1278 | name = "rustix" 1279 | version = "1.0.7" 1280 | source = "registry+https://github.com/rust-lang/crates.io-index" 1281 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 1282 | dependencies = [ 1283 | "bitflags 2.9.0", 1284 | "errno", 1285 | "libc", 1286 | "linux-raw-sys", 1287 | "windows-sys 0.59.0", 1288 | ] 1289 | 1290 | [[package]] 1291 | name = "rustversion" 1292 | version = "1.0.20" 1293 | source = "registry+https://github.com/rust-lang/crates.io-index" 1294 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 1295 | 1296 | [[package]] 1297 | name = "ryu" 1298 | version = "1.0.20" 1299 | source = "registry+https://github.com/rust-lang/crates.io-index" 1300 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1301 | 1302 | [[package]] 1303 | name = "same-file" 1304 | version = "1.0.6" 1305 | source = "registry+https://github.com/rust-lang/crates.io-index" 1306 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1307 | dependencies = [ 1308 | "winapi-util", 1309 | ] 1310 | 1311 | [[package]] 1312 | name = "scoped-tls" 1313 | version = "1.0.1" 1314 | source = "registry+https://github.com/rust-lang/crates.io-index" 1315 | checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 1316 | 1317 | [[package]] 1318 | name = "sdl2" 1319 | version = "0.35.2" 1320 | source = "registry+https://github.com/rust-lang/crates.io-index" 1321 | checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a" 1322 | dependencies = [ 1323 | "bitflags 1.3.2", 1324 | "lazy_static", 1325 | "libc", 1326 | "sdl2-sys", 1327 | ] 1328 | 1329 | [[package]] 1330 | name = "sdl2-sys" 1331 | version = "0.35.2" 1332 | source = "registry+https://github.com/rust-lang/crates.io-index" 1333 | checksum = "e3586be2cf6c0a8099a79a12b4084357aa9b3e0b0d7980e3b67aaf7a9d55f9f0" 1334 | dependencies = [ 1335 | "cfg-if", 1336 | "libc", 1337 | "version-compare", 1338 | ] 1339 | 1340 | [[package]] 1341 | name = "semver" 1342 | version = "1.0.26" 1343 | source = "registry+https://github.com/rust-lang/crates.io-index" 1344 | checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 1345 | 1346 | [[package]] 1347 | name = "serde" 1348 | version = "1.0.219" 1349 | source = "registry+https://github.com/rust-lang/crates.io-index" 1350 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1351 | dependencies = [ 1352 | "serde_derive", 1353 | ] 1354 | 1355 | [[package]] 1356 | name = "serde_derive" 1357 | version = "1.0.219" 1358 | source = "registry+https://github.com/rust-lang/crates.io-index" 1359 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1360 | dependencies = [ 1361 | "proc-macro2", 1362 | "quote", 1363 | "syn", 1364 | ] 1365 | 1366 | [[package]] 1367 | name = "serde_json" 1368 | version = "1.0.140" 1369 | source = "registry+https://github.com/rust-lang/crates.io-index" 1370 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1371 | dependencies = [ 1372 | "itoa", 1373 | "memchr", 1374 | "ryu", 1375 | "serde", 1376 | ] 1377 | 1378 | [[package]] 1379 | name = "shlex" 1380 | version = "1.3.0" 1381 | source = "registry+https://github.com/rust-lang/crates.io-index" 1382 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1383 | 1384 | [[package]] 1385 | name = "simd-adler32" 1386 | version = "0.3.7" 1387 | source = "registry+https://github.com/rust-lang/crates.io-index" 1388 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 1389 | 1390 | [[package]] 1391 | name = "slab" 1392 | version = "0.4.9" 1393 | source = "registry+https://github.com/rust-lang/crates.io-index" 1394 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1395 | dependencies = [ 1396 | "autocfg", 1397 | ] 1398 | 1399 | [[package]] 1400 | name = "smallvec" 1401 | version = "1.15.0" 1402 | source = "registry+https://github.com/rust-lang/crates.io-index" 1403 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 1404 | 1405 | [[package]] 1406 | name = "spectrum-analyzer" 1407 | version = "1.7.0" 1408 | source = "registry+https://github.com/rust-lang/crates.io-index" 1409 | checksum = "5f5353405212be77e7f9e95aa77934f0aafb0f80c586571680b9c20ce5bae758" 1410 | dependencies = [ 1411 | "float-cmp", 1412 | "libm", 1413 | "microfft", 1414 | "paste", 1415 | ] 1416 | 1417 | [[package]] 1418 | name = "static_assertions" 1419 | version = "1.1.0" 1420 | source = "registry+https://github.com/rust-lang/crates.io-index" 1421 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1422 | 1423 | [[package]] 1424 | name = "symphonia" 1425 | version = "0.5.4" 1426 | source = "registry+https://github.com/rust-lang/crates.io-index" 1427 | checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" 1428 | dependencies = [ 1429 | "lazy_static", 1430 | "symphonia-bundle-mp3", 1431 | "symphonia-core", 1432 | "symphonia-metadata", 1433 | ] 1434 | 1435 | [[package]] 1436 | name = "symphonia-bundle-mp3" 1437 | version = "0.5.4" 1438 | source = "registry+https://github.com/rust-lang/crates.io-index" 1439 | checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" 1440 | dependencies = [ 1441 | "lazy_static", 1442 | "log", 1443 | "symphonia-core", 1444 | "symphonia-metadata", 1445 | ] 1446 | 1447 | [[package]] 1448 | name = "symphonia-core" 1449 | version = "0.5.4" 1450 | source = "registry+https://github.com/rust-lang/crates.io-index" 1451 | checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" 1452 | dependencies = [ 1453 | "arrayvec", 1454 | "bitflags 1.3.2", 1455 | "bytemuck", 1456 | "lazy_static", 1457 | "log", 1458 | ] 1459 | 1460 | [[package]] 1461 | name = "symphonia-metadata" 1462 | version = "0.5.4" 1463 | source = "registry+https://github.com/rust-lang/crates.io-index" 1464 | checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" 1465 | dependencies = [ 1466 | "encoding_rs", 1467 | "lazy_static", 1468 | "log", 1469 | "symphonia-core", 1470 | ] 1471 | 1472 | [[package]] 1473 | name = "syn" 1474 | version = "2.0.101" 1475 | source = "registry+https://github.com/rust-lang/crates.io-index" 1476 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 1477 | dependencies = [ 1478 | "proc-macro2", 1479 | "quote", 1480 | "unicode-ident", 1481 | ] 1482 | 1483 | [[package]] 1484 | name = "tempfile" 1485 | version = "3.19.1" 1486 | source = "registry+https://github.com/rust-lang/crates.io-index" 1487 | checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 1488 | dependencies = [ 1489 | "fastrand", 1490 | "getrandom 0.3.3", 1491 | "once_cell", 1492 | "rustix", 1493 | "windows-sys 0.59.0", 1494 | ] 1495 | 1496 | [[package]] 1497 | name = "thiserror" 1498 | version = "1.0.69" 1499 | source = "registry+https://github.com/rust-lang/crates.io-index" 1500 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1501 | dependencies = [ 1502 | "thiserror-impl", 1503 | ] 1504 | 1505 | [[package]] 1506 | name = "thiserror-impl" 1507 | version = "1.0.69" 1508 | source = "registry+https://github.com/rust-lang/crates.io-index" 1509 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1510 | dependencies = [ 1511 | "proc-macro2", 1512 | "quote", 1513 | "syn", 1514 | ] 1515 | 1516 | [[package]] 1517 | name = "toml_datetime" 1518 | version = "0.6.9" 1519 | source = "registry+https://github.com/rust-lang/crates.io-index" 1520 | checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" 1521 | 1522 | [[package]] 1523 | name = "toml_edit" 1524 | version = "0.22.26" 1525 | source = "registry+https://github.com/rust-lang/crates.io-index" 1526 | checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" 1527 | dependencies = [ 1528 | "indexmap", 1529 | "toml_datetime", 1530 | "winnow", 1531 | ] 1532 | 1533 | [[package]] 1534 | name = "ttf-parser" 1535 | version = "0.20.0" 1536 | source = "registry+https://github.com/rust-lang/crates.io-index" 1537 | checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" 1538 | 1539 | [[package]] 1540 | name = "unicode-ident" 1541 | version = "1.0.18" 1542 | source = "registry+https://github.com/rust-lang/crates.io-index" 1543 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1544 | 1545 | [[package]] 1546 | name = "version-compare" 1547 | version = "0.1.1" 1548 | source = "registry+https://github.com/rust-lang/crates.io-index" 1549 | checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" 1550 | 1551 | [[package]] 1552 | name = "walkdir" 1553 | version = "2.5.0" 1554 | source = "registry+https://github.com/rust-lang/crates.io-index" 1555 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1556 | dependencies = [ 1557 | "same-file", 1558 | "winapi-util", 1559 | ] 1560 | 1561 | [[package]] 1562 | name = "wasi" 1563 | version = "0.11.0+wasi-snapshot-preview1" 1564 | source = "registry+https://github.com/rust-lang/crates.io-index" 1565 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1566 | 1567 | [[package]] 1568 | name = "wasi" 1569 | version = "0.14.2+wasi-0.2.4" 1570 | source = "registry+https://github.com/rust-lang/crates.io-index" 1571 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1572 | dependencies = [ 1573 | "wit-bindgen-rt", 1574 | ] 1575 | 1576 | [[package]] 1577 | name = "wasm-bindgen" 1578 | version = "0.2.100" 1579 | source = "registry+https://github.com/rust-lang/crates.io-index" 1580 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1581 | dependencies = [ 1582 | "cfg-if", 1583 | "once_cell", 1584 | "rustversion", 1585 | "serde", 1586 | "serde_json", 1587 | "wasm-bindgen-macro", 1588 | ] 1589 | 1590 | [[package]] 1591 | name = "wasm-bindgen-backend" 1592 | version = "0.2.100" 1593 | source = "registry+https://github.com/rust-lang/crates.io-index" 1594 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1595 | dependencies = [ 1596 | "bumpalo", 1597 | "log", 1598 | "proc-macro2", 1599 | "quote", 1600 | "syn", 1601 | "wasm-bindgen-shared", 1602 | ] 1603 | 1604 | [[package]] 1605 | name = "wasm-bindgen-futures" 1606 | version = "0.4.50" 1607 | source = "registry+https://github.com/rust-lang/crates.io-index" 1608 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 1609 | dependencies = [ 1610 | "cfg-if", 1611 | "js-sys", 1612 | "once_cell", 1613 | "wasm-bindgen", 1614 | "web-sys", 1615 | ] 1616 | 1617 | [[package]] 1618 | name = "wasm-bindgen-macro" 1619 | version = "0.2.100" 1620 | source = "registry+https://github.com/rust-lang/crates.io-index" 1621 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1622 | dependencies = [ 1623 | "quote", 1624 | "wasm-bindgen-macro-support", 1625 | ] 1626 | 1627 | [[package]] 1628 | name = "wasm-bindgen-macro-support" 1629 | version = "0.2.100" 1630 | source = "registry+https://github.com/rust-lang/crates.io-index" 1631 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1632 | dependencies = [ 1633 | "proc-macro2", 1634 | "quote", 1635 | "syn", 1636 | "wasm-bindgen-backend", 1637 | "wasm-bindgen-shared", 1638 | ] 1639 | 1640 | [[package]] 1641 | name = "wasm-bindgen-shared" 1642 | version = "0.2.100" 1643 | source = "registry+https://github.com/rust-lang/crates.io-index" 1644 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1645 | dependencies = [ 1646 | "unicode-ident", 1647 | ] 1648 | 1649 | [[package]] 1650 | name = "wayland-client" 1651 | version = "0.29.5" 1652 | source = "registry+https://github.com/rust-lang/crates.io-index" 1653 | checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" 1654 | dependencies = [ 1655 | "bitflags 1.3.2", 1656 | "downcast-rs", 1657 | "libc", 1658 | "nix", 1659 | "scoped-tls", 1660 | "wayland-commons", 1661 | "wayland-scanner", 1662 | "wayland-sys", 1663 | ] 1664 | 1665 | [[package]] 1666 | name = "wayland-commons" 1667 | version = "0.29.5" 1668 | source = "registry+https://github.com/rust-lang/crates.io-index" 1669 | checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" 1670 | dependencies = [ 1671 | "nix", 1672 | "once_cell", 1673 | "smallvec", 1674 | "wayland-sys", 1675 | ] 1676 | 1677 | [[package]] 1678 | name = "wayland-cursor" 1679 | version = "0.29.5" 1680 | source = "registry+https://github.com/rust-lang/crates.io-index" 1681 | checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" 1682 | dependencies = [ 1683 | "nix", 1684 | "wayland-client", 1685 | "xcursor", 1686 | ] 1687 | 1688 | [[package]] 1689 | name = "wayland-protocols" 1690 | version = "0.29.5" 1691 | source = "registry+https://github.com/rust-lang/crates.io-index" 1692 | checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6" 1693 | dependencies = [ 1694 | "bitflags 1.3.2", 1695 | "wayland-client", 1696 | "wayland-commons", 1697 | "wayland-scanner", 1698 | ] 1699 | 1700 | [[package]] 1701 | name = "wayland-scanner" 1702 | version = "0.29.5" 1703 | source = "registry+https://github.com/rust-lang/crates.io-index" 1704 | checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" 1705 | dependencies = [ 1706 | "proc-macro2", 1707 | "quote", 1708 | "xml-rs", 1709 | ] 1710 | 1711 | [[package]] 1712 | name = "wayland-sys" 1713 | version = "0.29.5" 1714 | source = "registry+https://github.com/rust-lang/crates.io-index" 1715 | checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" 1716 | dependencies = [ 1717 | "dlib", 1718 | "lazy_static", 1719 | "pkg-config", 1720 | ] 1721 | 1722 | [[package]] 1723 | name = "web-sys" 1724 | version = "0.3.77" 1725 | source = "registry+https://github.com/rust-lang/crates.io-index" 1726 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 1727 | dependencies = [ 1728 | "js-sys", 1729 | "wasm-bindgen", 1730 | ] 1731 | 1732 | [[package]] 1733 | name = "weezl" 1734 | version = "0.1.8" 1735 | source = "registry+https://github.com/rust-lang/crates.io-index" 1736 | checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" 1737 | 1738 | [[package]] 1739 | name = "winapi" 1740 | version = "0.3.9" 1741 | source = "registry+https://github.com/rust-lang/crates.io-index" 1742 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1743 | dependencies = [ 1744 | "winapi-i686-pc-windows-gnu", 1745 | "winapi-x86_64-pc-windows-gnu", 1746 | ] 1747 | 1748 | [[package]] 1749 | name = "winapi-i686-pc-windows-gnu" 1750 | version = "0.4.0" 1751 | source = "registry+https://github.com/rust-lang/crates.io-index" 1752 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1753 | 1754 | [[package]] 1755 | name = "winapi-util" 1756 | version = "0.1.9" 1757 | source = "registry+https://github.com/rust-lang/crates.io-index" 1758 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 1759 | dependencies = [ 1760 | "windows-sys 0.59.0", 1761 | ] 1762 | 1763 | [[package]] 1764 | name = "winapi-x86_64-pc-windows-gnu" 1765 | version = "0.4.0" 1766 | source = "registry+https://github.com/rust-lang/crates.io-index" 1767 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1768 | 1769 | [[package]] 1770 | name = "windows" 1771 | version = "0.54.0" 1772 | source = "registry+https://github.com/rust-lang/crates.io-index" 1773 | checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" 1774 | dependencies = [ 1775 | "windows-core 0.54.0", 1776 | "windows-targets 0.52.6", 1777 | ] 1778 | 1779 | [[package]] 1780 | name = "windows-core" 1781 | version = "0.54.0" 1782 | source = "registry+https://github.com/rust-lang/crates.io-index" 1783 | checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" 1784 | dependencies = [ 1785 | "windows-result 0.1.2", 1786 | "windows-targets 0.52.6", 1787 | ] 1788 | 1789 | [[package]] 1790 | name = "windows-core" 1791 | version = "0.61.0" 1792 | source = "registry+https://github.com/rust-lang/crates.io-index" 1793 | checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" 1794 | dependencies = [ 1795 | "windows-implement", 1796 | "windows-interface", 1797 | "windows-link", 1798 | "windows-result 0.3.2", 1799 | "windows-strings", 1800 | ] 1801 | 1802 | [[package]] 1803 | name = "windows-implement" 1804 | version = "0.60.0" 1805 | source = "registry+https://github.com/rust-lang/crates.io-index" 1806 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 1807 | dependencies = [ 1808 | "proc-macro2", 1809 | "quote", 1810 | "syn", 1811 | ] 1812 | 1813 | [[package]] 1814 | name = "windows-interface" 1815 | version = "0.59.1" 1816 | source = "registry+https://github.com/rust-lang/crates.io-index" 1817 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 1818 | dependencies = [ 1819 | "proc-macro2", 1820 | "quote", 1821 | "syn", 1822 | ] 1823 | 1824 | [[package]] 1825 | name = "windows-link" 1826 | version = "0.1.1" 1827 | source = "registry+https://github.com/rust-lang/crates.io-index" 1828 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 1829 | 1830 | [[package]] 1831 | name = "windows-result" 1832 | version = "0.1.2" 1833 | source = "registry+https://github.com/rust-lang/crates.io-index" 1834 | checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" 1835 | dependencies = [ 1836 | "windows-targets 0.52.6", 1837 | ] 1838 | 1839 | [[package]] 1840 | name = "windows-result" 1841 | version = "0.3.2" 1842 | source = "registry+https://github.com/rust-lang/crates.io-index" 1843 | checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" 1844 | dependencies = [ 1845 | "windows-link", 1846 | ] 1847 | 1848 | [[package]] 1849 | name = "windows-strings" 1850 | version = "0.4.0" 1851 | source = "registry+https://github.com/rust-lang/crates.io-index" 1852 | checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" 1853 | dependencies = [ 1854 | "windows-link", 1855 | ] 1856 | 1857 | [[package]] 1858 | name = "windows-sys" 1859 | version = "0.45.0" 1860 | source = "registry+https://github.com/rust-lang/crates.io-index" 1861 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 1862 | dependencies = [ 1863 | "windows-targets 0.42.2", 1864 | ] 1865 | 1866 | [[package]] 1867 | name = "windows-sys" 1868 | version = "0.48.0" 1869 | source = "registry+https://github.com/rust-lang/crates.io-index" 1870 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1871 | dependencies = [ 1872 | "windows-targets 0.48.5", 1873 | ] 1874 | 1875 | [[package]] 1876 | name = "windows-sys" 1877 | version = "0.59.0" 1878 | source = "registry+https://github.com/rust-lang/crates.io-index" 1879 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1880 | dependencies = [ 1881 | "windows-targets 0.52.6", 1882 | ] 1883 | 1884 | [[package]] 1885 | name = "windows-targets" 1886 | version = "0.42.2" 1887 | source = "registry+https://github.com/rust-lang/crates.io-index" 1888 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 1889 | dependencies = [ 1890 | "windows_aarch64_gnullvm 0.42.2", 1891 | "windows_aarch64_msvc 0.42.2", 1892 | "windows_i686_gnu 0.42.2", 1893 | "windows_i686_msvc 0.42.2", 1894 | "windows_x86_64_gnu 0.42.2", 1895 | "windows_x86_64_gnullvm 0.42.2", 1896 | "windows_x86_64_msvc 0.42.2", 1897 | ] 1898 | 1899 | [[package]] 1900 | name = "windows-targets" 1901 | version = "0.48.5" 1902 | source = "registry+https://github.com/rust-lang/crates.io-index" 1903 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1904 | dependencies = [ 1905 | "windows_aarch64_gnullvm 0.48.5", 1906 | "windows_aarch64_msvc 0.48.5", 1907 | "windows_i686_gnu 0.48.5", 1908 | "windows_i686_msvc 0.48.5", 1909 | "windows_x86_64_gnu 0.48.5", 1910 | "windows_x86_64_gnullvm 0.48.5", 1911 | "windows_x86_64_msvc 0.48.5", 1912 | ] 1913 | 1914 | [[package]] 1915 | name = "windows-targets" 1916 | version = "0.52.6" 1917 | source = "registry+https://github.com/rust-lang/crates.io-index" 1918 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1919 | dependencies = [ 1920 | "windows_aarch64_gnullvm 0.52.6", 1921 | "windows_aarch64_msvc 0.52.6", 1922 | "windows_i686_gnu 0.52.6", 1923 | "windows_i686_gnullvm 0.52.6", 1924 | "windows_i686_msvc 0.52.6", 1925 | "windows_x86_64_gnu 0.52.6", 1926 | "windows_x86_64_gnullvm 0.52.6", 1927 | "windows_x86_64_msvc 0.52.6", 1928 | ] 1929 | 1930 | [[package]] 1931 | name = "windows-targets" 1932 | version = "0.53.0" 1933 | source = "registry+https://github.com/rust-lang/crates.io-index" 1934 | checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" 1935 | dependencies = [ 1936 | "windows_aarch64_gnullvm 0.53.0", 1937 | "windows_aarch64_msvc 0.53.0", 1938 | "windows_i686_gnu 0.53.0", 1939 | "windows_i686_gnullvm 0.53.0", 1940 | "windows_i686_msvc 0.53.0", 1941 | "windows_x86_64_gnu 0.53.0", 1942 | "windows_x86_64_gnullvm 0.53.0", 1943 | "windows_x86_64_msvc 0.53.0", 1944 | ] 1945 | 1946 | [[package]] 1947 | name = "windows_aarch64_gnullvm" 1948 | version = "0.42.2" 1949 | source = "registry+https://github.com/rust-lang/crates.io-index" 1950 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 1951 | 1952 | [[package]] 1953 | name = "windows_aarch64_gnullvm" 1954 | version = "0.48.5" 1955 | source = "registry+https://github.com/rust-lang/crates.io-index" 1956 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1957 | 1958 | [[package]] 1959 | name = "windows_aarch64_gnullvm" 1960 | version = "0.52.6" 1961 | source = "registry+https://github.com/rust-lang/crates.io-index" 1962 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1963 | 1964 | [[package]] 1965 | name = "windows_aarch64_gnullvm" 1966 | version = "0.53.0" 1967 | source = "registry+https://github.com/rust-lang/crates.io-index" 1968 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 1969 | 1970 | [[package]] 1971 | name = "windows_aarch64_msvc" 1972 | version = "0.42.2" 1973 | source = "registry+https://github.com/rust-lang/crates.io-index" 1974 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 1975 | 1976 | [[package]] 1977 | name = "windows_aarch64_msvc" 1978 | version = "0.48.5" 1979 | source = "registry+https://github.com/rust-lang/crates.io-index" 1980 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1981 | 1982 | [[package]] 1983 | name = "windows_aarch64_msvc" 1984 | version = "0.52.6" 1985 | source = "registry+https://github.com/rust-lang/crates.io-index" 1986 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1987 | 1988 | [[package]] 1989 | name = "windows_aarch64_msvc" 1990 | version = "0.53.0" 1991 | source = "registry+https://github.com/rust-lang/crates.io-index" 1992 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 1993 | 1994 | [[package]] 1995 | name = "windows_i686_gnu" 1996 | version = "0.42.2" 1997 | source = "registry+https://github.com/rust-lang/crates.io-index" 1998 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 1999 | 2000 | [[package]] 2001 | name = "windows_i686_gnu" 2002 | version = "0.48.5" 2003 | source = "registry+https://github.com/rust-lang/crates.io-index" 2004 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 2005 | 2006 | [[package]] 2007 | name = "windows_i686_gnu" 2008 | version = "0.52.6" 2009 | source = "registry+https://github.com/rust-lang/crates.io-index" 2010 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2011 | 2012 | [[package]] 2013 | name = "windows_i686_gnu" 2014 | version = "0.53.0" 2015 | source = "registry+https://github.com/rust-lang/crates.io-index" 2016 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 2017 | 2018 | [[package]] 2019 | name = "windows_i686_gnullvm" 2020 | version = "0.52.6" 2021 | source = "registry+https://github.com/rust-lang/crates.io-index" 2022 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2023 | 2024 | [[package]] 2025 | name = "windows_i686_gnullvm" 2026 | version = "0.53.0" 2027 | source = "registry+https://github.com/rust-lang/crates.io-index" 2028 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 2029 | 2030 | [[package]] 2031 | name = "windows_i686_msvc" 2032 | version = "0.42.2" 2033 | source = "registry+https://github.com/rust-lang/crates.io-index" 2034 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 2035 | 2036 | [[package]] 2037 | name = "windows_i686_msvc" 2038 | version = "0.48.5" 2039 | source = "registry+https://github.com/rust-lang/crates.io-index" 2040 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 2041 | 2042 | [[package]] 2043 | name = "windows_i686_msvc" 2044 | version = "0.52.6" 2045 | source = "registry+https://github.com/rust-lang/crates.io-index" 2046 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2047 | 2048 | [[package]] 2049 | name = "windows_i686_msvc" 2050 | version = "0.53.0" 2051 | source = "registry+https://github.com/rust-lang/crates.io-index" 2052 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 2053 | 2054 | [[package]] 2055 | name = "windows_x86_64_gnu" 2056 | version = "0.42.2" 2057 | source = "registry+https://github.com/rust-lang/crates.io-index" 2058 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 2059 | 2060 | [[package]] 2061 | name = "windows_x86_64_gnu" 2062 | version = "0.48.5" 2063 | source = "registry+https://github.com/rust-lang/crates.io-index" 2064 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 2065 | 2066 | [[package]] 2067 | name = "windows_x86_64_gnu" 2068 | version = "0.52.6" 2069 | source = "registry+https://github.com/rust-lang/crates.io-index" 2070 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2071 | 2072 | [[package]] 2073 | name = "windows_x86_64_gnu" 2074 | version = "0.53.0" 2075 | source = "registry+https://github.com/rust-lang/crates.io-index" 2076 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 2077 | 2078 | [[package]] 2079 | name = "windows_x86_64_gnullvm" 2080 | version = "0.42.2" 2081 | source = "registry+https://github.com/rust-lang/crates.io-index" 2082 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 2083 | 2084 | [[package]] 2085 | name = "windows_x86_64_gnullvm" 2086 | version = "0.48.5" 2087 | source = "registry+https://github.com/rust-lang/crates.io-index" 2088 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 2089 | 2090 | [[package]] 2091 | name = "windows_x86_64_gnullvm" 2092 | version = "0.52.6" 2093 | source = "registry+https://github.com/rust-lang/crates.io-index" 2094 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2095 | 2096 | [[package]] 2097 | name = "windows_x86_64_gnullvm" 2098 | version = "0.53.0" 2099 | source = "registry+https://github.com/rust-lang/crates.io-index" 2100 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 2101 | 2102 | [[package]] 2103 | name = "windows_x86_64_msvc" 2104 | version = "0.42.2" 2105 | source = "registry+https://github.com/rust-lang/crates.io-index" 2106 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 2107 | 2108 | [[package]] 2109 | name = "windows_x86_64_msvc" 2110 | version = "0.48.5" 2111 | source = "registry+https://github.com/rust-lang/crates.io-index" 2112 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 2113 | 2114 | [[package]] 2115 | name = "windows_x86_64_msvc" 2116 | version = "0.52.6" 2117 | source = "registry+https://github.com/rust-lang/crates.io-index" 2118 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2119 | 2120 | [[package]] 2121 | name = "windows_x86_64_msvc" 2122 | version = "0.53.0" 2123 | source = "registry+https://github.com/rust-lang/crates.io-index" 2124 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 2125 | 2126 | [[package]] 2127 | name = "winnow" 2128 | version = "0.7.10" 2129 | source = "registry+https://github.com/rust-lang/crates.io-index" 2130 | checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" 2131 | dependencies = [ 2132 | "memchr", 2133 | ] 2134 | 2135 | [[package]] 2136 | name = "wio" 2137 | version = "0.2.2" 2138 | source = "registry+https://github.com/rust-lang/crates.io-index" 2139 | checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" 2140 | dependencies = [ 2141 | "winapi", 2142 | ] 2143 | 2144 | [[package]] 2145 | name = "wit-bindgen-rt" 2146 | version = "0.39.0" 2147 | source = "registry+https://github.com/rust-lang/crates.io-index" 2148 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 2149 | dependencies = [ 2150 | "bitflags 2.9.0", 2151 | ] 2152 | 2153 | [[package]] 2154 | name = "x11-dl" 2155 | version = "2.21.0" 2156 | source = "registry+https://github.com/rust-lang/crates.io-index" 2157 | checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" 2158 | dependencies = [ 2159 | "libc", 2160 | "once_cell", 2161 | "pkg-config", 2162 | ] 2163 | 2164 | [[package]] 2165 | name = "xcursor" 2166 | version = "0.3.8" 2167 | source = "registry+https://github.com/rust-lang/crates.io-index" 2168 | checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" 2169 | 2170 | [[package]] 2171 | name = "xml-rs" 2172 | version = "0.8.26" 2173 | source = "registry+https://github.com/rust-lang/crates.io-index" 2174 | checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" 2175 | 2176 | [[package]] 2177 | name = "yeslogic-fontconfig-sys" 2178 | version = "6.0.0" 2179 | source = "registry+https://github.com/rust-lang/crates.io-index" 2180 | checksum = "503a066b4c037c440169d995b869046827dbc71263f6e8f3be6d77d4f3229dbd" 2181 | dependencies = [ 2182 | "dlib", 2183 | "once_cell", 2184 | "pkg-config", 2185 | ] 2186 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "audio-visualizer" 3 | description = """ 4 | Simple audio visualization library which is especially useful for developers to visually check audio 5 | samples, e.g. by waveform or spectrum. (So far) this library is not capable of doing nice visualizations 6 | for end users. 7 | """ 8 | version = "0.5.0" 9 | authors = ["Philipp Schuster "] 10 | edition = "2021" 11 | keywords = ["audio", "visualizer", "waveform", "spectrum"] 12 | categories = ["multimedia::audio", "development-tools"] 13 | readme = "README.md" 14 | license = "MIT" 15 | homepage = "https://github.com/phip1611/audio-visualizer" 16 | repository = "https://github.com/phip1611/audio-visualizer" 17 | documentation = "https://docs.rs/audio-visualizer/" 18 | exclude = [ 19 | "res", 20 | "test" 21 | ] 22 | 23 | [dependencies] 24 | png = "0.17" 25 | # faster compilation: remove unnecessary features 26 | plotters = { version = "0.3.7", features = ["bitmap_backend", "line_series"] } 27 | plotters-bitmap = "0.3.7" 28 | ringbuffer = "0.15.0" 29 | cpal = "0.15.3" 30 | minifb = "0.28.0" # gui window 31 | 32 | [dev-dependencies] 33 | biquad = "0.5.0" 34 | lowpass-filter = "0.3.2" 35 | spectrum-analyzer = "1.7.0" 36 | symphonia = { version = "0.5.4", default-features = false, features = ["mp3"] } 37 | 38 | # otherwise FFT and other code is too slow 39 | [profile.dev] 40 | opt-level = 1 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Philipp Schuster 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust library: audio-visualizer 2 | 3 | So far this library is rather basic and targets developers that develop audio algorithms. With this library you can 4 | easily display your current audio data/waveform/spectrum and check if everything looks good/as expected. 5 | 6 | ## Covered Functionality 7 | 8 | - **dynamic real-time audio** 9 | - [x] functionality to record audio and connect it with a GUI window 10 | - [x] side-by-side (top/btm) view of original waveform and custom view (e.g. spectrum or lowpass filter) 11 | - [x] cross-platform (Windows with WASAPI, Linux with ALSA, MacOS with coreaudio) 12 | - **static waveform** 13 | - [x] very basic PNG output 14 | - [x] PNG output with basic axes/labels using https://crates.io/crates/plotters 15 | - [ ] TODO fancy static output (code contributions are welcome) 16 | 17 | - **static spectrum** 18 | - [x] very basic PNG output with the option to highlight specific frequencies 19 | (definitely needs more work, code contributions are welcome) 20 | - [x] PNG output with basic axes/labels using https://crates.io/crates/plotters 21 | (definitely needs more work, code contributions are welcome) 22 | - [ ] TODO fancy static output (code contributions are welcome) 23 | 24 | ## (Code) Examples 25 | There are several examples in the `examples/` directory. Below, you can see some visualization examples. 26 | 27 | ### Real-time audio + lowpass filter (6.9MB GIF) 28 | ![Example visualization of real-time audio + lowpass filter](res/live_demo_lowpass_filter_green_day_holiday.gif "Example visualization of real-time audio + lowpass filter") \ 29 | On the top you see the original waveform of the song Holiday by Green Day. On the bottom you see the data after a 30 | lowpass filter was applied. The beats are visible. 31 | 32 | ### Real-time audio + frequency spectrum (5.4MB GIF) 33 | ![Example visualization of real-time audio + spectrum analysis](res/live_demo_spectrum_green_day_holiday.gif "Example visualization of real-time audio + spectrum analysis") \ 34 | On the top you see the original waveform of the song Holiday by Green Day. On the bottom you see 35 | the frequency spectrum of the latest 46ms of audio. Frequencies <2000Hz are clearly present. 36 | 37 | 38 | ### Example of a static waveform 39 | 40 | ![Example visualization of a waveform](png_waveform_example.png "Example visualization of a waveform") 41 | 42 | ### Example of a static spectrum 43 | 44 | ![Example visualization of a spectrum (0-140hz)](plotters_spectrum_example.png "Example visualization of a spectrum (0-140hz)") 45 | 46 | ## MSRV 47 | The MSRV is 1.81.0 stable. 48 | 49 | ## Troubleshooting 50 | ### Linux 51 | - make sure to have these required packages installed: `sudo apt install libasound2-dev libxkbcommon-dev` 52 | -------------------------------------------------------------------------------- /examples/example_static_png_waveform.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | use crate::test_util::decode_mp3; 25 | use audio_visualizer::waveform::png_file::waveform_static_png_visualize; 26 | use audio_visualizer::ChannelInterleavement; 27 | use audio_visualizer::Channels; 28 | use std::path::PathBuf; 29 | 30 | #[allow(unused)] 31 | #[path = "../src/tests/testutil/mod.rs"] 32 | mod test_util; 33 | 34 | fn main() { 35 | let mut path = PathBuf::new(); 36 | path.push("test/samples"); 37 | path.push("sample_1.mp3"); 38 | 39 | let lrlr_mp3_samples = decode_mp3(path.as_path()); 40 | 41 | waveform_static_png_visualize( 42 | &lrlr_mp3_samples, 43 | Channels::Stereo(ChannelInterleavement::LRLR), 44 | "target/test_out", 45 | "sample_1_waveform.png", 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /examples/live_visualize_biquad_lowpass_filter.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | use audio_visualizer::dynamic::live_input::{list_input_devs, AudioDevAndCfg}; 25 | use audio_visualizer::dynamic::window_top_btm::{open_window_connect_audio, TransformFn}; 26 | use biquad::{Biquad, Coefficients, DirectForm1, ToHertz, Type, Q_BUTTERWORTH_F32}; 27 | use std::io::{stdin, BufRead}; 28 | 29 | /// Example that creates a live visualization of realtime audio data 30 | /// through a lowpass filter. **Execute this with `--release`, otherwise it is very laggy!**. 31 | fn main() { 32 | let in_dev = select_input_dev(); 33 | open_window_connect_audio( 34 | "Live Audio Biquad Lowpass Filter View", 35 | None, 36 | None, 37 | None, 38 | None, 39 | "time (seconds)", 40 | "Amplitude (with Biquad Lowpass filter)", 41 | AudioDevAndCfg::new(Some(in_dev), None), 42 | // lowpass filter 43 | TransformFn::Basic(|vals, sampling_rate| { 44 | // Cutoff and sampling frequencies 45 | let f0 = 80.hz(); 46 | let fs = sampling_rate.hz(); 47 | 48 | // Create coefficients for the biquads 49 | let coeffs = 50 | Coefficients::::from_params(Type::LowPass, fs, f0, Q_BUTTERWORTH_F32).unwrap(); 51 | let mut lowpassed_data = Vec::with_capacity(vals.len()); 52 | let mut biquad_lpf = DirectForm1::::new(coeffs); 53 | vals.iter() 54 | .for_each(|val| lowpassed_data.push(biquad_lpf.run(*val))); 55 | lowpassed_data 56 | }), 57 | ); 58 | } 59 | 60 | /// Helps to select an input device. 61 | fn select_input_dev() -> cpal::Device { 62 | let mut devs = list_input_devs(); 63 | assert!(!devs.is_empty(), "no input devices found!"); 64 | if devs.len() == 1 { 65 | return devs.remove(0).1; 66 | } 67 | println!(); 68 | devs.iter().enumerate().for_each(|(i, (name, _dev))| { 69 | println!( 70 | " [{}] {}", 71 | i, 72 | name, 73 | // dev.supported_input_configs().unwrap().collect::>() 74 | ); 75 | }); 76 | let mut input = String::new(); 77 | stdin().lock().read_line(&mut input).unwrap(); 78 | let index = input[0..1].parse::().unwrap(); 79 | devs.remove(index).1 80 | } 81 | -------------------------------------------------------------------------------- /examples/live_visualize_lowpass_filter.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | use audio_visualizer::dynamic::live_input::{list_input_devs, AudioDevAndCfg}; 25 | use audio_visualizer::dynamic::window_top_btm::{open_window_connect_audio, TransformFn}; 26 | use cpal::traits::DeviceTrait; 27 | use lowpass_filter::lowpass_filter; 28 | use std::io::{stdin, BufRead}; 29 | 30 | /// Example that creates a live visualization of realtime audio data 31 | /// through a lowpass filter. **Execute this with `--release`, otherwise it is very laggy!**. 32 | fn main() { 33 | let in_dev = select_input_dev(); 34 | open_window_connect_audio( 35 | "Live Audio Lowpass Filter View", 36 | None, 37 | None, 38 | None, 39 | None, 40 | "time (seconds)", 41 | "Amplitude (with Lowpass filter)", 42 | AudioDevAndCfg::new(Some(in_dev), None), 43 | // lowpass filter 44 | TransformFn::Basic(|x, sampling_rate| { 45 | let mut data_f32 = x.to_vec(); 46 | lowpass_filter(&mut data_f32, sampling_rate, 80.0); 47 | data_f32 48 | }), 49 | ); 50 | } 51 | 52 | /// Helps to select an input device. 53 | fn select_input_dev() -> cpal::Device { 54 | let mut devs = list_input_devs(); 55 | assert!(!devs.is_empty(), "no input devices found!"); 56 | if devs.len() == 1 { 57 | return devs.remove(0).1; 58 | } 59 | println!(); 60 | devs.iter().enumerate().for_each(|(i, (name, dev))| { 61 | println!( 62 | " [{}] {} {:?}", 63 | i, 64 | name, 65 | dev.default_input_config().unwrap() 66 | ); 67 | }); 68 | let mut input = String::new(); 69 | stdin().lock().read_line(&mut input).unwrap(); 70 | let index = input[0..1].parse::().unwrap(); 71 | devs.remove(index).1 72 | } 73 | -------------------------------------------------------------------------------- /examples/live_visualize_signal_power.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | use audio_visualizer::dynamic::live_input::{list_input_devs, AudioDevAndCfg}; 25 | use audio_visualizer::dynamic::window_top_btm::{open_window_connect_audio, TransformFn}; 26 | use cpal::traits::DeviceTrait; 27 | use ringbuffer::{AllocRingBuffer, RingBuffer}; 28 | use std::cell::{Cell, RefCell}; 29 | use std::io::{stdin, BufRead}; 30 | use std::time::Instant; 31 | 32 | /// Example that creates a live visualization of the audio signal power of realtime audio data 33 | /// **Execute this with `--release`, otherwise it is very laggy!**. 34 | fn main() { 35 | let epoch = Cell::new(Instant::now()); 36 | 37 | let power_history: RefCell> = 38 | RefCell::new(AllocRingBuffer::new(2_usize.pow(12))); 39 | 40 | // Closure that captures `visualize_spectrum`. 41 | let to_power_fn = move |audio: &[f32], _sampling_rate: f32| { 42 | let elapsed_s = epoch.get().elapsed().as_secs_f64(); 43 | 44 | // equals 11.6ms with 44.1kHz sampling rate or 10.7ms with 48kHz sampling rate. 45 | const NUM_SAMPLES: usize = 256; 46 | let skip_elements = audio.len() - NUM_SAMPLES; 47 | // spectrum analysis only of the latest 46ms 48 | let relevant_samples = &audio[skip_elements..]; 49 | 50 | let power_sum = relevant_samples 51 | .iter() 52 | .copied() 53 | .map(|x| x * x) 54 | .fold(0.0, |acc, val| acc + val as f64); 55 | let power = power_sum / relevant_samples.len() as f64; 56 | 57 | let mut power_history = power_history.borrow_mut(); 58 | let mut power_history_vec = power_history.to_vec(); 59 | power_history_vec.iter_mut().for_each(|(time, _val)| { 60 | *time -= elapsed_s; 61 | }); 62 | power_history_vec.push((0.0 - 0.01, power)); 63 | power_history.extend(power_history_vec.clone()); 64 | /*loop { 65 | match power_history.iter_mut().next() { 66 | None => break, 67 | Some((time, _val)) => { 68 | dbg!("WAH"); 69 | *time -= elapsed_s; 70 | } 71 | } 72 | } 73 | dbg!("WAH"); 74 | power_history.push((0.0, power));*/ 75 | 76 | epoch.replace(Instant::now()); 77 | power_history_vec 78 | }; 79 | 80 | let in_dev = select_input_dev(); 81 | open_window_connect_audio( 82 | "Live Spectrum View", 83 | None, 84 | None, 85 | // 0.0..22050.0_f64.log(100.0), 86 | Some(-5.9..0.0), 87 | Some(0.0..0.25), 88 | "x-axis", 89 | "y-axis", 90 | AudioDevAndCfg::new(Some(in_dev), None), 91 | TransformFn::Complex(&to_power_fn), 92 | ); 93 | } 94 | 95 | /// Helps to select an input device. 96 | fn select_input_dev() -> cpal::Device { 97 | let mut devs = list_input_devs(); 98 | assert!(!devs.is_empty(), "no input devices found!"); 99 | if devs.len() == 1 { 100 | return devs.remove(0).1; 101 | } 102 | println!(); 103 | devs.iter().enumerate().for_each(|(i, (name, dev))| { 104 | println!( 105 | " [{}] {} {:?}", 106 | i, 107 | name, 108 | dev.default_input_config().unwrap() 109 | ); 110 | }); 111 | let mut input = String::new(); 112 | stdin().lock().read_line(&mut input).unwrap(); 113 | let index = input[0..1].parse::().unwrap(); 114 | devs.remove(index).1 115 | } 116 | -------------------------------------------------------------------------------- /examples/live_visualize_spectrum.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | use audio_visualizer::dynamic::live_input::{list_input_devs, AudioDevAndCfg}; 25 | use audio_visualizer::dynamic::window_top_btm::{open_window_connect_audio, TransformFn}; 26 | use cpal::traits::DeviceTrait; 27 | use spectrum_analyzer::scaling::divide_by_N; 28 | use spectrum_analyzer::windows::hann_window; 29 | use spectrum_analyzer::{samples_fft_to_spectrum, FrequencyLimit, FrequencyValue}; 30 | use std::cell::RefCell; 31 | use std::cmp::max; 32 | use std::io::{stdin, BufRead}; 33 | 34 | /// Example that creates a live visualization of the frequency spectrum of realtime audio data 35 | /// **Execute this with `--release`, otherwise it is very laggy!**. 36 | fn main() { 37 | // Contains the data for the spectrum to be visualized. It contains ordered pairs of 38 | // `(frequency, frequency_value)`. During each iteration, the frequency value gets 39 | // combined with `max(old_value * smoothing_factor, new_value)`. 40 | let visualize_spectrum: RefCell> = RefCell::new(vec![(0.0, 0.0); 1024]); 41 | 42 | // Closure that captures `visualize_spectrum`. 43 | let to_spectrum_fn = move |audio: &[f32], sampling_rate| { 44 | let skip_elements = audio.len() - 2048; 45 | // spectrum analysis only of the latest 46ms 46 | let relevant_samples = &audio[skip_elements..skip_elements + 2048]; 47 | 48 | // do FFT 49 | let hann_window = hann_window(relevant_samples); 50 | let latest_spectrum = samples_fft_to_spectrum( 51 | &hann_window, 52 | sampling_rate as u32, 53 | FrequencyLimit::All, 54 | Some(÷_by_N), 55 | ) 56 | .unwrap(); 57 | 58 | // now smoothen the spectrum; old values are decreased a bit and replaced, 59 | // if the new value is higher 60 | latest_spectrum 61 | .data() 62 | .iter() 63 | .zip(visualize_spectrum.borrow_mut().iter_mut()) 64 | .for_each(|((fr_new, fr_val_new), (fr_old, fr_val_old))| { 65 | // actually only required in very first iteration 66 | *fr_old = fr_new.val() as f64; 67 | let old_val = *fr_val_old * 0.84; 68 | let max = max( 69 | *fr_val_new * 5000.0_f32.into(), 70 | FrequencyValue::from(old_val as f32), 71 | ); 72 | *fr_val_old = max.val() as f64; 73 | }); 74 | 75 | visualize_spectrum.borrow().clone() 76 | }; 77 | 78 | let in_dev = select_input_dev(); 79 | open_window_connect_audio( 80 | "Live Spectrum View", 81 | None, 82 | None, 83 | // 0.0..22050.0_f64.log(100.0), 84 | Some(0.0..22050.0), 85 | Some(0.0..500.0), 86 | "x-axis", 87 | "y-axis", 88 | AudioDevAndCfg::new(Some(in_dev), None), 89 | TransformFn::Complex(&to_spectrum_fn), 90 | ); 91 | } 92 | 93 | /// Helps to select an input device. 94 | fn select_input_dev() -> cpal::Device { 95 | let mut devs = list_input_devs(); 96 | assert!(!devs.is_empty(), "no input devices found!"); 97 | if devs.len() == 1 { 98 | return devs.remove(0).1; 99 | } 100 | println!(); 101 | devs.iter().enumerate().for_each(|(i, (name, dev))| { 102 | println!( 103 | " [{}] {} {:?}", 104 | i, 105 | name, 106 | dev.default_input_config().unwrap() 107 | ); 108 | }); 109 | let mut input = String::new(); 110 | stdin().lock().read_line(&mut input).unwrap(); 111 | let index = input[0..1].parse::().unwrap(); 112 | devs.remove(index).1 113 | } 114 | -------------------------------------------------------------------------------- /plotters_spectrum_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phip1611/audio-visualizer/3caf9e089f19f2bfbd82f56961e541f6a33d2c25/plotters_spectrum_example.png -------------------------------------------------------------------------------- /png_waveform_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phip1611/audio-visualizer/3caf9e089f19f2bfbd82f56961e541f6a33d2c25/png_waveform_example.png -------------------------------------------------------------------------------- /res/live_demo_lowpass_filter_green_day_holiday.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phip1611/audio-visualizer/3caf9e089f19f2bfbd82f56961e541f6a33d2c25/res/live_demo_lowpass_filter_green_day_holiday.gif -------------------------------------------------------------------------------- /res/live_demo_lowpass_filter_green_day_holiday.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phip1611/audio-visualizer/3caf9e089f19f2bfbd82f56961e541f6a33d2c25/res/live_demo_lowpass_filter_green_day_holiday.webm -------------------------------------------------------------------------------- /res/live_demo_spectrum_green_day_holiday.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phip1611/audio-visualizer/3caf9e089f19f2bfbd82f56961e541f6a33d2c25/res/live_demo_spectrum_green_day_holiday.gif -------------------------------------------------------------------------------- /res/live_demo_spectrum_green_day_holiday.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phip1611/audio-visualizer/3caf9e089f19f2bfbd82f56961e541f6a33d2c25/res/live_demo_spectrum_green_day_holiday.webm -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | pkgs.mkShell rec { 3 | nativeBuildInputs = with pkgs; [ 4 | pkg-config 5 | cargo-nextest 6 | ]; 7 | 8 | buildInputs = with pkgs; [ 9 | alsa-lib 10 | fontconfig 11 | libxkbcommon 12 | xorg.libXcursor 13 | xorg.libX11 14 | ]; 15 | 16 | LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath buildInputs}"; 17 | } 18 | -------------------------------------------------------------------------------- /src/bin/test_cpal_audio_input.rs: -------------------------------------------------------------------------------- 1 | use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; 2 | use cpal::{BufferSize, SampleRate, StreamConfig}; 3 | use std::thread::sleep; 4 | use std::time::Duration; 5 | 6 | /// Small binary to test audio input across platforms (windows, mac, linux) in a fast way. 7 | fn main() { 8 | for host in cpal::available_hosts() { 9 | println!("Host={:?}, input devices:", host); 10 | for dev in cpal::host_from_id(host).unwrap().input_devices().unwrap() { 11 | println!( 12 | " {} - Supported input configs:", 13 | dev.name() 14 | .as_ref() 15 | .map(|s| s.as_str()) 16 | .unwrap_or(""), 17 | ); 18 | 19 | for cfg in dev.supported_input_configs().unwrap() { 20 | println!(" {:#?}", cfg); 21 | } 22 | } 23 | } 24 | 25 | let default_in = cpal::default_host().default_input_device().unwrap(); 26 | let stream = default_in 27 | .build_input_stream::( 28 | &StreamConfig { 29 | channels: 2, 30 | sample_rate: SampleRate(48000), 31 | buffer_size: BufferSize::Default, 32 | }, 33 | |data, _x| { 34 | println!("got data: {} samples", data.len()); 35 | }, 36 | |_e| {}, 37 | None, 38 | ) 39 | .unwrap(); 40 | stream.play().unwrap(); 41 | sleep(Duration::from_secs(5)); 42 | stream.pause().unwrap(); 43 | } 44 | -------------------------------------------------------------------------------- /src/dynamic/live_input.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | //! This module enables to record audio and store the latest audio data in a synchronized 25 | //! ringbuffer. See [`setup_audio_input_loop`]. 26 | //! 27 | //! It uses the [`cpal`] crate to record audio. 28 | 29 | use cpal::traits::{DeviceTrait, HostTrait}; 30 | use cpal::Device; 31 | use ringbuffer::AllocRingBuffer; 32 | use std::fmt::{Debug, Formatter}; 33 | use std::sync::{Arc, Mutex}; 34 | 35 | /// Describes the audio input device that should be used and the config for the input stream. 36 | /// Caller must be certain, that the config works for the given device on the current platform. 37 | pub struct AudioDevAndCfg { 38 | /// The input device. 39 | dev: cpal::Device, 40 | /// Desired configuration for the input stream. 41 | cfg: cpal::StreamConfig, 42 | } 43 | 44 | impl AudioDevAndCfg { 45 | /// Creates an instance. If no device is passed, it falls back to the default input 46 | /// device of the system. 47 | pub fn new( 48 | preferred_dev: Option, 49 | preferred_cfg: Option, 50 | ) -> Self { 51 | let dev = preferred_dev.unwrap_or_else(|| { 52 | let host = cpal::default_host(); 53 | host.default_input_device().unwrap_or_else(|| { 54 | panic!( 55 | "No default audio input device found for host {}", 56 | host.id().name() 57 | ) 58 | }) 59 | }); 60 | let cfg = preferred_cfg.unwrap_or_else(|| dev.default_input_config().unwrap().config()); 61 | Self { dev, cfg } 62 | } 63 | 64 | /// Getter for audio device. 65 | pub const fn dev(&self) -> &cpal::Device { 66 | &self.dev 67 | } 68 | 69 | /// Getter for audio input stream config. 70 | pub const fn cfg(&self) -> &cpal::StreamConfig { 71 | &self.cfg 72 | } 73 | } 74 | 75 | impl Debug for AudioDevAndCfg { 76 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 77 | f.debug_struct("AudioDevAndCfg") 78 | .field( 79 | "dev", 80 | &self 81 | .dev 82 | .name() 83 | .as_ref() 84 | .map(|x| x.as_str()) 85 | .unwrap_or(""), 86 | ) 87 | .field("cfg", &self.cfg) 88 | .finish() 89 | } 90 | } 91 | 92 | /// Sets up audio recording with the [`cpal`] library on the given audio input device. 93 | /// 94 | /// If no input device is given, it uses the default input device. Panics, if it not present. 95 | /// Returns the stream plus the chosen config for the device. 96 | /// 97 | /// Appends all audio data to the ringbuffer `latest_audio_data`. 98 | /// 99 | /// Works on Windows (WASAPI), Linux (ALSA) and MacOS (coreaudio). 100 | pub fn setup_audio_input_loop( 101 | latest_audio_data: Arc>>, 102 | audio_dev_and_cfg: AudioDevAndCfg, 103 | ) -> cpal::Stream { 104 | let dev = audio_dev_and_cfg.dev(); 105 | let cfg = audio_dev_and_cfg.cfg(); 106 | 107 | eprintln!( 108 | "Using input device '{}' with config: {:?}", 109 | dev.name() 110 | .as_ref() 111 | .map(|x| x.as_str()) 112 | .unwrap_or(""), 113 | cfg 114 | ); 115 | 116 | assert!( 117 | cfg.channels == 1 || cfg.channels == 2, 118 | "only supports Mono or Stereo channels!" 119 | ); 120 | 121 | if cfg.sample_rate.0 != 44100 && cfg.sample_rate.0 != 48000 { 122 | eprintln!( 123 | "WARN: sampling rate is {}, but the crate was only tested with 44,1/48khz.", 124 | cfg.sample_rate.0 125 | ); 126 | } 127 | 128 | let is_mono = cfg.channels == 1; 129 | 130 | let stream = dev 131 | .build_input_stream( 132 | // This is not as easy as it might look. Even if the supported configs show, that a 133 | // input device supports a given fixed buffer size, ALSA but also WASAPI tend to 134 | // fail with unclear error messages. I found out, that using the default option is the 135 | // only variant that is working on all platforms (Windows, Mac, Linux). The buffer 136 | // size tends to be not as small as it would be optimal (for super low latency) 137 | // but is still good enough (for example ~10ms on Windows) or ~6ms on ALSA (in my 138 | // tests). 139 | audio_dev_and_cfg.cfg(), 140 | // this is pretty cool by "cpal"; we can use u16, i16 or f32 and 141 | // the type system does all the magic behind the scenes. f32 also works 142 | // on Windows (WASAPI), MacOS (coreaudio), and Linux (ALSA). 143 | // TODO: I found out that we probably can't rely on the fact, that every audio input device 144 | // supports f32. I guess, I need to check this in the supported audio stream config too.. 145 | move |data: &[f32], _info| { 146 | let mut audio_buf = latest_audio_data.lock().unwrap(); 147 | // Audio buffer only contains Mono data 148 | if is_mono { 149 | audio_buf.extend(data.iter().copied()); 150 | } else { 151 | // interleaving for stereo is LRLR (de-facto standard?) 152 | audio_buf.extend(data.chunks_exact(2).map(|vals| (vals[0] + vals[1]) / 2.0)) 153 | } 154 | }, 155 | |err| { 156 | eprintln!("got stream error: {:#?}", err); 157 | }, 158 | None, 159 | ) 160 | .unwrap(); 161 | 162 | stream 163 | } 164 | 165 | /// Lists all input devices for [`cpal`]. Can be used to select a device for 166 | /// [`setup_audio_input_loop`]. 167 | pub fn list_input_devs() -> Vec<(String, cpal::Device)> { 168 | let host = cpal::default_host(); 169 | type DeviceName = String; 170 | let mut devs: Vec<(DeviceName, Device)> = host 171 | .input_devices() 172 | .unwrap() 173 | .map(|dev| { 174 | ( 175 | dev.name().unwrap_or_else(|_| String::from("")), 176 | dev, 177 | ) 178 | }) 179 | .collect(); 180 | devs.sort_by(|(n1, _), (n2, _)| n1.cmp(n2)); 181 | devs 182 | } 183 | 184 | #[cfg(test)] 185 | mod tests { 186 | use super::*; 187 | 188 | #[test] 189 | fn test_list_input_devs() { 190 | dbg!(list_input_devs() 191 | .iter() 192 | .map(|(n, d)| (n, d.default_input_config())) 193 | .collect::>()); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/dynamic/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | //! Module provides functions to dynamically get audio input (e.g. from mic). 26 | //! 27 | //! Further, it provides functionality to display the recorded audio 28 | //! side-by-side with a transformation of the audio data, such as a lowpass 29 | //! filter or a frequency spectrum. 30 | //! 31 | //! **Its recommended to execute all functions here only with `--release`-flag. 32 | //! Otherwise, the demo might run really slowly. 33 | 34 | pub mod live_input; 35 | pub mod window_top_btm; 36 | -------------------------------------------------------------------------------- /src/dynamic/window_top_btm/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | //! This module provides the functionality to display a GUI window. 25 | //! 26 | //! In the window, the upper half shows the real-time recorded audio data 27 | //! whereas the lower half shows a diagram of transformed data, such as a 28 | //! lowpass filter or a frequency spectrum. 29 | //! 30 | //! It uses the [`minifb`] crate to display GUI windows. 31 | use crate::dynamic::live_input::{setup_audio_input_loop, AudioDevAndCfg}; 32 | use crate::dynamic::window_top_btm::visualize_minifb::{ 33 | get_drawing_areas, setup_window, DEFAULT_H, DEFAULT_W, 34 | }; 35 | use cpal::traits::StreamTrait; 36 | 37 | use minifb::Key; 38 | use plotters::chart::ChartContext; 39 | use plotters::coord::cartesian::Cartesian2d; 40 | use plotters::coord::types::RangedCoordf64; 41 | use plotters::prelude::BitMapBackend; 42 | use plotters::series::LineSeries; 43 | use plotters::style::{BLACK, CYAN}; 44 | use plotters_bitmap::bitmap_pixel::BGRXPixel; 45 | use ringbuffer::{AllocRingBuffer, RingBuffer}; 46 | use std::borrow::{Borrow, BorrowMut}; 47 | use std::ops::Range; 48 | use std::sync::{Arc, Mutex}; 49 | 50 | pub mod pixel_buf; 51 | pub mod visualize_minifb; 52 | 53 | /// Parameter type for [`open_window_connect_audio`]. Describes how the audio data shall 54 | /// be transformed, and thus, how it should be displayed in the lower part of the window. 55 | /// 56 | /// The function is called every x milliseconds (refresh rate of window). 57 | /// 58 | /// This works cross-platform (Windows, MacOS, Linux). 59 | #[allow(missing_debug_implementations)] 60 | pub enum TransformFn<'a> { 61 | /// Synchronized x-axis with the original data. Useful for transformations on the 62 | /// waveform, such as a (lowpass) filter. 63 | /// 64 | /// Functions takes amplitude values and transforms them to a new amplitude value. 65 | /// It gets the sampling rate as second argument. 66 | Basic(fn(&[f32], f32) -> Vec), 67 | /// Use this, when the x-axis is different than for the original data. For example, 68 | /// if you want to display a spectrum. 69 | /// 70 | /// Functions takes amplitude values (and their index) and transforms them to a new 71 | /// (x,y)-pair. Takes a closure instead of a function, so that it can capture state. 72 | /// It gets the sampling rate as second argument. 73 | #[allow(clippy::complexity)] 74 | Complex(&'a dyn Fn(&[f32], f32) -> Vec<(f64, f64)>), 75 | } 76 | 77 | /// Starts the audio recording via `cpal` on the given audio device (or the default input device), 78 | /// opens a GUI window and displays two graphs. 79 | /// 80 | /// The upper graph is the latest audio input as 81 | /// wave form (live/real time). The lower graph can be customized, to show for example a 82 | /// spectrum or the lowpassed data. 83 | /// 84 | /// This operation is blocking. It returns, when the GUI window is closed. 85 | /// 86 | /// **This operation is expensive and will be very laggy in "Debug" builds!** 87 | /// 88 | /// # Parameters 89 | /// - `name` Name of the GUI window 90 | /// - `preferred_height` Preferred height of GUI window. Default is [`DEFAULT_H`]. 91 | /// - `preferred_width` Preferred height of GUI window. Default is [`DEFAULT_W`]. 92 | /// - `preferred_x_range` Preferred range for the x-axis of the lower (=custom) diagram. 93 | /// If no value is present, the same value as for the upper diagram is used. 94 | /// - `preferred_y_range` Preferred range for the y-axis of the lower (=custom) diagram. 95 | /// If no value is present, the same value as for the upper diagram is used. 96 | /// - `x_desc` Description for the x-axis of the lower (=custom) diagram. 97 | /// - `y_desc` Description for the y-axis of the lower (=custom) diagram. 98 | /// - `preferred_input_dev` See [`AudioDevAndCfg`]. 99 | /// - `audio_data_transform_fn` See [`open_window_connect_audio`]. 100 | #[allow(clippy::too_many_arguments)] 101 | pub fn open_window_connect_audio( 102 | name: &str, 103 | preferred_height: Option, 104 | preferred_width: Option, 105 | preferred_x_range: Option>, 106 | preferred_y_range: Option>, 107 | x_desc: &str, 108 | y_desc: &str, 109 | input_dev_and_cfg: AudioDevAndCfg, 110 | audio_data_transform_fn: TransformFn, 111 | ) { 112 | let sample_rate = input_dev_and_cfg.cfg().sample_rate.0 as f32; 113 | let latest_audio_data = init_ringbuffer(sample_rate as usize); 114 | let audio_buffer_len = latest_audio_data.lock().unwrap().len(); 115 | let stream = setup_audio_input_loop(latest_audio_data.clone(), input_dev_and_cfg); 116 | // This will be 1/44100 or 1/48000; the two most common sampling rates. 117 | let time_per_sample = 1.0 / sample_rate as f64; 118 | 119 | // start recording; audio will be continuously stored in "latest_audio_data" 120 | stream.play().unwrap(); 121 | let (mut window, top_cs, btm_cs, mut pixel_buf) = setup_window( 122 | name, 123 | preferred_height, 124 | preferred_width, 125 | preferred_x_range, 126 | preferred_y_range, 127 | x_desc, 128 | y_desc, 129 | audio_buffer_len, 130 | time_per_sample, 131 | ); 132 | window.set_target_fps(144); 133 | 134 | // GUI refresh loop; CPU-limited by "window.limit_update_rate" 135 | while window.is_open() { 136 | if window.is_key_down(Key::Escape) { 137 | break; 138 | } 139 | 140 | let (top_drawing_area, btm_drawing_area) = get_drawing_areas( 141 | pixel_buf.borrow_mut(), 142 | preferred_width.unwrap_or(DEFAULT_W), 143 | preferred_height.unwrap_or(DEFAULT_H), 144 | ); 145 | 146 | let top_chart = top_cs.clone().restore(&top_drawing_area); 147 | let btm_chart = btm_cs.clone().restore(&btm_drawing_area); 148 | 149 | // remove drawings from previous iteration (but keep axis etc) 150 | top_chart.plotting_area().fill(&BLACK).borrow(); 151 | btm_chart.plotting_area().fill(&BLACK).borrow(); 152 | 153 | // lock released immediately after oneliner 154 | let latest_audio_data = latest_audio_data.clone().lock().unwrap().to_vec(); 155 | fill_chart_waveform_over_time( 156 | top_chart, 157 | &latest_audio_data, 158 | time_per_sample, 159 | audio_buffer_len, 160 | ); 161 | if let TransformFn::Basic(fnc) = audio_data_transform_fn { 162 | let data = fnc(&latest_audio_data, sample_rate); 163 | fill_chart_waveform_over_time(btm_chart, &data, time_per_sample, audio_buffer_len); 164 | } else if let TransformFn::Complex(fnc) = audio_data_transform_fn { 165 | let data = fnc(&latest_audio_data, sample_rate); 166 | fill_chart_complex_fnc(btm_chart, data); 167 | } else { 168 | // required for compilation 169 | drop(btm_chart); 170 | panic!("invalid transform fn variant"); 171 | } 172 | 173 | // make sure that "pixel_buf" is not borrowed longer 174 | drop(top_drawing_area); 175 | drop(btm_drawing_area); 176 | 177 | // REQUIRED to call on of the .update*()-methods, otherwise mouse and keyboard events 178 | // are not updated 179 | // 180 | // Update() also does the rate limiting/set the thread to sleep if not enough time 181 | // sine the last refresh happened 182 | window 183 | .update_with_buffer( 184 | pixel_buf.borrow(), 185 | preferred_width.unwrap_or(DEFAULT_W), 186 | preferred_height.unwrap_or(DEFAULT_H), 187 | ) 188 | .unwrap(); 189 | } 190 | stream.pause().unwrap(); 191 | } 192 | 193 | /// Inits a ringbuffer on the heap and fills it with zeroes. 194 | fn init_ringbuffer(sampling_rate: usize) -> Arc>> { 195 | // Must be a power (ringbuffer requirement). 196 | let mut buf = AllocRingBuffer::new((5 * sampling_rate).next_power_of_two()); 197 | buf.fill(0.0); 198 | Arc::new(Mutex::new(buf)) 199 | } 200 | 201 | /// Fills the given chart with the waveform over time, from the past (left) to now/realtime (right). 202 | fn fill_chart_complex_fnc( 203 | mut chart: ChartContext, Cartesian2d>, 204 | audio_data: Vec<(f64, f64)>, 205 | ) { 206 | // dedicated function; otherwise lifetime problems/compiler errors 207 | chart 208 | .draw_series(LineSeries::new(audio_data, &CYAN)) 209 | .unwrap(); 210 | } 211 | 212 | /// Fills the given chart with the waveform over time, from the past (left) to now/realtime (right). 213 | fn fill_chart_waveform_over_time( 214 | mut chart: ChartContext, Cartesian2d>, 215 | audio_data: &[f32], 216 | time_per_sample: f64, 217 | audio_history_buf_len: usize, 218 | ) { 219 | debug_assert_eq!(audio_data.len(), audio_history_buf_len); 220 | let timeshift = audio_history_buf_len as f64 * time_per_sample; 221 | 222 | // calculate timestamp of each index (x coordinate) 223 | let data_iter = audio_data 224 | .iter() 225 | .enumerate() 226 | // Important to reduce the calculation complexity by reducing the number of elements, 227 | // because drawing tens of thousands of points into the diagram is very expensive. 228 | // 229 | // If we skip too many elements, animation becomes un-smooth.... 4 seems to be sensible 230 | // due to tests by me. 231 | .filter(|(i, _)| *i % 4 == 0) 232 | .map(|(i, amplitude)| { 233 | let timestamp = time_per_sample * (i as f64) - timeshift; 234 | // Values for amplitude in interval [-1.0; 1.0] 235 | (timestamp, (*amplitude) as f64) 236 | }); 237 | 238 | // Draws all points as a line of connected points. 239 | // LineSeries is reasonable efficient for the big workload, but still very expensive.. 240 | // (4-6ms in release mode on my intel i5 10th generation) 241 | chart 242 | .draw_series(LineSeries::new(data_iter, &CYAN)) 243 | .unwrap(); 244 | } 245 | 246 | #[cfg(test)] 247 | mod tests { 248 | use super::*; 249 | 250 | #[ignore] 251 | #[test] 252 | fn test_record_live_audio_and_visualize() { 253 | open_window_connect_audio( 254 | "Test", 255 | None, 256 | None, 257 | None, 258 | None, 259 | "x-axis", 260 | "y-axis", 261 | AudioDevAndCfg::new(None, None), 262 | TransformFn::Basic(|vals, _| vals.to_vec()), 263 | ); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/dynamic/window_top_btm/pixel_buf.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | use std::borrow::{Borrow, BorrowMut}; 25 | 26 | /// Abstraction that helps to access the Pixel data as `[u32]` and `[u8]`. 27 | /// Required, because `plotters` bitmap works on `[u8]` and `minifb` on 28 | /// the other. 29 | #[derive(Debug)] 30 | pub struct PixelBuf(pub Vec); 31 | impl Borrow<[u8]> for PixelBuf { 32 | fn borrow(&self) -> &[u8] { 33 | // Safe for alignment: align_of(u8) <= align_of(u32) 34 | // Safe for cast: u32 can be thought of as being transparent over [u8; 4] 35 | unsafe { std::slice::from_raw_parts(self.0.as_ptr() as *const u8, self.0.len() * 4) } 36 | } 37 | } 38 | impl BorrowMut<[u8]> for PixelBuf { 39 | fn borrow_mut(&mut self) -> &mut [u8] { 40 | // Safe for alignment: align_of(u8) <= align_of(u32) 41 | // Safe for cast: u32 can be thought of as being transparent over [u8; 4] 42 | unsafe { std::slice::from_raw_parts_mut(self.0.as_mut_ptr() as *mut u8, self.0.len() * 4) } 43 | } 44 | } 45 | impl Borrow<[u32]> for PixelBuf { 46 | fn borrow(&self) -> &[u32] { 47 | self.0.as_slice() 48 | } 49 | } 50 | impl BorrowMut<[u32]> for PixelBuf { 51 | fn borrow_mut(&mut self) -> &mut [u32] { 52 | self.0.as_mut_slice() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/dynamic/window_top_btm/visualize_minifb.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | //! Helps to visualize audio data 25 | 26 | use crate::dynamic::window_top_btm::pixel_buf::PixelBuf; 27 | use minifb::{Window, WindowOptions}; 28 | use plotters::chart::{ChartBuilder, ChartState}; 29 | use plotters::coord::cartesian::Cartesian2d; 30 | use plotters::coord::types::RangedCoordf64; 31 | use plotters::coord::Shift; 32 | use plotters::drawing::{DrawingArea, IntoDrawingArea}; 33 | use plotters::style::{IntoFont, WHITE}; 34 | use plotters_bitmap::bitmap_pixel::BGRXPixel; 35 | use plotters_bitmap::BitMapBackend; 36 | use std::borrow::{Borrow, BorrowMut}; 37 | use std::ops::Range; 38 | 39 | /// Width of the window. 40 | pub const DEFAULT_W: usize = 1280; 41 | /// Height of the window. 42 | pub const DEFAULT_H: usize = 720; 43 | 44 | /// Initializes the [`minifb`] window and draws the initial grid into it. 45 | /// 46 | /// It splits the drawing area into an upper chart and a lower chart. The 47 | /// upper exists to show original audio data. The lower exists to show transformed 48 | /// audio data, e.g. spectrum or lowpass filter. 49 | /// 50 | /// # Parameters 51 | /// - `name` Name of the GUI window 52 | /// - `preferred_height` Preferred height of GUI window. Default is [`DEFAULT_H`]. 53 | /// - `preferred_width` Preferred height of GUI window. Default is [`DEFAULT_W`]. 54 | /// - `preferred_x_range` Preferred range for the x-axis of the lower (=custom) diagram. 55 | /// If no value is present, the same value as for the upper diagram is used. 56 | /// - `preferred_y_range` Preferred range for the y-axis of the lower (=custom) diagram. 57 | /// If no value is present, the same value as for the upper diagram is used. 58 | /// - `x_desc` Description for the x-axis of the lower (=custom) diagram. 59 | /// - `y_desc` Description for the y-axis of the lower (=custom) diagram. 60 | /// - `audio_buffer_len` Number of elements in the audio buffer. Needed for the scaling of the x-axis. 61 | /// - `time_per_sample` Time per sample. Needed for the scaling of the x-axis. 62 | /// 63 | /// # Returns 64 | /// - window object 65 | /// - chartstate of the upper chart 66 | /// - chartstate of the lower chart 67 | /// - the shared pixel buf 68 | #[allow(clippy::type_complexity, clippy::too_many_arguments)] 69 | pub fn setup_window( 70 | name: &str, 71 | preferred_height: Option, 72 | preferred_width: Option, 73 | preferred_x_range: Option>, 74 | preferred_y_range: Option>, 75 | x_desc: &str, 76 | y_desc: &str, 77 | audio_buffer_len: usize, 78 | time_per_sample: f64, 79 | ) -> ( 80 | Window, 81 | ChartState>, 82 | ChartState>, 83 | PixelBuf, 84 | ) { 85 | let height = preferred_height.unwrap_or(DEFAULT_H); 86 | let width = preferred_width.unwrap_or(DEFAULT_W); 87 | let mut window = 88 | Window::new(&String::from(name), width, height, WindowOptions::default()).unwrap(); 89 | let x_range_top = -(audio_buffer_len as f64 * time_per_sample)..0.0; 90 | let y_range_top = -1.0..1.01; 91 | let x_range_btm = preferred_x_range.unwrap_or_else(|| x_range_top.clone()); 92 | let y_range_btm = preferred_y_range.unwrap_or_else(|| y_range_top.clone()); 93 | 94 | // Buffer where we draw the Chart as bitmap into: we update the "minifb" window from it too 95 | let mut pixel_buf = PixelBuf(vec![0_u32; width * height]); 96 | 97 | let (top_drawing_area, btm_drawing_area) = 98 | get_drawing_areas(pixel_buf.borrow_mut(), width, height); 99 | 100 | let top_chart = draw_chart( 101 | top_drawing_area, 102 | x_range_top, 103 | y_range_top, 104 | "time (seconds)", 105 | "amplitude", 106 | ); 107 | let btm_chart = draw_chart(btm_drawing_area, x_range_btm, y_range_btm, x_desc, y_desc); 108 | 109 | // unborrow "pixel_buf" again 110 | //drop(root_drawing_area); 111 | 112 | window 113 | .update_with_buffer(pixel_buf.borrow(), width, height) 114 | .unwrap(); 115 | 116 | (window, top_chart, btm_chart, pixel_buf) 117 | } 118 | 119 | /// Returns two drawing areas, that together fill the whole window. 120 | /// Upper: original audio data 121 | /// Lower: transformed audio data 122 | pub fn get_drawing_areas( 123 | pixel_buf: &mut [u8], 124 | width: usize, 125 | height: usize, 126 | ) -> ( 127 | DrawingArea, Shift>, 128 | DrawingArea, Shift>, 129 | ) { 130 | // BGRXPixel format required by "minifb" (alpha, red, green, blue) 131 | let root_drawing_area = BitMapBackend::::with_buffer_and_format( 132 | pixel_buf.borrow_mut(), 133 | (width as u32, height as u32), 134 | ) 135 | .unwrap() 136 | .into_drawing_area(); 137 | 138 | let (top_drawing_area, btm_drawing_area) = 139 | root_drawing_area.split_vertically((height / 2) as f64); 140 | (top_drawing_area, btm_drawing_area) 141 | } 142 | 143 | /// Draws the initial, empty into the dedicated drawing area. 144 | /// Drops the drawing area, which is important to let this compile. 145 | /// It's important that the chart gets returned as `ChartState`. 146 | /// 147 | /// I don't understand it correctly, but it seems that the chart state is 148 | /// a strategy by `plotter` to retain some state while not borrowing anything. 149 | /// Furthermore this is more efficient, because axis etc. doesn't has to be 150 | /// redrawn on incremental updates. 151 | fn draw_chart<'a>( 152 | drawing_area: DrawingArea, Shift>, 153 | x_range: Range, 154 | y_range: Range, 155 | x_desc: &'a str, 156 | y_desc: &'a str, 157 | ) -> ChartState> { 158 | let mut chart = ChartBuilder::on(&drawing_area) 159 | // margin effects the distance to the border of the window of the chart 160 | .margin(10) 161 | .set_all_label_area_size(60) 162 | .build_cartesian_2d(x_range, y_range) 163 | .unwrap(); 164 | 165 | chart 166 | .configure_mesh() 167 | .label_style(("sans-serif", 15).into_font().color(&WHITE)) 168 | .x_desc(x_desc) 169 | .y_desc(y_desc) 170 | .x_labels(10) 171 | .y_labels(10) 172 | .axis_style(WHITE) 173 | .draw() 174 | .unwrap(); 175 | 176 | chart.into_chart_state() 177 | } 178 | 179 | #[cfg(test)] 180 | mod tests { 181 | 182 | use minifb::Key; 183 | 184 | #[ignore] 185 | #[test] 186 | fn test_minifb_window() { 187 | let (mut window, _, _, _) = super::setup_window( 188 | "Test", 189 | None, 190 | None, 191 | Some(-5.0..0.0), 192 | Some(0.0..5.01), 193 | "x-axis", 194 | "y-axis", 195 | (44100 * 5_usize).next_power_of_two(), 196 | 1.0 / 44100.0, 197 | ); 198 | while window.is_open() && !window.is_key_down(Key::Escape) { 199 | // REQUIRED to get keyboard and mouse events (such as close) 200 | window.update(); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | //! Super basic and simple audio visualization library which is especially useful for developers to 25 | //! visually check audio samples, e.g. by waveform or spectrum. (So far) this library is not 26 | //! capable of doing nice visualizations for end users. Contributions are welcome. 27 | 28 | #![deny( 29 | clippy::all, 30 | clippy::cargo, 31 | clippy::nursery, 32 | // clippy::restriction, 33 | // clippy::pedantic 34 | )] 35 | // now allow a few rules which are denied by the above statement 36 | // --> they are ridiculous and not necessary 37 | #![allow( 38 | clippy::suboptimal_flops, 39 | clippy::redundant_pub_crate, 40 | clippy::fallible_impl_from, 41 | clippy::multiple_crate_versions 42 | )] 43 | #![deny(missing_debug_implementations)] 44 | #![deny(rustdoc::all)] 45 | 46 | pub mod spectrum; 47 | pub mod waveform; 48 | 49 | pub mod dynamic; 50 | #[cfg(test)] 51 | mod tests; 52 | pub mod util; 53 | 54 | /// Describes the interleavement of audio data if 55 | /// it is not mono but stereo. 56 | #[derive(Debug, Copy, Clone)] 57 | pub enum ChannelInterleavement { 58 | /// Stereo samples of one vector of audio data are alternating: left, right, left, right 59 | LRLR, 60 | /// Stereo samples of one vector of audio data are ordered like: left, left, ..., right, right 61 | /// In this case the length must be a multiple of 2. 62 | LLRR, 63 | } 64 | 65 | impl ChannelInterleavement { 66 | pub const fn is_lrlr(&self) -> bool { 67 | matches!(self, Self::LRLR) 68 | } 69 | pub const fn is_lllrr(&self) -> bool { 70 | matches!(self, Self::LLRR) 71 | } 72 | /// Transforms the interleaved data into two vectors. 73 | /// Returns a tuple. First/left value is left channel, second/right value is right channel. 74 | pub fn to_channel_data(&self, interleaved_data: &[i16]) -> (Vec, Vec) { 75 | let mut left_data = vec![]; 76 | let mut right_data = vec![]; 77 | 78 | if self.is_lrlr() { 79 | let mut is_left = true; 80 | for sample in interleaved_data { 81 | if is_left { 82 | left_data.push(*sample); 83 | } else { 84 | right_data.push(*sample) 85 | } 86 | is_left = !is_left; 87 | } 88 | } else { 89 | let n = interleaved_data.len(); 90 | for sample_i in interleaved_data.iter().take(n / 2).copied() { 91 | left_data.push(sample_i); 92 | } 93 | for sample_i in interleaved_data.iter().skip(n / 2).copied() { 94 | right_data.push(sample_i); 95 | } 96 | } 97 | 98 | (left_data, right_data) 99 | } 100 | } 101 | 102 | /// Describes the number of channels of an audio stream. 103 | #[derive(Debug, Copy, Clone)] 104 | pub enum Channels { 105 | Mono, 106 | Stereo(ChannelInterleavement), 107 | } 108 | 109 | impl Channels { 110 | pub const fn is_mono(&self) -> bool { 111 | matches!(self, Self::Mono) 112 | } 113 | 114 | pub const fn is_stereo(&self) -> bool { 115 | matches!(self, Self::Stereo(_)) 116 | } 117 | 118 | pub fn stereo_interleavement(&self) -> ChannelInterleavement { 119 | match self { 120 | Self::Stereo(interleavmement) => *interleavmement, 121 | _ => panic!("Not stereo"), 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/spectrum/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | //! Module for several frequency spectrum visualization implementations. 26 | //! 27 | //! This module focuses on static visualization. For dynamic visualization, 28 | //! look into the [`crate::dynamic`] module + corresponding examples in `examples/`. 29 | 30 | pub mod plotters_png_file; 31 | pub mod png_file; 32 | -------------------------------------------------------------------------------- /src/spectrum/plotters_png_file.rs: -------------------------------------------------------------------------------- 1 | //! Static spectrum analysis: print spectrum to PNG file. 2 | 3 | use plotters::prelude::*; 4 | use std::collections::BTreeMap; 5 | use std::fs; 6 | use std::path::PathBuf; 7 | 8 | pub fn spectrum_static_plotters_png_visualize( 9 | frequency_spectrum: &BTreeMap, 10 | directory: &str, 11 | filename: &str, 12 | ) { 13 | // assert no NAN 14 | assert!( 15 | !frequency_spectrum.iter().any(|(_, f)| f.is_nan()), 16 | "There are NAN-values in the spectrum!" 17 | ); 18 | 19 | // find maximum for graphics scaling 20 | let mut max = 0.0; 21 | for mag in frequency_spectrum.values() { 22 | if *mag > max { 23 | max = *mag; 24 | } 25 | } 26 | 27 | let max_frequency = *frequency_spectrum 28 | .iter() 29 | .skip(frequency_spectrum.len() - 2) 30 | .last() 31 | .unwrap() 32 | .0; 33 | 34 | if !fs::exists(directory).unwrap() { 35 | fs::create_dir(directory).unwrap(); 36 | } 37 | let mut path = PathBuf::new(); 38 | path.push(directory); 39 | path.push(filename); 40 | 41 | let mut width = frequency_spectrum.len() as u32; 42 | if width < 700 { 43 | width = 700; 44 | } 45 | 46 | let height = if width < 700 { 47 | (width as f32 / 0.8) as u32 48 | } else { 49 | 700 50 | }; 51 | 52 | let root = BitMapBackend::new(&path, (width, height)).into_drawing_area(); 53 | root.fill(&WHITE).unwrap(); 54 | let mut chart = ChartBuilder::on(&root) 55 | .caption("y=f magnitudes of sample", ("sans-serif", 20).into_font()) 56 | .margin(5) 57 | .x_label_area_size(60) 58 | .y_label_area_size(60) 59 | .build_cartesian_2d(0.0..(max_frequency as f32) /*.log10()*/, 0.0..max) 60 | .unwrap(); 61 | 62 | chart.configure_mesh().draw().unwrap(); 63 | 64 | chart 65 | .draw_series(LineSeries::new( 66 | // (-50..=50).map(|x| x as f32 / 50.0).map(|x| (x, x * x)), 67 | frequency_spectrum 68 | .iter() 69 | .map(|(frequency, magnitude)| ((*frequency as f32) /*.log10()*/, *magnitude)), 70 | &RED, 71 | )) 72 | .unwrap() 73 | .label("frequency magnitude") 74 | .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED)); 75 | 76 | chart 77 | .configure_series_labels() 78 | .background_style(WHITE.mix(0.8)) 79 | .border_style(BLACK) 80 | .draw() 81 | .unwrap(); 82 | } 83 | 84 | #[cfg(test)] 85 | mod tests { 86 | use super::*; 87 | use crate::tests::testutil::TEST_OUT_DIR; 88 | 89 | #[test] 90 | fn test_visualize_sine_waves_spectrum_plotters() { 91 | let mut spectrum = BTreeMap::new(); 92 | spectrum.insert(0, 0.0); 93 | spectrum.insert(10, 5.0); 94 | spectrum.insert(20, 20.0); 95 | spectrum.insert(30, 40.0); 96 | spectrum.insert(40, 80.0); 97 | spectrum.insert(50, 120.0); 98 | spectrum.insert(55, 130.0); 99 | spectrum.insert(60, 140.0); 100 | spectrum.insert(65, 130.0); 101 | spectrum.insert(70, 120.0); 102 | spectrum.insert(80, 80.0); 103 | spectrum.insert(90, 40.0); 104 | spectrum.insert(100, 20.0); 105 | spectrum.insert(110, 5.0); 106 | spectrum.insert(120, 0.0); 107 | spectrum.insert(130, 0.0); 108 | 109 | spectrum_static_plotters_png_visualize( 110 | &spectrum, 111 | TEST_OUT_DIR, 112 | "spectrum_60hz_peak_plotters_visualization.png", 113 | ); 114 | } 115 | 116 | #[allow(non_snake_case)] 117 | #[test] 118 | #[should_panic] 119 | fn test_panic_on_NAN() { 120 | let mut spectrum = BTreeMap::new(); 121 | spectrum.insert(0, f32::NAN); 122 | 123 | spectrum_static_plotters_png_visualize( 124 | &spectrum, 125 | TEST_OUT_DIR, 126 | "spectrum_60hz_peak_plotters_visualization_NAN.png", 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/spectrum/png_file.rs: -------------------------------------------------------------------------------- 1 | //! Static spectrum analysis: print spectrum to PNG file. 2 | 3 | use crate::util::png::write_png_file_rgb_tuples; 4 | use std::collections::BTreeMap; 5 | use std::fs; 6 | use std::path::PathBuf; 7 | 8 | pub fn spectrum_static_png_visualize( 9 | frequency_spectrum: &BTreeMap, 10 | directory: &str, 11 | filename: &str, 12 | highlighted_frequencies: &[f32], 13 | ) { 14 | // assert no NAN 15 | assert!( 16 | !frequency_spectrum.iter().any(|(_, f)| f.is_nan()), 17 | "There are NAN-values in the spectrum!" 18 | ); 19 | 20 | let image_width = 5000; 21 | let image_height = 3000; 22 | 23 | let mut rgb_img = vec![vec![(255, 255, 255); image_width]; image_height]; 24 | 25 | // find maximum for graphics scaling 26 | let mut max = 0.0; 27 | for mag in frequency_spectrum.values() { 28 | if *mag > max { 29 | max = *mag; 30 | } 31 | } 32 | 33 | let x_step = image_width as f64 / frequency_spectrum.len() as f64; 34 | for (i, (frequency, mag)) in frequency_spectrum.iter().enumerate() { 35 | let mag = mag / max * image_height as f32; 36 | 37 | let x = (i as f64 * x_step) as usize; 38 | 39 | for j in 0..mag as usize { 40 | let mut color = (0, 0, 0); 41 | 42 | let highlight = highlighted_frequencies 43 | .iter() 44 | .any(|f| (*frequency as f32 - *f).abs() < 5.0); 45 | if highlight { 46 | color = (255, 0, 0); 47 | } 48 | 49 | // make it wider 50 | if x > 2 && highlight { 51 | rgb_img[image_height - 1 - j][x - 1] = color; 52 | rgb_img[image_height - 1 - j][x - 2] = color; 53 | } 54 | rgb_img[image_height - 1 - j][x] = color; 55 | } 56 | } 57 | 58 | if !fs::exists(directory).unwrap() { 59 | fs::create_dir(directory).unwrap(); 60 | } 61 | let mut path = PathBuf::new(); 62 | path.push(directory); 63 | path.push(filename); 64 | write_png_file_rgb_tuples(&path, &rgb_img); 65 | } 66 | 67 | #[cfg(test)] 68 | mod tests { 69 | use super::*; 70 | use crate::tests::testutil::TEST_OUT_DIR; 71 | 72 | #[test] 73 | fn test_visualize_sine_waves_spectrum() { 74 | let mut spectrum = BTreeMap::new(); 75 | spectrum.insert(0, 0.0); 76 | spectrum.insert(10, 5.0); 77 | spectrum.insert(20, 20.0); 78 | spectrum.insert(30, 40.0); 79 | spectrum.insert(40, 80.0); 80 | spectrum.insert(50, 120.0); 81 | spectrum.insert(55, 130.0); 82 | spectrum.insert(60, 140.0); 83 | spectrum.insert(65, 130.0); 84 | spectrum.insert(70, 120.0); 85 | spectrum.insert(80, 80.0); 86 | spectrum.insert(90, 40.0); 87 | spectrum.insert(100, 20.0); 88 | spectrum.insert(110, 5.0); 89 | spectrum.insert(120, 0.0); 90 | spectrum.insert(130, 0.0); 91 | 92 | // Do FFT + get spectrum 93 | spectrum_static_png_visualize( 94 | &spectrum, 95 | TEST_OUT_DIR, 96 | "spectrum_60hz_peak_basic_visualization.png", 97 | &[60.0], 98 | ); 99 | } 100 | 101 | #[allow(non_snake_case)] 102 | #[test] 103 | #[should_panic] 104 | fn test_panic_on_NAN() { 105 | let mut spectrum = BTreeMap::new(); 106 | spectrum.insert(0, f32::NAN); 107 | 108 | spectrum_static_png_visualize( 109 | &spectrum, 110 | TEST_OUT_DIR, 111 | "spectrum_60hz_peak_plotters_visualization_NAN.png", 112 | &[], 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module for all tests and "example-like" tests. 2 | 3 | pub mod testutil; 4 | pub mod visualize_sine_10hz; 5 | pub mod visualize_sine_50hz_plus_250hz; 6 | -------------------------------------------------------------------------------- /src/tests/testutil/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::path::Path; 3 | use symphonia::core::audio::{AudioBuffer, Signal}; 4 | use symphonia::core::io::MediaSourceStream; 5 | use symphonia::core::probe::Hint; 6 | use symphonia::default::{get_codecs, get_probe}; 7 | 8 | pub mod sine; 9 | 10 | /// Directory with test samples (e.g. mp3) can be found here. 11 | pub const TEST_SAMPLES_DIR: &str = "test/samples"; 12 | /// If tests create files, they should be stored here. 13 | pub const TEST_OUT_DIR: &str = "target/test_out"; 14 | 15 | /// Returns an MP3 as decoded i16 samples and with LRLR interleavement. 16 | pub fn decode_mp3(file: &Path) -> Vec { 17 | let file = File::open(file).unwrap(); 18 | let mss = MediaSourceStream::new(Box::new(file), Default::default()); 19 | let probed = get_probe() 20 | .format( 21 | &Hint::default(), 22 | mss, 23 | &Default::default(), 24 | &Default::default(), 25 | ) 26 | .unwrap(); 27 | let mut format_reader = probed.format; 28 | let track = format_reader.tracks().first().unwrap(); 29 | let mut decoder = get_codecs() 30 | .make(&track.codec_params, &Default::default()) 31 | .unwrap(); 32 | 33 | let mut audio_data_lrlr = Vec::new(); 34 | while let Ok(packet) = format_reader.next_packet() { 35 | if let Ok(audio_buf_ref) = decoder.decode(&packet) { 36 | let audio_spec = audio_buf_ref.spec(); 37 | let mut audio_buf_i16 = 38 | AudioBuffer::::new(audio_buf_ref.frames() as u64, *audio_spec); 39 | audio_buf_ref.convert(&mut audio_buf_i16); 40 | 41 | match audio_spec.channels.count() { 42 | 2 => { 43 | let iter = audio_buf_i16 44 | .chan(0) 45 | .iter() 46 | .zip(audio_buf_i16.chan(1)) 47 | // LRLR interleavment 48 | .flat_map(|(&l, &r)| [l, r]); 49 | //.map(|(&l, &r)| ((l as i32 + r as i32) / 2) as i16); 50 | audio_data_lrlr.extend(iter); 51 | } 52 | n => panic!("Unsupported amount of channels: {n}"), 53 | } 54 | } 55 | } 56 | audio_data_lrlr 57 | } 58 | -------------------------------------------------------------------------------- /src/tests/testutil/sine.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | use std::f64::consts::PI; 25 | 26 | /// Creates a sine (sinus) wave function for a given frequency. 27 | /// Don't forget to scale up the value to the audio resolution. 28 | /// So far, amplitude is in interval `[-1; 1]`. The parameter 29 | /// of the returned function is the point in time in seconds. 30 | /// 31 | /// * `frequency` is in Hertz 32 | pub fn sine_wave(frequency: f64) -> Box f64> { 33 | Box::new(move |t| (t * frequency * 2.0 * PI).sin()) 34 | } 35 | 36 | /// See [`sine_wave_audio_data_multiple`] 37 | pub fn sine_wave_audio_data(frequency: f64, sampling_rate: u32, duration_ms: u32) -> Vec { 38 | sine_wave_audio_data_multiple(&[frequency], sampling_rate, duration_ms) 39 | } 40 | 41 | /// Like [`sine_wave_audio_data`] but puts multiple sinus waves on top of each other. 42 | /// Returns a audio signal encoded in 16 bit audio resolution which is the sum of 43 | /// multiple sine waves on top of each other. The amplitudes will be scaled from 44 | /// `[-1; 1]` to `[i16::min_value(); i16::max_value()]` 45 | /// 46 | /// * `frequency` frequency in Hz for the sinus wave 47 | /// * `sampling_rate` sampling rate, i.e. 44100Hz 48 | /// * `duration_ms` duration of the audio data in milliseconds 49 | pub fn sine_wave_audio_data_multiple( 50 | frequencies: &[f64], 51 | sampling_rate: u32, 52 | duration_ms: u32, 53 | ) -> Vec { 54 | if frequencies.is_empty() { 55 | return vec![]; 56 | } 57 | 58 | // Generate all sine wave function 59 | let sine_waves = frequencies 60 | .iter() 61 | .map(|f| sine_wave(*f)) 62 | .collect:: f64>>>(); 63 | 64 | // How many samples to generate with each sine wave function 65 | let sample_count = (sampling_rate as f64 * (duration_ms as f64 / 1000_f64)) as usize; 66 | 67 | // Calculate the final sine wave 68 | let mut sine_wave = Vec::with_capacity(sample_count); 69 | for i_sample in 0..sample_count { 70 | // t: time 71 | let t = (1.0 / sampling_rate as f64) * i_sample as f64; 72 | 73 | // BEGIN: add sine waves 74 | let mut acc = 0.0; 75 | for sine_wave in &sine_waves { 76 | acc += sine_wave(t); 77 | } 78 | // END: add sine waves 79 | 80 | // BEGIN: scale 81 | // times 0.6 to prevent to harsh clipping if multiple sinus waves are added above each other 82 | let acc = acc * i16::MAX as f64 * 0.6; 83 | // END: scale 84 | 85 | // BEGIN: truncate in interval 86 | let acc = if acc > i16::MAX as f64 { 87 | i16::MAX 88 | } else if acc < i16::MIN as f64 { 89 | i16::MIN 90 | } else { 91 | acc as i16 92 | }; 93 | // END: truncate in interval 94 | 95 | sine_wave.push(acc) 96 | } 97 | 98 | sine_wave 99 | } 100 | -------------------------------------------------------------------------------- /src/tests/visualize_sine_10hz.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | use crate::tests::testutil::sine::sine_wave_audio_data; 26 | use crate::tests::testutil::TEST_OUT_DIR; 27 | use crate::waveform::png_file::waveform_static_png_visualize; 28 | use crate::Channels; 29 | 30 | #[test] 31 | fn visualize_sine_10hz() { 32 | let frequency = 10_f64; 33 | let sampling_rate = 44100; 34 | let duration_ms = 1000; 35 | // we expect 10 time periods of the sine wav in the time interval 36 | let audio_signal = sine_wave_audio_data(frequency, sampling_rate, duration_ms); 37 | waveform_static_png_visualize( 38 | &audio_signal, 39 | Channels::Mono, 40 | TEST_OUT_DIR, 41 | "sinus-wave-10hz.png", 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/tests/visualize_sine_50hz_plus_250hz.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | use crate::tests::testutil::sine::sine_wave_audio_data_multiple; 25 | use crate::tests::testutil::TEST_OUT_DIR; 26 | use crate::waveform::png_file::waveform_static_png_visualize; 27 | use crate::Channels; 28 | 29 | #[test] 30 | fn visualize_sine_50hz_plus_250hz() { 31 | let sampling_rate = 44100; 32 | let duration_ms = 100; 33 | let sin_audio_sum = sine_wave_audio_data_multiple( 34 | // 50Hz in 100ms => sin wave will have five time periods 35 | // 250Hz in 100ms => sin wave will have twenty-five time periods 36 | &[50_f64, 250_f64], 37 | sampling_rate, 38 | duration_ms, 39 | ); 40 | waveform_static_png_visualize( 41 | &sin_audio_sum, 42 | Channels::Mono, 43 | TEST_OUT_DIR, 44 | "sinus-wave-50hz_plus_250hz.png", 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | //! Common utility functions required in multiple other modules. 26 | 27 | pub mod png; 28 | -------------------------------------------------------------------------------- /src/util/png.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | use std::fs::File; 25 | use std::io::BufWriter; 26 | use std::path::Path; 27 | 28 | /// Writes RGB-bytes into the given file using [`png`]-crate. 29 | pub fn write_png_file_u8(file: &Path, rgb_data: &[u8], image_width: u32, image_height: u32) { 30 | let file = File::create(file).unwrap(); 31 | let mut writer = BufWriter::new(file); 32 | 33 | let mut encoder = png::Encoder::new(&mut writer, image_width, image_height); 34 | encoder.set_color(png::ColorType::Rgb); 35 | encoder.set_depth(png::BitDepth::Eight); 36 | let mut writer = encoder.write_header().unwrap(); 37 | 38 | writer.write_image_data(rgb_data).unwrap(); 39 | } 40 | 41 | /// Wrapper around [`write_png_file_u8`] that takes a vector of vectors with RGB-tuples. 42 | /// (rows, cols). 43 | pub fn write_png_file_rgb_tuples(file: &Path, rgb_image: &[Vec<(u8, u8, u8)>]) { 44 | let width = rgb_image[0].len() as u32; 45 | let height = rgb_image.len() as u32; 46 | 47 | // data must be RGBA sequence: RGBARGBARGBA... 48 | let rgb_data = rgb_image 49 | .iter() 50 | // get iter over each row 51 | .flat_map(|row| row.iter()) 52 | .flat_map(|(r, g, b)| vec![r, g, b].into_iter()) 53 | .copied() 54 | .collect::>(); 55 | 56 | write_png_file_u8(file, &rgb_data, width, height) 57 | } 58 | -------------------------------------------------------------------------------- /src/waveform/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | //! Module for several waveform visualization implementations. 25 | //! 26 | //! This module focuses on static visualization. For dynamic visualization, 27 | //! look into the [`crate::dynamic`] module + corresponding examples in `examples/`. 28 | 29 | pub mod plotters_png_file; 30 | pub mod png_file; 31 | -------------------------------------------------------------------------------- /src/waveform/plotters_png_file.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | //! Static waveform visualization which exports the waveform to a PNG file. 25 | 26 | use crate::Channels; 27 | use plotters::prelude::*; 28 | use std::fs; 29 | use std::path::PathBuf; 30 | 31 | /// Visualizes audio as a waveform in a png file using "plotters" crate. 32 | /// If the data is stereo, it creates two files (with "left_" and "right_" prefix). 33 | pub fn waveform_static_plotters_png_visualize( 34 | samples: &[i16], 35 | channels: Channels, 36 | directory: &str, 37 | filename: &str, 38 | ) { 39 | if channels.is_stereo() { 40 | assert_eq!( 41 | 0, 42 | samples.len() % 2, 43 | "If stereo is provided, the length of the audio data must be even!" 44 | ); 45 | let (left, right) = channels.stereo_interleavement().to_channel_data(samples); 46 | waveform_static_plotters_png_visualize( 47 | &left, 48 | Channels::Mono, 49 | directory, 50 | &format!("left_{}", filename), 51 | ); 52 | waveform_static_plotters_png_visualize( 53 | &right, 54 | Channels::Mono, 55 | directory, 56 | &format!("right_{}", filename), 57 | ); 58 | return; 59 | } 60 | 61 | if !fs::exists(directory).unwrap() { 62 | fs::create_dir(directory).unwrap(); 63 | } 64 | let mut path = PathBuf::new(); 65 | path.push(directory); 66 | path.push(filename); 67 | 68 | let mut max = 0; 69 | for sample in samples { 70 | let sample = *sample as i32; 71 | let sample = sample.abs(); 72 | if sample > max { 73 | max = sample; 74 | } 75 | } 76 | 77 | let width = (samples.len() / 5) as u32; 78 | let width = if width > 4000 { 4000 } else { width }; 79 | let root = BitMapBackend::new(&path, (width, 1000)).into_drawing_area(); 80 | root.fill(&WHITE).unwrap(); 81 | let mut chart = ChartBuilder::on(&root) 82 | .caption("y=music(t)", ("sans-serif", 50).into_font()) 83 | .margin(5) 84 | .x_label_area_size(30) 85 | .y_label_area_size(30) 86 | .build_cartesian_2d(0.0..samples.len() as f32, -max as f32..max as f32) 87 | .unwrap(); 88 | 89 | chart.configure_mesh().draw().unwrap(); 90 | 91 | chart 92 | .draw_series(LineSeries::new( 93 | // (-50..=50).map(|x| x as f32 / 50.0).map(|x| (x, x * x)), 94 | samples 95 | .iter() 96 | .enumerate() 97 | .map(|(sample_i, amplitude)| (sample_i as f32, *amplitude as f32)), 98 | &RED, 99 | )) 100 | .unwrap() 101 | // .label("y = music(t)") 102 | .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED)); 103 | 104 | chart 105 | .configure_series_labels() 106 | .background_style(WHITE.mix(0.8)) 107 | .border_style(BLACK) 108 | .draw() 109 | .unwrap(); 110 | } 111 | 112 | #[cfg(test)] 113 | mod tests { 114 | use super::*; 115 | use crate::tests::testutil::{decode_mp3, TEST_OUT_DIR, TEST_SAMPLES_DIR}; 116 | use crate::ChannelInterleavement; 117 | 118 | #[test] 119 | fn test_visualize_png_output() { 120 | let mut path = PathBuf::new(); 121 | path.push(TEST_SAMPLES_DIR); 122 | path.push("sample_1.mp3"); 123 | 124 | let lrlr_mp3_samples = decode_mp3(path.as_path()); 125 | 126 | waveform_static_plotters_png_visualize( 127 | &lrlr_mp3_samples, 128 | Channels::Stereo(ChannelInterleavement::LRLR), 129 | TEST_OUT_DIR, 130 | "waveform_static_plotters_png_visualize_example.png", 131 | ); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/waveform/png_file.rs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Philipp Schuster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | //! Static waveform visualization which exports the waveform to a PNG file. 25 | 26 | use crate::util::png::write_png_file_rgb_tuples; 27 | use crate::Channels; 28 | use std::fs; 29 | use std::path::PathBuf; 30 | 31 | /// Visualizes audio as a waveform in a png file in the most simple way. 32 | /// 33 | /// There are no axes. If the audio data is mono, it creates one file. 34 | /// If the data is stereo, it creates two files (with "left_" and "right_" prefix). 35 | pub fn waveform_static_png_visualize( 36 | samples: &[i16], 37 | channels: Channels, 38 | directory: &str, 39 | filename: &str, 40 | ) { 41 | let image_width = 1500; 42 | let image_height = 200; 43 | if channels.is_stereo() { 44 | assert_eq!( 45 | 0, 46 | samples.len() % 2, 47 | "If stereo is provided, the length of the audio data must be even!" 48 | ); 49 | let (left, right) = channels.stereo_interleavement().to_channel_data(samples); 50 | waveform_static_png_visualize( 51 | &left, 52 | Channels::Mono, 53 | directory, 54 | &format!("left_{}", filename), 55 | ); 56 | waveform_static_png_visualize( 57 | &right, 58 | Channels::Mono, 59 | directory, 60 | &format!("right_{}", filename), 61 | ); 62 | return; 63 | } 64 | 65 | // needed for offset calculation; width per sample 66 | let width_per_sample = image_width as f64 / samples.len() as f64; 67 | // height in pixel per possible value of a sample; counts in that the y axis lays in the middle 68 | let height_per_max_amplitude = image_height as f64 / 2_f64 / i16::MAX as f64; 69 | 70 | // RGB image data 71 | let mut image = vec![vec![(255, 255, 255); image_width]; image_height]; 72 | for (sample_index, sample_value) in samples.iter().enumerate() { 73 | // x offset; from left 74 | let x = (sample_index as f64 * width_per_sample) as usize; 75 | // y offset; from top 76 | // image_height/2: there is our y-axis 77 | let sample_value = *sample_value as f64 * -1.0; // y axis grows downwards 78 | let mut y = ((image_height / 2) as f64 + sample_value * height_per_max_amplitude) as usize; 79 | 80 | // due to rounding it can happen that we get out of bounds 81 | if y == image_height { 82 | y -= 1; 83 | } 84 | 85 | image[y][x] = (0, 0, 0); 86 | } 87 | 88 | if !fs::exists(directory).unwrap() { 89 | fs::create_dir(directory).unwrap(); 90 | } 91 | let mut path = PathBuf::new(); 92 | path.push(directory); 93 | path.push(filename); 94 | write_png_file_rgb_tuples(&path, &image); 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use super::*; 100 | use crate::tests::testutil::{decode_mp3, TEST_OUT_DIR, TEST_SAMPLES_DIR}; 101 | use crate::ChannelInterleavement; 102 | 103 | /// This test works, if it doesn't panic. 104 | #[test] 105 | fn test_no_out_of_bounds_panic() { 106 | let audio_data = vec![i16::MAX, i16::MIN]; 107 | waveform_static_png_visualize( 108 | &audio_data, 109 | Channels::Mono, 110 | TEST_OUT_DIR, 111 | "sample_1_waveform-test-out-of-bounds-check.png", 112 | ); 113 | } 114 | 115 | #[test] 116 | fn test_visualize_png_output() { 117 | let mut path = PathBuf::new(); 118 | path.push(TEST_SAMPLES_DIR); 119 | path.push("sample_1.mp3"); 120 | 121 | let lrlr_mp3_samples = decode_mp3(path.as_path()); 122 | 123 | waveform_static_png_visualize( 124 | &lrlr_mp3_samples, 125 | Channels::Stereo(ChannelInterleavement::LRLR), 126 | TEST_OUT_DIR, 127 | "waveform_static_png_visualize_example.png", 128 | ); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /test/samples/sample_1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phip1611/audio-visualizer/3caf9e089f19f2bfbd82f56961e541f6a33d2c25/test/samples/sample_1.mp3 --------------------------------------------------------------------------------