A decentralized music tracking and discovery platform built on AT Protocol 🎵

update install instructions

+1503 -1
+14
Cargo.lock
··· 4077 4077 checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" 4078 4078 4079 4079 [[package]] 4080 + name = "raichu" 4081 + version = "0.1.0" 4082 + dependencies = [ 4083 + "base64 0.22.1", 4084 + "getrandom 0.2.16", 4085 + "js-sys", 4086 + "rand 0.8.5", 4087 + "serde_json", 4088 + "symphonia", 4089 + "wasm-bindgen", 4090 + "web-sys", 4091 + ] 4092 + 4093 + [[package]] 4080 4094 name = "rand" 4081 4095 version = "0.8.5" 4082 4096 source = "registry+https://github.com/rust-lang/crates.io-index"
+2
README.md
··· 8 8 - Rust 9 9 - Turbo 10 10 - Docker 11 + - Wasm Pack https://rustwasm.github.io/wasm-pack/installer/ 11 12 12 13 ## Getting Started 13 14 ··· 20 21 ```bash 21 22 npm install -g turbo 22 23 bun install 24 + bun run build:raichu 23 25 ``` 24 26 3. Set up the environment variables: 25 27 ```bash
+11
crates/raichu/.appveyor.yml
··· 1 + install: 2 + - appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe 3 + - if not defined RUSTFLAGS rustup-init.exe -y --default-host x86_64-pc-windows-msvc --default-toolchain nightly 4 + - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin 5 + - rustc -V 6 + - cargo -V 7 + 8 + build: false 9 + 10 + test_script: 11 + - cargo test --locked
+8
crates/raichu/.github/dependabot.yml
··· 1 + version: 2 2 + updates: 3 + - package-ecosystem: cargo 4 + directory: "/" 5 + schedule: 6 + interval: daily 7 + time: "08:00" 8 + open-pull-requests-limit: 10
+6
crates/raichu/.gitignore
··· 1 + /target 2 + **/*.rs.bk 3 + Cargo.lock 4 + bin/ 5 + pkg/ 6 + wasm-pack.log
+69
crates/raichu/.travis.yml
··· 1 + language: rust 2 + sudo: false 3 + 4 + cache: cargo 5 + 6 + matrix: 7 + include: 8 + 9 + # Builds with wasm-pack. 10 + - rust: beta 11 + env: RUST_BACKTRACE=1 12 + addons: 13 + firefox: latest 14 + chrome: stable 15 + before_script: 16 + - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) 17 + - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) 18 + - cargo install-update -a 19 + - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f 20 + script: 21 + - cargo generate --git . --name testing 22 + # Having a broken Cargo.toml (in that it has curlies in fields) anywhere 23 + # in any of our parent dirs is problematic. 24 + - mv Cargo.toml Cargo.toml.tmpl 25 + - cd testing 26 + - wasm-pack build 27 + - wasm-pack test --chrome --firefox --headless 28 + 29 + # Builds on nightly. 30 + - rust: nightly 31 + env: RUST_BACKTRACE=1 32 + before_script: 33 + - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) 34 + - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) 35 + - cargo install-update -a 36 + - rustup target add wasm32-unknown-unknown 37 + script: 38 + - cargo generate --git . --name testing 39 + - mv Cargo.toml Cargo.toml.tmpl 40 + - cd testing 41 + - cargo check 42 + - cargo check --target wasm32-unknown-unknown 43 + - cargo check --no-default-features 44 + - cargo check --target wasm32-unknown-unknown --no-default-features 45 + - cargo check --no-default-features --features console_error_panic_hook 46 + - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook 47 + - cargo check --no-default-features --features "console_error_panic_hook wee_alloc" 48 + - cargo check --target wasm32-unknown-unknown --no-default-features --features "console_error_panic_hook wee_alloc" 49 + 50 + # Builds on beta. 51 + - rust: beta 52 + env: RUST_BACKTRACE=1 53 + before_script: 54 + - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) 55 + - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) 56 + - cargo install-update -a 57 + - rustup target add wasm32-unknown-unknown 58 + script: 59 + - cargo generate --git . --name testing 60 + - mv Cargo.toml Cargo.toml.tmpl 61 + - cd testing 62 + - cargo check 63 + - cargo check --target wasm32-unknown-unknown 64 + - cargo check --no-default-features 65 + - cargo check --target wasm32-unknown-unknown --no-default-features 66 + - cargo check --no-default-features --features console_error_panic_hook 67 + - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook 68 + # Note: no enabling the `wee_alloc` feature here because it requires 69 + # nightly for now.
+21
crates/raichu/Cargo.toml
··· 1 + [package] 2 + name = "raichu" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [lib] 7 + crate-type = ["cdylib"] 8 + 9 + [dependencies] 10 + js-sys = "0.3.77" 11 + rand = "0.8.5" 12 + getrandom = { version = "0.2", features = ["js"] } 13 + symphonia = { version = "0.5.4", features = ["all"] } 14 + wasm-bindgen = "0.2.100" 15 + web-sys = { version = "0.3.77", features = [ 16 + "AudioContext", 17 + "AudioBuffer", 18 + "AudioBufferSourceNode", 19 + ] } 20 + serde_json = "1.0.140" 21 + base64 = "0.22.1"
+201
crates/raichu/LICENSE_APACHE
··· 1 + Apache License 2 + Version 2.0, January 2004 3 + http://www.apache.org/licenses/ 4 + 5 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 + 7 + 1. Definitions. 8 + 9 + "License" shall mean the terms and conditions for use, reproduction, 10 + and distribution as defined by Sections 1 through 9 of this document. 11 + 12 + "Licensor" shall mean the copyright owner or entity authorized by 13 + the copyright owner that is granting the License. 14 + 15 + "Legal Entity" shall mean the union of the acting entity and all 16 + other entities that control, are controlled by, or are under common 17 + control with that entity. For the purposes of this definition, 18 + "control" means (i) the power, direct or indirect, to cause the 19 + direction or management of such entity, whether by contract or 20 + otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 + outstanding shares, or (iii) beneficial ownership of such entity. 22 + 23 + "You" (or "Your") shall mean an individual or Legal Entity 24 + exercising permissions granted by this License. 25 + 26 + "Source" form shall mean the preferred form for making modifications, 27 + including but not limited to software source code, documentation 28 + source, and configuration files. 29 + 30 + "Object" form shall mean any form resulting from mechanical 31 + transformation or translation of a Source form, including but 32 + not limited to compiled object code, generated documentation, 33 + and conversions to other media types. 34 + 35 + "Work" shall mean the work of authorship, whether in Source or 36 + Object form, made available under the License, as indicated by a 37 + copyright notice that is included in or attached to the work 38 + (an example is provided in the Appendix below). 39 + 40 + "Derivative Works" shall mean any work, whether in Source or Object 41 + form, that is based on (or derived from) the Work and for which the 42 + editorial revisions, annotations, elaborations, or other modifications 43 + represent, as a whole, an original work of authorship. For the purposes 44 + of this License, Derivative Works shall not include works that remain 45 + separable from, or merely link (or bind by name) to the interfaces of, 46 + the Work and Derivative Works thereof. 47 + 48 + "Contribution" shall mean any work of authorship, including 49 + the original version of the Work and any modifications or additions 50 + to that Work or Derivative Works thereof, that is intentionally 51 + submitted to Licensor for inclusion in the Work by the copyright owner 52 + or by an individual or Legal Entity authorized to submit on behalf of 53 + the copyright owner. For the purposes of this definition, "submitted" 54 + means any form of electronic, verbal, or written communication sent 55 + to the Licensor or its representatives, including but not limited to 56 + communication on electronic mailing lists, source code control systems, 57 + and issue tracking systems that are managed by, or on behalf of, the 58 + Licensor for the purpose of discussing and improving the Work, but 59 + excluding communication that is conspicuously marked or otherwise 60 + designated in writing by the copyright owner as "Not a Contribution." 61 + 62 + "Contributor" shall mean Licensor and any individual or Legal Entity 63 + on behalf of whom a Contribution has been received by Licensor and 64 + subsequently incorporated within the Work. 65 + 66 + 2. Grant of Copyright License. Subject to the terms and conditions of 67 + this License, each Contributor hereby grants to You a perpetual, 68 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 + copyright license to reproduce, prepare Derivative Works of, 70 + publicly display, publicly perform, sublicense, and distribute the 71 + Work and such Derivative Works in Source or Object form. 72 + 73 + 3. Grant of Patent License. Subject to the terms and conditions of 74 + this License, each Contributor hereby grants to You a perpetual, 75 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 + (except as stated in this section) patent license to make, have made, 77 + use, offer to sell, sell, import, and otherwise transfer the Work, 78 + where such license applies only to those patent claims licensable 79 + by such Contributor that are necessarily infringed by their 80 + Contribution(s) alone or by combination of their Contribution(s) 81 + with the Work to which such Contribution(s) was submitted. If You 82 + institute patent litigation against any entity (including a 83 + cross-claim or counterclaim in a lawsuit) alleging that the Work 84 + or a Contribution incorporated within the Work constitutes direct 85 + or contributory patent infringement, then any patent licenses 86 + granted to You under this License for that Work shall terminate 87 + as of the date such litigation is filed. 88 + 89 + 4. Redistribution. You may reproduce and distribute copies of the 90 + Work or Derivative Works thereof in any medium, with or without 91 + modifications, and in Source or Object form, provided that You 92 + meet the following conditions: 93 + 94 + (a) You must give any other recipients of the Work or 95 + Derivative Works a copy of this License; and 96 + 97 + (b) You must cause any modified files to carry prominent notices 98 + stating that You changed the files; and 99 + 100 + (c) You must retain, in the Source form of any Derivative Works 101 + that You distribute, all copyright, patent, trademark, and 102 + attribution notices from the Source form of the Work, 103 + excluding those notices that do not pertain to any part of 104 + the Derivative Works; and 105 + 106 + (d) If the Work includes a "NOTICE" text file as part of its 107 + distribution, then any Derivative Works that You distribute must 108 + include a readable copy of the attribution notices contained 109 + within such NOTICE file, excluding those notices that do not 110 + pertain to any part of the Derivative Works, in at least one 111 + of the following places: within a NOTICE text file distributed 112 + as part of the Derivative Works; within the Source form or 113 + documentation, if provided along with the Derivative Works; or, 114 + within a display generated by the Derivative Works, if and 115 + wherever such third-party notices normally appear. The contents 116 + of the NOTICE file are for informational purposes only and 117 + do not modify the License. You may add Your own attribution 118 + notices within Derivative Works that You distribute, alongside 119 + or as an addendum to the NOTICE text from the Work, provided 120 + that such additional attribution notices cannot be construed 121 + as modifying the License. 122 + 123 + You may add Your own copyright statement to Your modifications and 124 + may provide additional or different license terms and conditions 125 + for use, reproduction, or distribution of Your modifications, or 126 + for any such Derivative Works as a whole, provided Your use, 127 + reproduction, and distribution of the Work otherwise complies with 128 + the conditions stated in this License. 129 + 130 + 5. Submission of Contributions. Unless You explicitly state otherwise, 131 + any Contribution intentionally submitted for inclusion in the Work 132 + by You to the Licensor shall be under the terms and conditions of 133 + this License, without any additional terms or conditions. 134 + Notwithstanding the above, nothing herein shall supersede or modify 135 + the terms of any separate license agreement you may have executed 136 + with Licensor regarding such Contributions. 137 + 138 + 6. Trademarks. This License does not grant permission to use the trade 139 + names, trademarks, service marks, or product names of the Licensor, 140 + except as required for reasonable and customary use in describing the 141 + origin of the Work and reproducing the content of the NOTICE file. 142 + 143 + 7. Disclaimer of Warranty. Unless required by applicable law or 144 + agreed to in writing, Licensor provides the Work (and each 145 + Contributor provides its Contributions) on an "AS IS" BASIS, 146 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 + implied, including, without limitation, any warranties or conditions 148 + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 + PARTICULAR PURPOSE. You are solely responsible for determining the 150 + appropriateness of using or redistributing the Work and assume any 151 + risks associated with Your exercise of permissions under this License. 152 + 153 + 8. Limitation of Liability. In no event and under no legal theory, 154 + whether in tort (including negligence), contract, or otherwise, 155 + unless required by applicable law (such as deliberate and grossly 156 + negligent acts) or agreed to in writing, shall any Contributor be 157 + liable to You for damages, including any direct, indirect, special, 158 + incidental, or consequential damages of any character arising as a 159 + result of this License or out of the use or inability to use the 160 + Work (including but not limited to damages for loss of goodwill, 161 + work stoppage, computer failure or malfunction, or any and all 162 + other commercial damages or losses), even if such Contributor 163 + has been advised of the possibility of such damages. 164 + 165 + 9. Accepting Warranty or Additional Liability. While redistributing 166 + the Work or Derivative Works thereof, You may choose to offer, 167 + and charge a fee for, acceptance of support, warranty, indemnity, 168 + or other liability obligations and/or rights consistent with this 169 + License. However, in accepting such obligations, You may act only 170 + on Your own behalf and on Your sole responsibility, not on behalf 171 + of any other Contributor, and only if You agree to indemnify, 172 + defend, and hold each Contributor harmless for any liability 173 + incurred by, or claims asserted against, such Contributor by reason 174 + of your accepting any such warranty or additional liability. 175 + 176 + END OF TERMS AND CONDITIONS 177 + 178 + APPENDIX: How to apply the Apache License to your work. 179 + 180 + To apply the Apache License to your work, attach the following 181 + boilerplate notice, with the fields enclosed by brackets "[]" 182 + replaced with your own identifying information. (Don't include 183 + the brackets!) The text should be enclosed in the appropriate 184 + comment syntax for the file format. We also recommend that a 185 + file or class name and description of purpose be included on the 186 + same "printed page" as the copyright notice for easier 187 + identification within third-party archives. 188 + 189 + Copyright [yyyy] [name of copyright owner] 190 + 191 + Licensed under the Apache License, Version 2.0 (the "License"); 192 + you may not use this file except in compliance with the License. 193 + You may obtain a copy of the License at 194 + 195 + http://www.apache.org/licenses/LICENSE-2.0 196 + 197 + Unless required by applicable law or agreed to in writing, software 198 + distributed under the License is distributed on an "AS IS" BASIS, 199 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 + See the License for the specific language governing permissions and 201 + limitations under the License.
+25
crates/raichu/LICENSE_MIT
··· 1 + Copyright (c) 2018 Tsiry Sandratraina <tsiry.sndr@fluentci.io> 2 + 3 + Permission is hereby granted, free of charge, to any 4 + person obtaining a copy of this software and associated 5 + documentation files (the "Software"), to deal in the 6 + Software without restriction, including without 7 + limitation the rights to use, copy, modify, merge, 8 + publish, distribute, sublicense, and/or sell copies of 9 + the Software, and to permit persons to whom the Software 10 + is furnished to do so, subject to the following 11 + conditions: 12 + 13 + The above copyright notice and this permission notice 14 + shall be included in all copies or substantial portions 15 + of the Software. 16 + 17 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 + ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 + TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 + PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 + SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 + IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 + DEALINGS IN THE SOFTWARE.
+84
crates/raichu/README.md
··· 1 + <div align="center"> 2 + 3 + <h1><code>wasm-pack-template</code></h1> 4 + 5 + <strong>A template for kick starting a Rust and WebAssembly project using <a href="https://github.com/rustwasm/wasm-pack">wasm-pack</a>.</strong> 6 + 7 + <p> 8 + <a href="https://travis-ci.org/rustwasm/wasm-pack-template"><img src="https://img.shields.io/travis/rustwasm/wasm-pack-template.svg?style=flat-square" alt="Build Status" /></a> 9 + </p> 10 + 11 + <h3> 12 + <a href="https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html">Tutorial</a> 13 + <span> | </span> 14 + <a href="https://discordapp.com/channels/442252698964721669/443151097398296587">Chat</a> 15 + </h3> 16 + 17 + <sub>Built with 🦀🕸 by <a href="https://rustwasm.github.io/">The Rust and WebAssembly Working Group</a></sub> 18 + </div> 19 + 20 + ## About 21 + 22 + [**📚 Read this template tutorial! 📚**][template-docs] 23 + 24 + This template is designed for compiling Rust libraries into WebAssembly and 25 + publishing the resulting package to NPM. 26 + 27 + Be sure to check out [other `wasm-pack` tutorials online][tutorials] for other 28 + templates and usages of `wasm-pack`. 29 + 30 + [tutorials]: https://rustwasm.github.io/docs/wasm-pack/tutorials/index.html 31 + [template-docs]: https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html 32 + 33 + ## 🚴 Usage 34 + 35 + ### 🐑 Use `cargo generate` to Clone this Template 36 + 37 + [Learn more about `cargo generate` here.](https://github.com/ashleygwilliams/cargo-generate) 38 + 39 + ``` 40 + cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project 41 + cd my-project 42 + ``` 43 + 44 + ### 🛠️ Build with `wasm-pack build` 45 + 46 + ``` 47 + wasm-pack build 48 + ``` 49 + 50 + ### 🔬 Test in Headless Browsers with `wasm-pack test` 51 + 52 + ``` 53 + wasm-pack test --headless --firefox 54 + ``` 55 + 56 + ### 🎁 Publish to NPM with `wasm-pack publish` 57 + 58 + ``` 59 + wasm-pack publish 60 + ``` 61 + 62 + ## 🔋 Batteries Included 63 + 64 + * [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) for communicating 65 + between WebAssembly and JavaScript. 66 + * [`console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook) 67 + for logging panic messages to the developer console. 68 + * `LICENSE-APACHE` and `LICENSE-MIT`: most Rust projects are licensed this way, so these are included for you 69 + 70 + ## License 71 + 72 + Licensed under either of 73 + 74 + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 75 + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 76 + 77 + at your option. 78 + 79 + ### Contribution 80 + 81 + Unless you explicitly state otherwise, any contribution intentionally 82 + submitted for inclusion in the work by you, as defined in the Apache-2.0 83 + license, shall be dual licensed as above, without any additional terms or 84 + conditions.
+1060
crates/raichu/src/lib.rs
··· 1 + use base64::prelude::*; 2 + use rand::seq::SliceRandom; 3 + use rand::thread_rng; 4 + use serde_json::json; 5 + use std::f32::consts::PI; 6 + use std::io::Cursor; 7 + use symphonia::core::audio::SampleBuffer; 8 + use symphonia::core::codecs::{DecoderOptions, CODEC_TYPE_NULL}; 9 + use symphonia::core::formats::FormatOptions; 10 + use symphonia::core::io::{MediaSource, MediaSourceStream}; 11 + use symphonia::core::meta::MetadataOptions; 12 + use symphonia::core::probe::Hint; 13 + use wasm_bindgen::prelude::*; 14 + 15 + #[wasm_bindgen] 16 + pub fn extract_audio_metadata(data: &[u8]) -> JsValue { 17 + let media_source: Box<dyn MediaSource> = Box::new(Cursor::new(data.to_vec())); 18 + let mss = MediaSourceStream::new(media_source, Default::default()); 19 + 20 + let hint = Hint::new(); 21 + 22 + let meta_opts = MetadataOptions::default(); 23 + let format_opts = FormatOptions::default(); 24 + 25 + let mut probed = 26 + match symphonia::default::get_probe().format(&hint, mss, &format_opts, &meta_opts) { 27 + Ok(probed) => probed, 28 + Err(_) => return JsValue::NULL, // Return null if the format is unsupported 29 + }; 30 + 31 + let mut metadata = json!({}); 32 + 33 + // Extract metadata tags 34 + if let Some(track) = probed.format.metadata().current() { 35 + for tag in track.tags() { 36 + if let Some(key) = tag.std_key { 37 + metadata[&format!("{:?}", key)] = serde_json::Value::String(tag.value.to_string()); 38 + } 39 + } 40 + 41 + // Extract album art if available 42 + if let Some(cover) = track 43 + .visuals() 44 + .iter() 45 + .find(|v| v.media_type.starts_with("image/")) 46 + { 47 + let base64_image = BASE64_STANDARD.encode(&cover.data); 48 + let mime_type = &cover.media_type; 49 + metadata["album_art"] = json!({ 50 + "data": base64_image, 51 + "mime": mime_type, 52 + }); 53 + } 54 + } 55 + 56 + if let Some(track) = probed.format.tracks().first() { 57 + if let Some(duration) = track.codec_params.n_frames { 58 + if let Some(sample_rate) = track.codec_params.sample_rate { 59 + let duration_seconds = duration as f64 / sample_rate as f64; 60 + metadata["Duration"] = json!(duration_seconds); 61 + } 62 + } 63 + } 64 + 65 + JsValue::from_str(serde_json::to_string(&metadata).unwrap().as_str()) 66 + } 67 + 68 + #[wasm_bindgen] 69 + pub struct AudioDecoder { 70 + pcm_data: Vec<f32>, 71 + sample_rate: u32, 72 + channels: u16, 73 + } 74 + 75 + #[wasm_bindgen] 76 + impl AudioDecoder { 77 + #[wasm_bindgen(constructor)] 78 + pub fn new() -> Self { 79 + Self { 80 + pcm_data: Vec::new(), 81 + sample_rate: 44100, 82 + channels: 2, 83 + } 84 + } 85 + 86 + #[wasm_bindgen] 87 + pub fn decode(&mut self, audio_data: &[u8], ext: &str) -> Result<(), JsValue> { 88 + let media_source: Box<dyn MediaSource> = Box::new(Cursor::new(audio_data.to_vec())); 89 + 90 + let mss = MediaSourceStream::new(media_source, Default::default()); 91 + 92 + let mut hint = Hint::new(); 93 + hint.with_extension(ext); 94 + 95 + let probed = symphonia::default::get_probe() 96 + .format( 97 + &hint, 98 + mss, 99 + &FormatOptions { 100 + enable_gapless: false, 101 + ..Default::default() 102 + }, 103 + &MetadataOptions::default(), 104 + ) 105 + .map_err(|e| JsValue::from_str(&format!("Failed to read format: {}", e)))?; 106 + 107 + let mut format = probed.format; 108 + let track = format 109 + .default_track() 110 + .ok_or_else(|| JsValue::from_str("No default track found"))?; 111 + let codec_params = &track.codec_params; 112 + 113 + if codec_params.codec == CODEC_TYPE_NULL { 114 + return Err(JsValue::from_str("Unsupported codec")); 115 + } 116 + 117 + let mut decoder = symphonia::default::get_codecs() 118 + .make(&codec_params, &DecoderOptions::default()) 119 + .map_err(|e| JsValue::from_str(&format!("Failed to create decoder: {}", e)))?; 120 + 121 + self.sample_rate = codec_params.sample_rate.unwrap_or(44100); 122 + self.channels = 2; 123 + 124 + while let Ok(packet) = format.next_packet() { 125 + let decoded = decoder 126 + .decode(&packet) 127 + .map_err(|e| JsValue::from_str(&format!("Decode error: {}", e)))?; 128 + let mut sample_buf = 129 + SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec()); 130 + sample_buf.copy_interleaved_ref(decoded); 131 + 132 + self.pcm_data.extend(sample_buf.samples()); 133 + } 134 + 135 + Ok(()) 136 + } 137 + 138 + #[wasm_bindgen] 139 + pub fn get_pcm_data(&self) -> Vec<f32> { 140 + self.pcm_data.clone() 141 + } 142 + 143 + #[wasm_bindgen] 144 + pub fn get_sample_rate(&self) -> u32 { 145 + self.sample_rate 146 + } 147 + 148 + #[wasm_bindgen] 149 + pub fn get_channels(&self) -> u16 { 150 + self.channels 151 + } 152 + } 153 + 154 + #[wasm_bindgen] 155 + pub enum FadeCurve { 156 + Linear, 157 + Exponential, 158 + Logarithmic, 159 + } 160 + 161 + /// Crossfades between two audio buffers using a specified fade curve 162 + #[wasm_bindgen] 163 + pub fn crossfade( 164 + buffer_a: &[f32], 165 + buffer_b: &[f32], 166 + fade_duration: usize, 167 + fade_curve: FadeCurve, 168 + ) -> Vec<f32> { 169 + let len_a = buffer_a.len(); 170 + let len_b = buffer_b.len(); 171 + 172 + let crossfade_len = fade_duration.min(len_a).min(len_b); 173 + let mut output = Vec::with_capacity(len_a + len_b - crossfade_len); 174 + 175 + // Copy the first part of buffer A 176 + output.extend_from_slice(&buffer_a[..len_a - crossfade_len]); 177 + 178 + // Apply the crossfade 179 + for i in 0..crossfade_len { 180 + let t = i as f32 / crossfade_len as f32; 181 + let (fade_out, fade_in) = match fade_curve { 182 + FadeCurve::Linear => (1.0 - t, t), 183 + FadeCurve::Exponential => ((1.0 - t).powi(2), t.powi(2)), 184 + FadeCurve::Logarithmic => (1.0 - t.ln_1p(), t.ln_1p()), 185 + }; 186 + 187 + let mixed_sample = buffer_a[len_a - crossfade_len + i] * fade_out + buffer_b[i] * fade_in; 188 + 189 + output.push(mixed_sample); 190 + } 191 + 192 + // Copy the remaining part of buffer B 193 + output.extend_from_slice(&buffer_b[crossfade_len..]); 194 + 195 + output 196 + } 197 + 198 + #[wasm_bindgen] 199 + pub struct BiquadFilter { 200 + a0: f32, 201 + a1: f32, 202 + a2: f32, 203 + b0: f32, 204 + b1: f32, 205 + b2: f32, 206 + x1: f32, 207 + x2: f32, 208 + y1: f32, 209 + y2: f32, 210 + } 211 + 212 + #[wasm_bindgen] 213 + impl BiquadFilter { 214 + // Create a new peaking EQ filter 215 + #[wasm_bindgen] 216 + pub fn peaking_eq(sample_rate: f32, frequency: f32, q: f32, gain_db: f32) -> Self { 217 + let omega = 2.0 * PI * frequency / sample_rate; 218 + let alpha = omega.sin() / (2.0 * q); 219 + let a = 10.0_f32.powf(gain_db / 40.0); 220 + 221 + let cos_omega = omega.cos(); 222 + 223 + let b0 = 1.0 + alpha * a; 224 + let b1 = -2.0 * cos_omega; 225 + let b2 = 1.0 - alpha * a; 226 + let a0 = 1.0 + alpha / a; 227 + let a1 = -2.0 * cos_omega; 228 + let a2 = 1.0 - alpha / a; 229 + 230 + BiquadFilter { 231 + a0, 232 + a1, 233 + a2, 234 + b0, 235 + b1, 236 + b2, 237 + x1: 0.0, 238 + x2: 0.0, 239 + y1: 0.0, 240 + y2: 0.0, 241 + } 242 + } 243 + 244 + // Process a single sample through the filter 245 + #[wasm_bindgen] 246 + pub fn process(&mut self, input: f32) -> f32 { 247 + let output = (self.b0 / self.a0) * input 248 + + (self.b1 / self.a0) * self.x1 249 + + (self.b2 / self.a0) * self.x2 250 + - (self.a1 / self.a0) * self.y1 251 + - (self.a2 / self.a0) * self.y2; 252 + 253 + // Update filter state 254 + self.x2 = self.x1; 255 + self.x1 = input; 256 + self.y2 = self.y1; 257 + self.y1 = output; 258 + 259 + output 260 + } 261 + 262 + // Update filter parameters 263 + #[wasm_bindgen] 264 + pub fn update_parameters(&mut self, sample_rate: f32, frequency: f32, q: f32, gain_db: f32) { 265 + let omega = 2.0 * PI * frequency / sample_rate; 266 + let alpha = omega.sin() / (2.0 * q); 267 + let a = 10.0_f32.powf(gain_db / 40.0); 268 + 269 + let cos_omega = omega.cos(); 270 + 271 + self.b0 = 1.0 + alpha * a; 272 + self.b1 = -2.0 * cos_omega; 273 + self.b2 = 1.0 - alpha * a; 274 + self.a0 = 1.0 + alpha / a; 275 + self.a1 = -2.0 * cos_omega; 276 + self.a2 = 1.0 - alpha / a; 277 + } 278 + } 279 + 280 + // 12-band equalizer 281 + #[wasm_bindgen] 282 + pub struct Equalizer { 283 + bands: Vec<BiquadFilter>, 284 + sample_rate: f32, 285 + } 286 + 287 + #[wasm_bindgen] 288 + impl Equalizer { 289 + // Create a new 12-band equalizer with standard frequency bands 290 + #[wasm_bindgen(constructor)] 291 + pub fn new(sample_rate: f32) -> Self { 292 + // Standard frequency bands for a 12-band EQ (in Hz) 293 + let frequencies = [ 294 + 31.0, 62.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 12000.0, 16000.0, 295 + 20000.0, 296 + ]; 297 + 298 + let mut bands = Vec::with_capacity(frequencies.len()); 299 + 300 + // Create a filter for each frequency band with default gain of 0 dB 301 + for &freq in &frequencies { 302 + bands.push(BiquadFilter::peaking_eq(sample_rate, freq, 1.414, 0.0)); 303 + } 304 + 305 + Equalizer { bands, sample_rate } 306 + } 307 + 308 + // Set gain for a specific band 309 + #[wasm_bindgen] 310 + pub fn set_band_gain(&mut self, band_index: usize, gain_db: f32) { 311 + if band_index >= self.bands.len() { 312 + return; 313 + } 314 + 315 + // Standard frequency bands 316 + let frequencies = [ 317 + 31.0, 62.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 12000.0, 16000.0, 318 + 20000.0, 319 + ]; 320 + 321 + // Update the filter parameters with the new gain 322 + let frequency = frequencies[band_index]; 323 + let q = 1.414; // Standard Q value for EQ bands 324 + self.bands[band_index].update_parameters(self.sample_rate, frequency, q, gain_db); 325 + } 326 + 327 + // Process a single sample through all EQ bands 328 + #[wasm_bindgen] 329 + pub fn process(&mut self, input: f32) -> f32 { 330 + let mut output = input; 331 + 332 + // Pass the input through each band filter in series 333 + for band in &mut self.bands { 334 + output = band.process(output); 335 + } 336 + 337 + output 338 + } 339 + 340 + // Process a buffer of samples 341 + #[wasm_bindgen] 342 + pub fn process_buffer(&mut self, input_buffer: &[f32], output_buffer: &mut [f32]) { 343 + assert_eq!(input_buffer.len(), output_buffer.len()); 344 + 345 + for i in 0..input_buffer.len() { 346 + output_buffer[i] = self.process(input_buffer[i]); 347 + } 348 + } 349 + } 350 + 351 + #[wasm_bindgen] 352 + pub struct AudioFilter { 353 + sample_rate: f32, 354 + // Common filter state variables 355 + x1: f32, 356 + x2: f32, 357 + y1: f32, 358 + y2: f32, 359 + // Additional states for higher order filters 360 + x3: f32, 361 + x4: f32, 362 + y3: f32, 363 + y4: f32, 364 + } 365 + 366 + #[wasm_bindgen] 367 + impl AudioFilter { 368 + #[wasm_bindgen(constructor)] 369 + pub fn new(sample_rate: f32) -> Self { 370 + Self { 371 + sample_rate, 372 + x1: 0.0, 373 + x2: 0.0, 374 + y1: 0.0, 375 + y2: 0.0, 376 + x3: 0.0, 377 + x4: 0.0, 378 + y3: 0.0, 379 + y4: 0.0, 380 + } 381 + } 382 + 383 + #[wasm_bindgen] 384 + pub fn reset(&mut self) { 385 + self.x1 = 0.0; 386 + self.x2 = 0.0; 387 + self.y1 = 0.0; 388 + self.y2 = 0.0; 389 + self.x3 = 0.0; 390 + self.x4 = 0.0; 391 + self.y3 = 0.0; 392 + self.y4 = 0.0; 393 + } 394 + } 395 + 396 + #[wasm_bindgen] 397 + pub struct LowShelfFilter { 398 + filter: AudioFilter, 399 + a1: f32, 400 + a2: f32, 401 + b0: f32, 402 + b1: f32, 403 + b2: f32, 404 + } 405 + 406 + #[wasm_bindgen] 407 + impl LowShelfFilter { 408 + #[wasm_bindgen(constructor)] 409 + pub fn new(sample_rate: f32, frequency: f32, gain_db: f32, q: f32) -> Self { 410 + let mut filter = Self { 411 + filter: AudioFilter::new(sample_rate), 412 + a1: 0.0, 413 + a2: 0.0, 414 + b0: 0.0, 415 + b1: 0.0, 416 + b2: 0.0, 417 + }; 418 + filter.set_parameters(frequency, gain_db, q); 419 + filter 420 + } 421 + 422 + #[wasm_bindgen] 423 + pub fn set_parameters(&mut self, frequency: f32, gain_db: f32, q: f32) { 424 + let a = 10.0_f32.powf(gain_db / 40.0); 425 + let omega = 2.0 * std::f32::consts::PI * frequency / self.filter.sample_rate; 426 + let sin_omega = omega.sin(); 427 + let cos_omega = omega.cos(); 428 + let alpha = sin_omega / (2.0 * q); 429 + let beta = (a + 1.0 / a).sqrt() * 2.0 * alpha; 430 + 431 + let b0 = a * ((a + 1.0) - (a - 1.0) * cos_omega + beta); 432 + let b1 = 2.0 * a * ((a - 1.0) - (a + 1.0) * cos_omega); 433 + let b2 = a * ((a + 1.0) - (a - 1.0) * cos_omega - beta); 434 + let a0 = (a + 1.0) + (a - 1.0) * cos_omega + beta; 435 + let a1 = -2.0 * ((a - 1.0) + (a + 1.0) * cos_omega); 436 + let a2 = (a + 1.0) + (a - 1.0) * cos_omega - beta; 437 + 438 + self.b0 = b0 / a0; 439 + self.b1 = b1 / a0; 440 + self.b2 = b2 / a0; 441 + self.a1 = a1 / a0; 442 + self.a2 = a2 / a0; 443 + } 444 + 445 + #[wasm_bindgen] 446 + pub fn process(&mut self, input: f32) -> f32 { 447 + let output = self.b0 * input + self.b1 * self.filter.x1 + self.b2 * self.filter.x2 448 + - self.a1 * self.filter.y1 449 + - self.a2 * self.filter.y2; 450 + 451 + self.filter.x2 = self.filter.x1; 452 + self.filter.x1 = input; 453 + self.filter.y2 = self.filter.y1; 454 + self.filter.y1 = output; 455 + 456 + output 457 + } 458 + 459 + #[wasm_bindgen] 460 + pub fn reset(&mut self) { 461 + self.filter.reset(); 462 + } 463 + } 464 + 465 + #[wasm_bindgen] 466 + pub struct HighShelfFilter { 467 + filter: AudioFilter, 468 + a1: f32, 469 + a2: f32, 470 + b0: f32, 471 + b1: f32, 472 + b2: f32, 473 + } 474 + 475 + #[wasm_bindgen] 476 + impl HighShelfFilter { 477 + #[wasm_bindgen(constructor)] 478 + pub fn new(sample_rate: f32, frequency: f32, gain_db: f32, q: f32) -> Self { 479 + let mut filter = Self { 480 + filter: AudioFilter::new(sample_rate), 481 + a1: 0.0, 482 + a2: 0.0, 483 + b0: 0.0, 484 + b1: 0.0, 485 + b2: 0.0, 486 + }; 487 + filter.set_parameters(frequency, gain_db, q); 488 + filter 489 + } 490 + 491 + #[wasm_bindgen] 492 + pub fn set_parameters(&mut self, frequency: f32, gain_db: f32, q: f32) { 493 + let a = 10.0_f32.powf(gain_db / 40.0); 494 + let omega = 2.0 * std::f32::consts::PI * frequency / self.filter.sample_rate; 495 + let sin_omega = omega.sin(); 496 + let cos_omega = omega.cos(); 497 + let alpha = sin_omega / (2.0 * q); 498 + let beta = (a + 1.0 / a).sqrt() * 2.0 * alpha; 499 + 500 + let b0 = a * ((a + 1.0) + (a - 1.0) * cos_omega + beta); 501 + let b1 = -2.0 * a * ((a - 1.0) + (a + 1.0) * cos_omega); 502 + let b2 = a * ((a + 1.0) + (a - 1.0) * cos_omega - beta); 503 + let a0 = (a + 1.0) - (a - 1.0) * cos_omega + beta; 504 + let a1 = 2.0 * ((a - 1.0) - (a + 1.0) * cos_omega); 505 + let a2 = (a + 1.0) - (a - 1.0) * cos_omega - beta; 506 + 507 + self.b0 = b0 / a0; 508 + self.b1 = b1 / a0; 509 + self.b2 = b2 / a0; 510 + self.a1 = a1 / a0; 511 + self.a2 = a2 / a0; 512 + } 513 + 514 + #[wasm_bindgen] 515 + pub fn process(&mut self, input: f32) -> f32 { 516 + let output = self.b0 * input + self.b1 * self.filter.x1 + self.b2 * self.filter.x2 517 + - self.a1 * self.filter.y1 518 + - self.a2 * self.filter.y2; 519 + 520 + self.filter.x2 = self.filter.x1; 521 + self.filter.x1 = input; 522 + self.filter.y2 = self.filter.y1; 523 + self.filter.y1 = output; 524 + 525 + output 526 + } 527 + 528 + #[wasm_bindgen] 529 + pub fn reset(&mut self) { 530 + self.filter.reset(); 531 + } 532 + } 533 + 534 + #[wasm_bindgen] 535 + pub struct BandPassFilter { 536 + filter: AudioFilter, 537 + a1: f32, 538 + a2: f32, 539 + b0: f32, 540 + b1: f32, 541 + b2: f32, 542 + } 543 + 544 + #[wasm_bindgen] 545 + impl BandPassFilter { 546 + #[wasm_bindgen(constructor)] 547 + pub fn new(sample_rate: f32, frequency: f32, q: f32) -> Self { 548 + let mut filter = Self { 549 + filter: AudioFilter::new(sample_rate), 550 + a1: 0.0, 551 + a2: 0.0, 552 + b0: 0.0, 553 + b1: 0.0, 554 + b2: 0.0, 555 + }; 556 + filter.set_parameters(frequency, q); 557 + filter 558 + } 559 + 560 + #[wasm_bindgen] 561 + pub fn set_parameters(&mut self, frequency: f32, q: f32) { 562 + let omega = 2.0 * std::f32::consts::PI * frequency / self.filter.sample_rate; 563 + let sin_omega = omega.sin(); 564 + let cos_omega = omega.cos(); 565 + let alpha = sin_omega / (2.0 * q); 566 + 567 + let b0 = alpha; 568 + let b1 = 0.0; 569 + let b2 = -alpha; 570 + let a0 = 1.0 + alpha; 571 + let a1 = -2.0 * cos_omega; 572 + let a2 = 1.0 - alpha; 573 + 574 + self.b0 = b0 / a0; 575 + self.b1 = b1 / a0; 576 + self.b2 = b2 / a0; 577 + self.a1 = a1 / a0; 578 + self.a2 = a2 / a0; 579 + } 580 + 581 + #[wasm_bindgen] 582 + pub fn process(&mut self, input: f32) -> f32 { 583 + let output = self.b0 * input + self.b1 * self.filter.x1 + self.b2 * self.filter.x2 584 + - self.a1 * self.filter.y1 585 + - self.a2 * self.filter.y2; 586 + 587 + self.filter.x2 = self.filter.x1; 588 + self.filter.x1 = input; 589 + self.filter.y2 = self.filter.y1; 590 + self.filter.y1 = output; 591 + 592 + output 593 + } 594 + 595 + #[wasm_bindgen] 596 + pub fn reset(&mut self) { 597 + self.filter.reset(); 598 + } 599 + } 600 + 601 + #[wasm_bindgen] 602 + pub struct BesselFilter { 603 + filter: AudioFilter, 604 + order: usize, 605 + // Coefficients for sections 606 + a: Vec<Vec<f32>>, 607 + b: Vec<Vec<f32>>, 608 + } 609 + 610 + #[wasm_bindgen] 611 + impl BesselFilter { 612 + #[wasm_bindgen(constructor)] 613 + pub fn new(sample_rate: f32, cutoff_frequency: f32, order: usize) -> Self { 614 + if order != 4 && order != 8 { 615 + panic!("Only 4th and 8th order Bessel filters are implemented"); 616 + } 617 + 618 + let mut filter = Self { 619 + filter: AudioFilter::new(sample_rate), 620 + order, 621 + a: vec![], 622 + b: vec![], 623 + }; 624 + filter.set_parameters(cutoff_frequency); 625 + filter 626 + } 627 + 628 + #[wasm_bindgen] 629 + pub fn set_parameters(&mut self, cutoff_frequency: f32) { 630 + let fs = self.filter.sample_rate; 631 + let fc = cutoff_frequency; 632 + 633 + // Clear previous coefficients 634 + self.a = vec![]; 635 + self.b = vec![]; 636 + 637 + // Normalized frequency 638 + let omega_c = 2.0 * std::f32::consts::PI * fc / fs; 639 + 640 + // Bessel polynomials for 4th and 8th order 641 + // These are the poles of the normalized (omega_c = 1) Bessel filter 642 + let poles = if self.order == 4 { 643 + vec![ 644 + (-0.6572111112819416, 0.8301614350048806), 645 + (-0.6572111112819416, -0.8301614350048806), 646 + (-0.9047587967882449, 0.2709187330038746), 647 + (-0.9047587967882449, -0.2709187330038746), 648 + ] 649 + } else { 650 + // 8th order 651 + vec![ 652 + (-0.5905759446119192, 0.9072067564574548), 653 + (-0.5905759446119192, -0.9072067564574548), 654 + (-0.6707106781186548, 0.7937387988730166), 655 + (-0.6707106781186548, -0.7937387988730166), 656 + (-0.7996541858328288, 0.6000376420593046), 657 + (-0.7996541858328288, -0.6000376420593046), 658 + (-0.8717401485096066, 0.3349881501782813), 659 + (-0.8717401485096066, -0.3349881501782813), 660 + ] 661 + }; 662 + 663 + // Create second-order sections from complex conjugate pairs 664 + for section in poles.chunks(2) { 665 + let (real, imag) = section[0]; 666 + 667 + // Bilinear transform to map s-plane to z-plane 668 + let c = 1.0 / (omega_c * 2.0); 669 + let d = 1.0 + real * c; 670 + let e = real * real + imag * imag; 671 + 672 + let b0 = 1.0 / d; 673 + let b1 = 2.0 / d; 674 + let b2 = 1.0 / d; 675 + 676 + let a0 = 1.0; 677 + let a1 = 2.0 * (1.0 - e * c * c) / d; 678 + let a2 = (1.0 - 2.0 * real * c + e * c * c) / d; 679 + 680 + self.b.push(vec![b0, b1, b2]); 681 + self.a.push(vec![a0, a1, a2]); 682 + } 683 + } 684 + 685 + #[wasm_bindgen] 686 + pub fn process(&mut self, input: f32) -> f32 { 687 + let num_sections = self.order / 2; 688 + let mut output = input; 689 + 690 + for i in 0..num_sections { 691 + let x = output; 692 + 693 + output = 694 + self.b[i][0] * x + self.b[i][1] * self.filter.x1 + self.b[i][2] * self.filter.x2 695 + - self.a[i][1] * self.filter.y1 696 + - self.a[i][2] * self.filter.y2; 697 + 698 + self.filter.x2 = self.filter.x1; 699 + self.filter.x1 = x; 700 + self.filter.y2 = self.filter.y1; 701 + self.filter.y1 = output; 702 + } 703 + 704 + output 705 + } 706 + 707 + #[wasm_bindgen] 708 + pub fn reset(&mut self) { 709 + self.filter.reset(); 710 + } 711 + } 712 + 713 + #[wasm_bindgen] 714 + pub struct LinkwitzRileyFilter { 715 + filter: AudioFilter, 716 + // For 8th order (48 dB/Oct) we need 4 biquad sections 717 + a: Vec<Vec<f32>>, 718 + b: Vec<Vec<f32>>, 719 + state: Vec<Vec<f32>>, // x1, x2, y1, y2 for each section 720 + } 721 + 722 + #[wasm_bindgen] 723 + impl LinkwitzRileyFilter { 724 + #[wasm_bindgen(constructor)] 725 + pub fn new(sample_rate: f32, cutoff_frequency: f32, filter_type: FilterType) -> Self { 726 + let mut filter = Self { 727 + filter: AudioFilter::new(sample_rate), 728 + a: vec![vec![0.0; 3]; 4], // 4 biquad sections, each with 3 coefficients 729 + b: vec![vec![0.0; 3]; 4], 730 + state: vec![vec![0.0; 4]; 4], // 4 states per section 731 + }; 732 + filter.set_parameters(cutoff_frequency, filter_type); 733 + filter 734 + } 735 + 736 + #[wasm_bindgen] 737 + pub fn set_parameters(&mut self, cutoff_frequency: f32, filter_type: FilterType) { 738 + let omega_c = 2.0 * std::f32::consts::PI * cutoff_frequency / self.filter.sample_rate; 739 + let k = omega_c.tan(); 740 + 741 + // Butterworth poles for 8th order (48 dB/Oct) 742 + let poles = [ 743 + (-0.9808, 0.1951), 744 + (-0.9808, -0.1951), 745 + (-0.8315, 0.5556), 746 + (-0.8315, -0.5556), 747 + (-0.5556, 0.8315), 748 + (-0.5556, -0.8315), 749 + (-0.1951, 0.9808), 750 + (-0.1951, -0.9808), 751 + ]; 752 + 753 + // Process each section (4 sections for 8th order) 754 + for i in 0..4 { 755 + let (real, imag) = poles[i * 2]; 756 + 757 + // Bilinear transform 758 + let d = 1.0 + real * k + (real * real + imag * imag) * k * k; 759 + 760 + match filter_type { 761 + FilterType::LowPass => { 762 + let k_squared = k * k; 763 + self.b[i][0] = k_squared / d; 764 + self.b[i][1] = 2.0 * k_squared / d; 765 + self.b[i][2] = k_squared / d; 766 + self.a[i][0] = 1.0; 767 + self.a[i][1] = 2.0 * (k_squared - 1.0) / d; 768 + self.a[i][2] = (1.0 - real * k + (real * real + imag * imag) * k * k) / d; 769 + } 770 + FilterType::HighPass => { 771 + self.b[i][0] = 1.0 / d; 772 + self.b[i][1] = -2.0 / d; 773 + self.b[i][2] = 1.0 / d; 774 + self.a[i][0] = 1.0; 775 + self.a[i][1] = 2.0 * (k * k - 1.0) / d; 776 + self.a[i][2] = (1.0 - real * k + (real * real + imag * imag) * k * k) / d; 777 + } 778 + } 779 + } 780 + } 781 + 782 + #[wasm_bindgen] 783 + pub fn process(&mut self, input: f32) -> f32 { 784 + let mut output = input; 785 + 786 + for i in 0..4 { 787 + let x = output; 788 + 789 + // Apply the biquad filter 790 + output = self.b[i][0] * x 791 + + self.b[i][1] * self.state[i][0] 792 + + self.b[i][2] * self.state[i][1] 793 + - self.a[i][1] * self.state[i][2] 794 + - self.a[i][2] * self.state[i][3]; 795 + 796 + // Update states 797 + self.state[i][1] = self.state[i][0]; 798 + self.state[i][0] = x; 799 + self.state[i][3] = self.state[i][2]; 800 + self.state[i][2] = output; 801 + } 802 + 803 + output 804 + } 805 + 806 + #[wasm_bindgen] 807 + pub fn reset(&mut self) { 808 + for i in 0..4 { 809 + for j in 0..4 { 810 + self.state[i][j] = 0.0; 811 + } 812 + } 813 + } 814 + } 815 + 816 + #[wasm_bindgen] 817 + pub enum FilterType { 818 + LowPass, 819 + HighPass, 820 + } 821 + 822 + #[wasm_bindgen] 823 + pub struct Playlist { 824 + tracks: Vec<String>, 825 + current_track: usize, 826 + } 827 + 828 + #[wasm_bindgen] 829 + impl Playlist { 830 + #[wasm_bindgen(constructor)] 831 + pub fn new() -> Self { 832 + Self { 833 + tracks: Vec::new(), 834 + current_track: 0, 835 + } 836 + } 837 + 838 + #[wasm_bindgen] 839 + pub fn add_track(&mut self, track: &str) -> usize { 840 + self.tracks.push(track.to_string()); 841 + self.tracks.len() 842 + } 843 + 844 + #[wasm_bindgen] 845 + pub fn next_track(&mut self) -> String { 846 + if self.tracks.is_empty() { 847 + return String::new(); 848 + } 849 + 850 + self.current_track = (self.current_track + 1) % self.tracks.len(); 851 + self.tracks[self.current_track].clone() 852 + } 853 + 854 + #[wasm_bindgen] 855 + pub fn previous_track(&mut self) -> String { 856 + if self.tracks.is_empty() { 857 + return String::new(); 858 + } 859 + 860 + if self.current_track == 0 { 861 + self.current_track = self.tracks.len() - 1; 862 + } else { 863 + self.current_track -= 1; 864 + } 865 + 866 + self.tracks[self.current_track].clone() 867 + } 868 + 869 + #[wasm_bindgen] 870 + pub fn current_track(&self) -> String { 871 + if self.tracks.is_empty() { 872 + return String::new(); 873 + } 874 + 875 + self.tracks[self.current_track].clone() 876 + } 877 + 878 + #[wasm_bindgen] 879 + pub fn clear(&mut self) { 880 + self.tracks.clear(); 881 + self.current_track = 0; 882 + } 883 + 884 + #[wasm_bindgen] 885 + pub fn size(&self) -> usize { 886 + self.tracks.len() 887 + } 888 + 889 + #[wasm_bindgen] 890 + pub fn current_index(&self) -> usize { 891 + self.current_track 892 + } 893 + 894 + #[wasm_bindgen] 895 + pub fn set_current_index(&mut self, index: usize) { 896 + if index < self.tracks.len() { 897 + self.current_track = index; 898 + } 899 + } 900 + 901 + #[wasm_bindgen] 902 + pub fn remove_track(&mut self, index: usize) { 903 + if index < self.tracks.len() { 904 + self.tracks.remove(index); 905 + } 906 + } 907 + 908 + #[wasm_bindgen] 909 + pub fn get_track(&self, index: usize) -> String { 910 + if index < self.tracks.len() { 911 + self.tracks[index].clone() 912 + } else { 913 + String::new() 914 + } 915 + } 916 + 917 + #[wasm_bindgen] 918 + 919 + pub fn get_tracks(&self) -> js_sys::Array { 920 + self.tracks.iter().map(|t| JsValue::from_str(t)).collect() 921 + } 922 + 923 + #[wasm_bindgen] 924 + pub fn shuffle(&mut self) { 925 + let mut rng = thread_rng(); 926 + self.tracks.shuffle(&mut rng); 927 + } 928 + 929 + #[wasm_bindgen] 930 + pub fn insert_track(&mut self, index: usize, track: &str) { 931 + if index <= self.tracks.len() { 932 + self.tracks.insert(index, track.to_string()); 933 + } 934 + } 935 + 936 + #[wasm_bindgen] 937 + pub fn get_next_track(&self) -> String { 938 + if self.tracks.is_empty() { 939 + return String::new(); 940 + } 941 + 942 + let next_track = (self.current_track + 1) % self.tracks.len(); 943 + self.tracks[next_track].clone() 944 + } 945 + 946 + #[wasm_bindgen] 947 + pub fn get_next_tracks(&self) -> js_sys::Array { 948 + if self.tracks.is_empty() { 949 + return js_sys::Array::new(); 950 + } 951 + 952 + let current_track = self.current_track; 953 + let next_tracks: Vec<String> = self 954 + .tracks 955 + .iter() 956 + .skip(current_track + 1) 957 + .cloned() 958 + .collect(); 959 + next_tracks.iter().map(|t| JsValue::from_str(t)).collect() 960 + } 961 + } 962 + 963 + #[cfg(test)] 964 + mod tests { 965 + use super::*; 966 + 967 + #[test] 968 + fn test_playlist_add_and_get_tracks() { 969 + let mut playlist = Playlist::new(); 970 + playlist.add_track("Track 1"); 971 + playlist.add_track("Track 2"); 972 + playlist.add_track("Track 3"); 973 + 974 + assert_eq!(playlist.size(), 3); 975 + assert_eq!(playlist.get_track(0), "Track 1"); 976 + assert_eq!(playlist.get_track(1), "Track 2"); 977 + assert_eq!(playlist.get_track(2), "Track 3"); 978 + } 979 + 980 + #[test] 981 + fn test_playlist_navigation() { 982 + let mut playlist = Playlist::new(); 983 + playlist.add_track("Track 1"); 984 + playlist.add_track("Track 2"); 985 + playlist.add_track("Track 3"); 986 + 987 + assert_eq!(playlist.current_track(), "Track 1"); 988 + assert_eq!(playlist.next_track(), "Track 2"); 989 + assert_eq!(playlist.next_track(), "Track 3"); 990 + assert_eq!(playlist.next_track(), "Track 1"); 991 + 992 + assert_eq!(playlist.previous_track(), "Track 3"); 993 + assert_eq!(playlist.previous_track(), "Track 2"); 994 + assert_eq!(playlist.previous_track(), "Track 1"); 995 + } 996 + 997 + #[test] 998 + fn test_playlist_clear() { 999 + let mut playlist = Playlist::new(); 1000 + playlist.add_track("Track 1"); 1001 + playlist.add_track("Track 2"); 1002 + 1003 + assert_eq!(playlist.size(), 2); 1004 + playlist.clear(); 1005 + assert_eq!(playlist.size(), 0); 1006 + assert_eq!(playlist.current_track(), ""); 1007 + } 1008 + 1009 + #[test] 1010 + fn test_playlist_remove_track() { 1011 + let mut playlist = Playlist::new(); 1012 + playlist.add_track("Track 1"); 1013 + playlist.add_track("Track 2"); 1014 + playlist.add_track("Track 3"); 1015 + 1016 + playlist.remove_track(1); 1017 + assert_eq!(playlist.size(), 2); 1018 + assert_eq!(playlist.get_track(0), "Track 1"); 1019 + assert_eq!(playlist.get_track(1), "Track 3"); 1020 + } 1021 + 1022 + #[test] 1023 + fn test_playlist_insert_track() { 1024 + let mut playlist = Playlist::new(); 1025 + playlist.add_track("Track 1"); 1026 + playlist.add_track("Track 3"); 1027 + 1028 + playlist.insert_track(1, "Track 2"); 1029 + assert_eq!(playlist.size(), 3); 1030 + assert_eq!(playlist.get_track(0), "Track 1"); 1031 + assert_eq!(playlist.get_track(1), "Track 2"); 1032 + assert_eq!(playlist.get_track(2), "Track 3"); 1033 + } 1034 + 1035 + #[test] 1036 + fn test_playlist_set_current_index() { 1037 + let mut playlist = Playlist::new(); 1038 + playlist.add_track("Track 1"); 1039 + playlist.add_track("Track 2"); 1040 + playlist.add_track("Track 3"); 1041 + 1042 + playlist.set_current_index(2); 1043 + assert_eq!(playlist.current_track(), "Track 3"); 1044 + 1045 + playlist.set_current_index(0); 1046 + assert_eq!(playlist.current_track(), "Track 1"); 1047 + } 1048 + 1049 + #[test] 1050 + fn test_playlist_get_next_track() { 1051 + let mut playlist = Playlist::new(); 1052 + playlist.add_track("Track 1"); 1053 + playlist.add_track("Track 2"); 1054 + playlist.add_track("Track 3"); 1055 + 1056 + assert_eq!(playlist.get_next_track(), "Track 2"); 1057 + playlist.next_track(); 1058 + assert_eq!(playlist.get_next_track(), "Track 3"); 1059 + } 1060 + }
+2 -1
package.json
··· 23 23 "dev:scrobbler": "cargo run -p scrobbler --release -- serve", 24 24 "dev:spotify": "cargo run -p spotify --release -- serve", 25 25 "dev:storage": "cargo run -p storage --release -- serve", 26 - "dev:webscrobbler": "cargo run -p webscrobbler --release -- serve" 26 + "dev:webscrobbler": "cargo run -p webscrobbler --release -- serve", 27 + "build:raichu": "cd crates/raichu && wasm-pack build --release --target web && cp -r pkg ../../apps/web/src" 27 28 }, 28 29 "devDependencies": { 29 30 "biome": "^0.3.3",