···11+ Apache License
22+ Version 2.0, January 2004
33+ http://www.apache.org/licenses/
44+55+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
66+77+1. Definitions.
88+99+ "License" shall mean the terms and conditions for use, reproduction,
1010+ and distribution as defined by Sections 1 through 9 of this document.
1111+1212+ "Licensor" shall mean the copyright owner or entity authorized by
1313+ the copyright owner that is granting the License.
1414+1515+ "Legal Entity" shall mean the union of the acting entity and all
1616+ other entities that control, are controlled by, or are under common
1717+ control with that entity. For the purposes of this definition,
1818+ "control" means (i) the power, direct or indirect, to cause the
1919+ direction or management of such entity, whether by contract or
2020+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
2121+ outstanding shares, or (iii) beneficial ownership of such entity.
2222+2323+ "You" (or "Your") shall mean an individual or Legal Entity
2424+ exercising permissions granted by this License.
2525+2626+ "Source" form shall mean the preferred form for making modifications,
2727+ including but not limited to software source code, documentation
2828+ source, and configuration files.
2929+3030+ "Object" form shall mean any form resulting from mechanical
3131+ transformation or translation of a Source form, including but
3232+ not limited to compiled object code, generated documentation,
3333+ and conversions to other media types.
3434+3535+ "Work" shall mean the work of authorship, whether in Source or
3636+ Object form, made available under the License, as indicated by a
3737+ copyright notice that is included in or attached to the work
3838+ (an example is provided in the Appendix below).
3939+4040+ "Derivative Works" shall mean any work, whether in Source or Object
4141+ form, that is based on (or derived from) the Work and for which the
4242+ editorial revisions, annotations, elaborations, or other modifications
4343+ represent, as a whole, an original work of authorship. For the purposes
4444+ of this License, Derivative Works shall not include works that remain
4545+ separable from, or merely link (or bind by name) to the interfaces of,
4646+ the Work and Derivative Works thereof.
4747+4848+ "Contribution" shall mean any work of authorship, including
4949+ the original version of the Work and any modifications or additions
5050+ to that Work or Derivative Works thereof, that is intentionally
5151+ submitted to Licensor for inclusion in the Work by the copyright owner
5252+ or by an individual or Legal Entity authorized to submit on behalf of
5353+ the copyright owner. For the purposes of this definition, "submitted"
5454+ means any form of electronic, verbal, or written communication sent
5555+ to the Licensor or its representatives, including but not limited to
5656+ communication on electronic mailing lists, source code control systems,
5757+ and issue tracking systems that are managed by, or on behalf of, the
5858+ Licensor for the purpose of discussing and improving the Work, but
5959+ excluding communication that is conspicuously marked or otherwise
6060+ designated in writing by the copyright owner as "Not a Contribution."
6161+6262+ "Contributor" shall mean Licensor and any individual or Legal Entity
6363+ on behalf of whom a Contribution has been received by Licensor and
6464+ subsequently incorporated within the Work.
6565+6666+2. Grant of Copyright License. Subject to the terms and conditions of
6767+ this License, each Contributor hereby grants to You a perpetual,
6868+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
6969+ copyright license to reproduce, prepare Derivative Works of,
7070+ publicly display, publicly perform, sublicense, and distribute the
7171+ Work and such Derivative Works in Source or Object form.
7272+7373+3. Grant of Patent License. Subject to the terms and conditions of
7474+ this License, each Contributor hereby grants to You a perpetual,
7575+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
7676+ (except as stated in this section) patent license to make, have made,
7777+ use, offer to sell, sell, import, and otherwise transfer the Work,
7878+ where such license applies only to those patent claims licensable
7979+ by such Contributor that are necessarily infringed by their
8080+ Contribution(s) alone or by combination of their Contribution(s)
8181+ with the Work to which such Contribution(s) was submitted. If You
8282+ institute patent litigation against any entity (including a
8383+ cross-claim or counterclaim in a lawsuit) alleging that the Work
8484+ or a Contribution incorporated within the Work constitutes direct
8585+ or contributory patent infringement, then any patent licenses
8686+ granted to You under this License for that Work shall terminate
8787+ as of the date such litigation is filed.
8888+8989+4. Redistribution. You may reproduce and distribute copies of the
9090+ Work or Derivative Works thereof in any medium, with or without
9191+ modifications, and in Source or Object form, provided that You
9292+ meet the following conditions:
9393+9494+ (a) You must give any other recipients of the Work or
9595+ Derivative Works a copy of this License; and
9696+9797+ (b) You must cause any modified files to carry prominent notices
9898+ stating that You changed the files; and
9999+100100+ (c) You must retain, in the Source form of any Derivative Works
101101+ that You distribute, all copyright, patent, trademark, and
102102+ attribution notices from the Source form of the Work,
103103+ excluding those notices that do not pertain to any part of
104104+ the Derivative Works; and
105105+106106+ (d) If the Work includes a "NOTICE" text file as part of its
107107+ distribution, then any Derivative Works that You distribute must
108108+ include a readable copy of the attribution notices contained
109109+ within such NOTICE file, excluding those notices that do not
110110+ pertain to any part of the Derivative Works, in at least one
111111+ of the following places: within a NOTICE text file distributed
112112+ as part of the Derivative Works; within the Source form or
113113+ documentation, if provided along with the Derivative Works; or,
114114+ within a display generated by the Derivative Works, if and
115115+ wherever such third-party notices normally appear. The contents
116116+ of the NOTICE file are for informational purposes only and
117117+ do not modify the License. You may add Your own attribution
118118+ notices within Derivative Works that You distribute, alongside
119119+ or as an addendum to the NOTICE text from the Work, provided
120120+ that such additional attribution notices cannot be construed
121121+ as modifying the License.
122122+123123+ You may add Your own copyright statement to Your modifications and
124124+ may provide additional or different license terms and conditions
125125+ for use, reproduction, or distribution of Your modifications, or
126126+ for any such Derivative Works as a whole, provided Your use,
127127+ reproduction, and distribution of the Work otherwise complies with
128128+ the conditions stated in this License.
129129+130130+5. Submission of Contributions. Unless You explicitly state otherwise,
131131+ any Contribution intentionally submitted for inclusion in the Work
132132+ by You to the Licensor shall be under the terms and conditions of
133133+ this License, without any additional terms or conditions.
134134+ Notwithstanding the above, nothing herein shall supersede or modify
135135+ the terms of any separate license agreement you may have executed
136136+ with Licensor regarding such Contributions.
137137+138138+6. Trademarks. This License does not grant permission to use the trade
139139+ names, trademarks, service marks, or product names of the Licensor,
140140+ except as required for reasonable and customary use in describing the
141141+ origin of the Work and reproducing the content of the NOTICE file.
142142+143143+7. Disclaimer of Warranty. Unless required by applicable law or
144144+ agreed to in writing, Licensor provides the Work (and each
145145+ Contributor provides its Contributions) on an "AS IS" BASIS,
146146+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147147+ implied, including, without limitation, any warranties or conditions
148148+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149149+ PARTICULAR PURPOSE. You are solely responsible for determining the
150150+ appropriateness of using or redistributing the Work and assume any
151151+ risks associated with Your exercise of permissions under this License.
152152+153153+8. Limitation of Liability. In no event and under no legal theory,
154154+ whether in tort (including negligence), contract, or otherwise,
155155+ unless required by applicable law (such as deliberate and grossly
156156+ negligent acts) or agreed to in writing, shall any Contributor be
157157+ liable to You for damages, including any direct, indirect, special,
158158+ incidental, or consequential damages of any character arising as a
159159+ result of this License or out of the use or inability to use the
160160+ Work (including but not limited to damages for loss of goodwill,
161161+ work stoppage, computer failure or malfunction, or any and all
162162+ other commercial damages or losses), even if such Contributor
163163+ has been advised of the possibility of such damages.
164164+165165+9. Accepting Warranty or Additional Liability. While redistributing
166166+ the Work or Derivative Works thereof, You may choose to offer,
167167+ and charge a fee for, acceptance of support, warranty, indemnity,
168168+ or other liability obligations and/or rights consistent with this
169169+ License. However, in accepting such obligations, You may act only
170170+ on Your own behalf and on Your sole responsibility, not on behalf
171171+ of any other Contributor, and only if You agree to indemnify,
172172+ defend, and hold each Contributor harmless for any liability
173173+ incurred by, or claims asserted against, such Contributor by reason
174174+ of your accepting any such warranty or additional liability.
175175+176176+END OF TERMS AND CONDITIONS
177177+178178+APPENDIX: How to apply the Apache License to your work.
179179+180180+ To apply the Apache License to your work, attach the following
181181+ boilerplate notice, with the fields enclosed by brackets "[]"
182182+ replaced with your own identifying information. (Don't include
183183+ the brackets!) The text should be enclosed in the appropriate
184184+ comment syntax for the file format. We also recommend that a
185185+ file or class name and description of purpose be included on the
186186+ same "printed page" as the copyright notice for easier
187187+ identification within third-party archives.
188188+189189+Copyright [yyyy] [name of copyright owner]
190190+191191+Licensed under the Apache License, Version 2.0 (the "License");
192192+you may not use this file except in compliance with the License.
193193+You may obtain a copy of the License at
194194+195195+ http://www.apache.org/licenses/LICENSE-2.0
196196+197197+Unless required by applicable law or agreed to in writing, software
198198+distributed under the License is distributed on an "AS IS" BASIS,
199199+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200200+See the License for the specific language governing permissions and
201201+limitations under the License.
+25
crates/raichu/LICENSE_MIT
···11+Copyright (c) 2018 Tsiry Sandratraina <tsiry.sndr@fluentci.io>
22+33+Permission is hereby granted, free of charge, to any
44+person obtaining a copy of this software and associated
55+documentation files (the "Software"), to deal in the
66+Software without restriction, including without
77+limitation the rights to use, copy, modify, merge,
88+publish, distribute, sublicense, and/or sell copies of
99+the Software, and to permit persons to whom the Software
1010+is furnished to do so, subject to the following
1111+conditions:
1212+1313+The above copyright notice and this permission notice
1414+shall be included in all copies or substantial portions
1515+of the Software.
1616+1717+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
1818+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
1919+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
2020+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
2121+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
2222+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
2323+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
2424+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
2525+DEALINGS IN THE SOFTWARE.
+84
crates/raichu/README.md
···11+<div align="center">
22+33+ <h1><code>wasm-pack-template</code></h1>
44+55+ <strong>A template for kick starting a Rust and WebAssembly project using <a href="https://github.com/rustwasm/wasm-pack">wasm-pack</a>.</strong>
66+77+ <p>
88+ <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>
99+ </p>
1010+1111+ <h3>
1212+ <a href="https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html">Tutorial</a>
1313+ <span> | </span>
1414+ <a href="https://discordapp.com/channels/442252698964721669/443151097398296587">Chat</a>
1515+ </h3>
1616+1717+ <sub>Built with 🦀🕸 by <a href="https://rustwasm.github.io/">The Rust and WebAssembly Working Group</a></sub>
1818+</div>
1919+2020+## About
2121+2222+[**📚 Read this template tutorial! 📚**][template-docs]
2323+2424+This template is designed for compiling Rust libraries into WebAssembly and
2525+publishing the resulting package to NPM.
2626+2727+Be sure to check out [other `wasm-pack` tutorials online][tutorials] for other
2828+templates and usages of `wasm-pack`.
2929+3030+[tutorials]: https://rustwasm.github.io/docs/wasm-pack/tutorials/index.html
3131+[template-docs]: https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html
3232+3333+## 🚴 Usage
3434+3535+### 🐑 Use `cargo generate` to Clone this Template
3636+3737+[Learn more about `cargo generate` here.](https://github.com/ashleygwilliams/cargo-generate)
3838+3939+```
4040+cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project
4141+cd my-project
4242+```
4343+4444+### 🛠️ Build with `wasm-pack build`
4545+4646+```
4747+wasm-pack build
4848+```
4949+5050+### 🔬 Test in Headless Browsers with `wasm-pack test`
5151+5252+```
5353+wasm-pack test --headless --firefox
5454+```
5555+5656+### 🎁 Publish to NPM with `wasm-pack publish`
5757+5858+```
5959+wasm-pack publish
6060+```
6161+6262+## 🔋 Batteries Included
6363+6464+* [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) for communicating
6565+ between WebAssembly and JavaScript.
6666+* [`console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook)
6767+ for logging panic messages to the developer console.
6868+* `LICENSE-APACHE` and `LICENSE-MIT`: most Rust projects are licensed this way, so these are included for you
6969+7070+## License
7171+7272+Licensed under either of
7373+7474+* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
7575+* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
7676+7777+at your option.
7878+7979+### Contribution
8080+8181+Unless you explicitly state otherwise, any contribution intentionally
8282+submitted for inclusion in the work by you, as defined in the Apache-2.0
8383+license, shall be dual licensed as above, without any additional terms or
8484+conditions.
+1060
crates/raichu/src/lib.rs
···11+use base64::prelude::*;
22+use rand::seq::SliceRandom;
33+use rand::thread_rng;
44+use serde_json::json;
55+use std::f32::consts::PI;
66+use std::io::Cursor;
77+use symphonia::core::audio::SampleBuffer;
88+use symphonia::core::codecs::{DecoderOptions, CODEC_TYPE_NULL};
99+use symphonia::core::formats::FormatOptions;
1010+use symphonia::core::io::{MediaSource, MediaSourceStream};
1111+use symphonia::core::meta::MetadataOptions;
1212+use symphonia::core::probe::Hint;
1313+use wasm_bindgen::prelude::*;
1414+1515+#[wasm_bindgen]
1616+pub fn extract_audio_metadata(data: &[u8]) -> JsValue {
1717+ let media_source: Box<dyn MediaSource> = Box::new(Cursor::new(data.to_vec()));
1818+ let mss = MediaSourceStream::new(media_source, Default::default());
1919+2020+ let hint = Hint::new();
2121+2222+ let meta_opts = MetadataOptions::default();
2323+ let format_opts = FormatOptions::default();
2424+2525+ let mut probed =
2626+ match symphonia::default::get_probe().format(&hint, mss, &format_opts, &meta_opts) {
2727+ Ok(probed) => probed,
2828+ Err(_) => return JsValue::NULL, // Return null if the format is unsupported
2929+ };
3030+3131+ let mut metadata = json!({});
3232+3333+ // Extract metadata tags
3434+ if let Some(track) = probed.format.metadata().current() {
3535+ for tag in track.tags() {
3636+ if let Some(key) = tag.std_key {
3737+ metadata[&format!("{:?}", key)] = serde_json::Value::String(tag.value.to_string());
3838+ }
3939+ }
4040+4141+ // Extract album art if available
4242+ if let Some(cover) = track
4343+ .visuals()
4444+ .iter()
4545+ .find(|v| v.media_type.starts_with("image/"))
4646+ {
4747+ let base64_image = BASE64_STANDARD.encode(&cover.data);
4848+ let mime_type = &cover.media_type;
4949+ metadata["album_art"] = json!({
5050+ "data": base64_image,
5151+ "mime": mime_type,
5252+ });
5353+ }
5454+ }
5555+5656+ if let Some(track) = probed.format.tracks().first() {
5757+ if let Some(duration) = track.codec_params.n_frames {
5858+ if let Some(sample_rate) = track.codec_params.sample_rate {
5959+ let duration_seconds = duration as f64 / sample_rate as f64;
6060+ metadata["Duration"] = json!(duration_seconds);
6161+ }
6262+ }
6363+ }
6464+6565+ JsValue::from_str(serde_json::to_string(&metadata).unwrap().as_str())
6666+}
6767+6868+#[wasm_bindgen]
6969+pub struct AudioDecoder {
7070+ pcm_data: Vec<f32>,
7171+ sample_rate: u32,
7272+ channels: u16,
7373+}
7474+7575+#[wasm_bindgen]
7676+impl AudioDecoder {
7777+ #[wasm_bindgen(constructor)]
7878+ pub fn new() -> Self {
7979+ Self {
8080+ pcm_data: Vec::new(),
8181+ sample_rate: 44100,
8282+ channels: 2,
8383+ }
8484+ }
8585+8686+ #[wasm_bindgen]
8787+ pub fn decode(&mut self, audio_data: &[u8], ext: &str) -> Result<(), JsValue> {
8888+ let media_source: Box<dyn MediaSource> = Box::new(Cursor::new(audio_data.to_vec()));
8989+9090+ let mss = MediaSourceStream::new(media_source, Default::default());
9191+9292+ let mut hint = Hint::new();
9393+ hint.with_extension(ext);
9494+9595+ let probed = symphonia::default::get_probe()
9696+ .format(
9797+ &hint,
9898+ mss,
9999+ &FormatOptions {
100100+ enable_gapless: false,
101101+ ..Default::default()
102102+ },
103103+ &MetadataOptions::default(),
104104+ )
105105+ .map_err(|e| JsValue::from_str(&format!("Failed to read format: {}", e)))?;
106106+107107+ let mut format = probed.format;
108108+ let track = format
109109+ .default_track()
110110+ .ok_or_else(|| JsValue::from_str("No default track found"))?;
111111+ let codec_params = &track.codec_params;
112112+113113+ if codec_params.codec == CODEC_TYPE_NULL {
114114+ return Err(JsValue::from_str("Unsupported codec"));
115115+ }
116116+117117+ let mut decoder = symphonia::default::get_codecs()
118118+ .make(&codec_params, &DecoderOptions::default())
119119+ .map_err(|e| JsValue::from_str(&format!("Failed to create decoder: {}", e)))?;
120120+121121+ self.sample_rate = codec_params.sample_rate.unwrap_or(44100);
122122+ self.channels = 2;
123123+124124+ while let Ok(packet) = format.next_packet() {
125125+ let decoded = decoder
126126+ .decode(&packet)
127127+ .map_err(|e| JsValue::from_str(&format!("Decode error: {}", e)))?;
128128+ let mut sample_buf =
129129+ SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec());
130130+ sample_buf.copy_interleaved_ref(decoded);
131131+132132+ self.pcm_data.extend(sample_buf.samples());
133133+ }
134134+135135+ Ok(())
136136+ }
137137+138138+ #[wasm_bindgen]
139139+ pub fn get_pcm_data(&self) -> Vec<f32> {
140140+ self.pcm_data.clone()
141141+ }
142142+143143+ #[wasm_bindgen]
144144+ pub fn get_sample_rate(&self) -> u32 {
145145+ self.sample_rate
146146+ }
147147+148148+ #[wasm_bindgen]
149149+ pub fn get_channels(&self) -> u16 {
150150+ self.channels
151151+ }
152152+}
153153+154154+#[wasm_bindgen]
155155+pub enum FadeCurve {
156156+ Linear,
157157+ Exponential,
158158+ Logarithmic,
159159+}
160160+161161+/// Crossfades between two audio buffers using a specified fade curve
162162+#[wasm_bindgen]
163163+pub fn crossfade(
164164+ buffer_a: &[f32],
165165+ buffer_b: &[f32],
166166+ fade_duration: usize,
167167+ fade_curve: FadeCurve,
168168+) -> Vec<f32> {
169169+ let len_a = buffer_a.len();
170170+ let len_b = buffer_b.len();
171171+172172+ let crossfade_len = fade_duration.min(len_a).min(len_b);
173173+ let mut output = Vec::with_capacity(len_a + len_b - crossfade_len);
174174+175175+ // Copy the first part of buffer A
176176+ output.extend_from_slice(&buffer_a[..len_a - crossfade_len]);
177177+178178+ // Apply the crossfade
179179+ for i in 0..crossfade_len {
180180+ let t = i as f32 / crossfade_len as f32;
181181+ let (fade_out, fade_in) = match fade_curve {
182182+ FadeCurve::Linear => (1.0 - t, t),
183183+ FadeCurve::Exponential => ((1.0 - t).powi(2), t.powi(2)),
184184+ FadeCurve::Logarithmic => (1.0 - t.ln_1p(), t.ln_1p()),
185185+ };
186186+187187+ let mixed_sample = buffer_a[len_a - crossfade_len + i] * fade_out + buffer_b[i] * fade_in;
188188+189189+ output.push(mixed_sample);
190190+ }
191191+192192+ // Copy the remaining part of buffer B
193193+ output.extend_from_slice(&buffer_b[crossfade_len..]);
194194+195195+ output
196196+}
197197+198198+#[wasm_bindgen]
199199+pub struct BiquadFilter {
200200+ a0: f32,
201201+ a1: f32,
202202+ a2: f32,
203203+ b0: f32,
204204+ b1: f32,
205205+ b2: f32,
206206+ x1: f32,
207207+ x2: f32,
208208+ y1: f32,
209209+ y2: f32,
210210+}
211211+212212+#[wasm_bindgen]
213213+impl BiquadFilter {
214214+ // Create a new peaking EQ filter
215215+ #[wasm_bindgen]
216216+ pub fn peaking_eq(sample_rate: f32, frequency: f32, q: f32, gain_db: f32) -> Self {
217217+ let omega = 2.0 * PI * frequency / sample_rate;
218218+ let alpha = omega.sin() / (2.0 * q);
219219+ let a = 10.0_f32.powf(gain_db / 40.0);
220220+221221+ let cos_omega = omega.cos();
222222+223223+ let b0 = 1.0 + alpha * a;
224224+ let b1 = -2.0 * cos_omega;
225225+ let b2 = 1.0 - alpha * a;
226226+ let a0 = 1.0 + alpha / a;
227227+ let a1 = -2.0 * cos_omega;
228228+ let a2 = 1.0 - alpha / a;
229229+230230+ BiquadFilter {
231231+ a0,
232232+ a1,
233233+ a2,
234234+ b0,
235235+ b1,
236236+ b2,
237237+ x1: 0.0,
238238+ x2: 0.0,
239239+ y1: 0.0,
240240+ y2: 0.0,
241241+ }
242242+ }
243243+244244+ // Process a single sample through the filter
245245+ #[wasm_bindgen]
246246+ pub fn process(&mut self, input: f32) -> f32 {
247247+ let output = (self.b0 / self.a0) * input
248248+ + (self.b1 / self.a0) * self.x1
249249+ + (self.b2 / self.a0) * self.x2
250250+ - (self.a1 / self.a0) * self.y1
251251+ - (self.a2 / self.a0) * self.y2;
252252+253253+ // Update filter state
254254+ self.x2 = self.x1;
255255+ self.x1 = input;
256256+ self.y2 = self.y1;
257257+ self.y1 = output;
258258+259259+ output
260260+ }
261261+262262+ // Update filter parameters
263263+ #[wasm_bindgen]
264264+ pub fn update_parameters(&mut self, sample_rate: f32, frequency: f32, q: f32, gain_db: f32) {
265265+ let omega = 2.0 * PI * frequency / sample_rate;
266266+ let alpha = omega.sin() / (2.0 * q);
267267+ let a = 10.0_f32.powf(gain_db / 40.0);
268268+269269+ let cos_omega = omega.cos();
270270+271271+ self.b0 = 1.0 + alpha * a;
272272+ self.b1 = -2.0 * cos_omega;
273273+ self.b2 = 1.0 - alpha * a;
274274+ self.a0 = 1.0 + alpha / a;
275275+ self.a1 = -2.0 * cos_omega;
276276+ self.a2 = 1.0 - alpha / a;
277277+ }
278278+}
279279+280280+// 12-band equalizer
281281+#[wasm_bindgen]
282282+pub struct Equalizer {
283283+ bands: Vec<BiquadFilter>,
284284+ sample_rate: f32,
285285+}
286286+287287+#[wasm_bindgen]
288288+impl Equalizer {
289289+ // Create a new 12-band equalizer with standard frequency bands
290290+ #[wasm_bindgen(constructor)]
291291+ pub fn new(sample_rate: f32) -> Self {
292292+ // Standard frequency bands for a 12-band EQ (in Hz)
293293+ let frequencies = [
294294+ 31.0, 62.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 12000.0, 16000.0,
295295+ 20000.0,
296296+ ];
297297+298298+ let mut bands = Vec::with_capacity(frequencies.len());
299299+300300+ // Create a filter for each frequency band with default gain of 0 dB
301301+ for &freq in &frequencies {
302302+ bands.push(BiquadFilter::peaking_eq(sample_rate, freq, 1.414, 0.0));
303303+ }
304304+305305+ Equalizer { bands, sample_rate }
306306+ }
307307+308308+ // Set gain for a specific band
309309+ #[wasm_bindgen]
310310+ pub fn set_band_gain(&mut self, band_index: usize, gain_db: f32) {
311311+ if band_index >= self.bands.len() {
312312+ return;
313313+ }
314314+315315+ // Standard frequency bands
316316+ let frequencies = [
317317+ 31.0, 62.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 12000.0, 16000.0,
318318+ 20000.0,
319319+ ];
320320+321321+ // Update the filter parameters with the new gain
322322+ let frequency = frequencies[band_index];
323323+ let q = 1.414; // Standard Q value for EQ bands
324324+ self.bands[band_index].update_parameters(self.sample_rate, frequency, q, gain_db);
325325+ }
326326+327327+ // Process a single sample through all EQ bands
328328+ #[wasm_bindgen]
329329+ pub fn process(&mut self, input: f32) -> f32 {
330330+ let mut output = input;
331331+332332+ // Pass the input through each band filter in series
333333+ for band in &mut self.bands {
334334+ output = band.process(output);
335335+ }
336336+337337+ output
338338+ }
339339+340340+ // Process a buffer of samples
341341+ #[wasm_bindgen]
342342+ pub fn process_buffer(&mut self, input_buffer: &[f32], output_buffer: &mut [f32]) {
343343+ assert_eq!(input_buffer.len(), output_buffer.len());
344344+345345+ for i in 0..input_buffer.len() {
346346+ output_buffer[i] = self.process(input_buffer[i]);
347347+ }
348348+ }
349349+}
350350+351351+#[wasm_bindgen]
352352+pub struct AudioFilter {
353353+ sample_rate: f32,
354354+ // Common filter state variables
355355+ x1: f32,
356356+ x2: f32,
357357+ y1: f32,
358358+ y2: f32,
359359+ // Additional states for higher order filters
360360+ x3: f32,
361361+ x4: f32,
362362+ y3: f32,
363363+ y4: f32,
364364+}
365365+366366+#[wasm_bindgen]
367367+impl AudioFilter {
368368+ #[wasm_bindgen(constructor)]
369369+ pub fn new(sample_rate: f32) -> Self {
370370+ Self {
371371+ sample_rate,
372372+ x1: 0.0,
373373+ x2: 0.0,
374374+ y1: 0.0,
375375+ y2: 0.0,
376376+ x3: 0.0,
377377+ x4: 0.0,
378378+ y3: 0.0,
379379+ y4: 0.0,
380380+ }
381381+ }
382382+383383+ #[wasm_bindgen]
384384+ pub fn reset(&mut self) {
385385+ self.x1 = 0.0;
386386+ self.x2 = 0.0;
387387+ self.y1 = 0.0;
388388+ self.y2 = 0.0;
389389+ self.x3 = 0.0;
390390+ self.x4 = 0.0;
391391+ self.y3 = 0.0;
392392+ self.y4 = 0.0;
393393+ }
394394+}
395395+396396+#[wasm_bindgen]
397397+pub struct LowShelfFilter {
398398+ filter: AudioFilter,
399399+ a1: f32,
400400+ a2: f32,
401401+ b0: f32,
402402+ b1: f32,
403403+ b2: f32,
404404+}
405405+406406+#[wasm_bindgen]
407407+impl LowShelfFilter {
408408+ #[wasm_bindgen(constructor)]
409409+ pub fn new(sample_rate: f32, frequency: f32, gain_db: f32, q: f32) -> Self {
410410+ let mut filter = Self {
411411+ filter: AudioFilter::new(sample_rate),
412412+ a1: 0.0,
413413+ a2: 0.0,
414414+ b0: 0.0,
415415+ b1: 0.0,
416416+ b2: 0.0,
417417+ };
418418+ filter.set_parameters(frequency, gain_db, q);
419419+ filter
420420+ }
421421+422422+ #[wasm_bindgen]
423423+ pub fn set_parameters(&mut self, frequency: f32, gain_db: f32, q: f32) {
424424+ let a = 10.0_f32.powf(gain_db / 40.0);
425425+ let omega = 2.0 * std::f32::consts::PI * frequency / self.filter.sample_rate;
426426+ let sin_omega = omega.sin();
427427+ let cos_omega = omega.cos();
428428+ let alpha = sin_omega / (2.0 * q);
429429+ let beta = (a + 1.0 / a).sqrt() * 2.0 * alpha;
430430+431431+ let b0 = a * ((a + 1.0) - (a - 1.0) * cos_omega + beta);
432432+ let b1 = 2.0 * a * ((a - 1.0) - (a + 1.0) * cos_omega);
433433+ let b2 = a * ((a + 1.0) - (a - 1.0) * cos_omega - beta);
434434+ let a0 = (a + 1.0) + (a - 1.0) * cos_omega + beta;
435435+ let a1 = -2.0 * ((a - 1.0) + (a + 1.0) * cos_omega);
436436+ let a2 = (a + 1.0) + (a - 1.0) * cos_omega - beta;
437437+438438+ self.b0 = b0 / a0;
439439+ self.b1 = b1 / a0;
440440+ self.b2 = b2 / a0;
441441+ self.a1 = a1 / a0;
442442+ self.a2 = a2 / a0;
443443+ }
444444+445445+ #[wasm_bindgen]
446446+ pub fn process(&mut self, input: f32) -> f32 {
447447+ let output = self.b0 * input + self.b1 * self.filter.x1 + self.b2 * self.filter.x2
448448+ - self.a1 * self.filter.y1
449449+ - self.a2 * self.filter.y2;
450450+451451+ self.filter.x2 = self.filter.x1;
452452+ self.filter.x1 = input;
453453+ self.filter.y2 = self.filter.y1;
454454+ self.filter.y1 = output;
455455+456456+ output
457457+ }
458458+459459+ #[wasm_bindgen]
460460+ pub fn reset(&mut self) {
461461+ self.filter.reset();
462462+ }
463463+}
464464+465465+#[wasm_bindgen]
466466+pub struct HighShelfFilter {
467467+ filter: AudioFilter,
468468+ a1: f32,
469469+ a2: f32,
470470+ b0: f32,
471471+ b1: f32,
472472+ b2: f32,
473473+}
474474+475475+#[wasm_bindgen]
476476+impl HighShelfFilter {
477477+ #[wasm_bindgen(constructor)]
478478+ pub fn new(sample_rate: f32, frequency: f32, gain_db: f32, q: f32) -> Self {
479479+ let mut filter = Self {
480480+ filter: AudioFilter::new(sample_rate),
481481+ a1: 0.0,
482482+ a2: 0.0,
483483+ b0: 0.0,
484484+ b1: 0.0,
485485+ b2: 0.0,
486486+ };
487487+ filter.set_parameters(frequency, gain_db, q);
488488+ filter
489489+ }
490490+491491+ #[wasm_bindgen]
492492+ pub fn set_parameters(&mut self, frequency: f32, gain_db: f32, q: f32) {
493493+ let a = 10.0_f32.powf(gain_db / 40.0);
494494+ let omega = 2.0 * std::f32::consts::PI * frequency / self.filter.sample_rate;
495495+ let sin_omega = omega.sin();
496496+ let cos_omega = omega.cos();
497497+ let alpha = sin_omega / (2.0 * q);
498498+ let beta = (a + 1.0 / a).sqrt() * 2.0 * alpha;
499499+500500+ let b0 = a * ((a + 1.0) + (a - 1.0) * cos_omega + beta);
501501+ let b1 = -2.0 * a * ((a - 1.0) + (a + 1.0) * cos_omega);
502502+ let b2 = a * ((a + 1.0) + (a - 1.0) * cos_omega - beta);
503503+ let a0 = (a + 1.0) - (a - 1.0) * cos_omega + beta;
504504+ let a1 = 2.0 * ((a - 1.0) - (a + 1.0) * cos_omega);
505505+ let a2 = (a + 1.0) - (a - 1.0) * cos_omega - beta;
506506+507507+ self.b0 = b0 / a0;
508508+ self.b1 = b1 / a0;
509509+ self.b2 = b2 / a0;
510510+ self.a1 = a1 / a0;
511511+ self.a2 = a2 / a0;
512512+ }
513513+514514+ #[wasm_bindgen]
515515+ pub fn process(&mut self, input: f32) -> f32 {
516516+ let output = self.b0 * input + self.b1 * self.filter.x1 + self.b2 * self.filter.x2
517517+ - self.a1 * self.filter.y1
518518+ - self.a2 * self.filter.y2;
519519+520520+ self.filter.x2 = self.filter.x1;
521521+ self.filter.x1 = input;
522522+ self.filter.y2 = self.filter.y1;
523523+ self.filter.y1 = output;
524524+525525+ output
526526+ }
527527+528528+ #[wasm_bindgen]
529529+ pub fn reset(&mut self) {
530530+ self.filter.reset();
531531+ }
532532+}
533533+534534+#[wasm_bindgen]
535535+pub struct BandPassFilter {
536536+ filter: AudioFilter,
537537+ a1: f32,
538538+ a2: f32,
539539+ b0: f32,
540540+ b1: f32,
541541+ b2: f32,
542542+}
543543+544544+#[wasm_bindgen]
545545+impl BandPassFilter {
546546+ #[wasm_bindgen(constructor)]
547547+ pub fn new(sample_rate: f32, frequency: f32, q: f32) -> Self {
548548+ let mut filter = Self {
549549+ filter: AudioFilter::new(sample_rate),
550550+ a1: 0.0,
551551+ a2: 0.0,
552552+ b0: 0.0,
553553+ b1: 0.0,
554554+ b2: 0.0,
555555+ };
556556+ filter.set_parameters(frequency, q);
557557+ filter
558558+ }
559559+560560+ #[wasm_bindgen]
561561+ pub fn set_parameters(&mut self, frequency: f32, q: f32) {
562562+ let omega = 2.0 * std::f32::consts::PI * frequency / self.filter.sample_rate;
563563+ let sin_omega = omega.sin();
564564+ let cos_omega = omega.cos();
565565+ let alpha = sin_omega / (2.0 * q);
566566+567567+ let b0 = alpha;
568568+ let b1 = 0.0;
569569+ let b2 = -alpha;
570570+ let a0 = 1.0 + alpha;
571571+ let a1 = -2.0 * cos_omega;
572572+ let a2 = 1.0 - alpha;
573573+574574+ self.b0 = b0 / a0;
575575+ self.b1 = b1 / a0;
576576+ self.b2 = b2 / a0;
577577+ self.a1 = a1 / a0;
578578+ self.a2 = a2 / a0;
579579+ }
580580+581581+ #[wasm_bindgen]
582582+ pub fn process(&mut self, input: f32) -> f32 {
583583+ let output = self.b0 * input + self.b1 * self.filter.x1 + self.b2 * self.filter.x2
584584+ - self.a1 * self.filter.y1
585585+ - self.a2 * self.filter.y2;
586586+587587+ self.filter.x2 = self.filter.x1;
588588+ self.filter.x1 = input;
589589+ self.filter.y2 = self.filter.y1;
590590+ self.filter.y1 = output;
591591+592592+ output
593593+ }
594594+595595+ #[wasm_bindgen]
596596+ pub fn reset(&mut self) {
597597+ self.filter.reset();
598598+ }
599599+}
600600+601601+#[wasm_bindgen]
602602+pub struct BesselFilter {
603603+ filter: AudioFilter,
604604+ order: usize,
605605+ // Coefficients for sections
606606+ a: Vec<Vec<f32>>,
607607+ b: Vec<Vec<f32>>,
608608+}
609609+610610+#[wasm_bindgen]
611611+impl BesselFilter {
612612+ #[wasm_bindgen(constructor)]
613613+ pub fn new(sample_rate: f32, cutoff_frequency: f32, order: usize) -> Self {
614614+ if order != 4 && order != 8 {
615615+ panic!("Only 4th and 8th order Bessel filters are implemented");
616616+ }
617617+618618+ let mut filter = Self {
619619+ filter: AudioFilter::new(sample_rate),
620620+ order,
621621+ a: vec![],
622622+ b: vec![],
623623+ };
624624+ filter.set_parameters(cutoff_frequency);
625625+ filter
626626+ }
627627+628628+ #[wasm_bindgen]
629629+ pub fn set_parameters(&mut self, cutoff_frequency: f32) {
630630+ let fs = self.filter.sample_rate;
631631+ let fc = cutoff_frequency;
632632+633633+ // Clear previous coefficients
634634+ self.a = vec![];
635635+ self.b = vec![];
636636+637637+ // Normalized frequency
638638+ let omega_c = 2.0 * std::f32::consts::PI * fc / fs;
639639+640640+ // Bessel polynomials for 4th and 8th order
641641+ // These are the poles of the normalized (omega_c = 1) Bessel filter
642642+ let poles = if self.order == 4 {
643643+ vec![
644644+ (-0.6572111112819416, 0.8301614350048806),
645645+ (-0.6572111112819416, -0.8301614350048806),
646646+ (-0.9047587967882449, 0.2709187330038746),
647647+ (-0.9047587967882449, -0.2709187330038746),
648648+ ]
649649+ } else {
650650+ // 8th order
651651+ vec![
652652+ (-0.5905759446119192, 0.9072067564574548),
653653+ (-0.5905759446119192, -0.9072067564574548),
654654+ (-0.6707106781186548, 0.7937387988730166),
655655+ (-0.6707106781186548, -0.7937387988730166),
656656+ (-0.7996541858328288, 0.6000376420593046),
657657+ (-0.7996541858328288, -0.6000376420593046),
658658+ (-0.8717401485096066, 0.3349881501782813),
659659+ (-0.8717401485096066, -0.3349881501782813),
660660+ ]
661661+ };
662662+663663+ // Create second-order sections from complex conjugate pairs
664664+ for section in poles.chunks(2) {
665665+ let (real, imag) = section[0];
666666+667667+ // Bilinear transform to map s-plane to z-plane
668668+ let c = 1.0 / (omega_c * 2.0);
669669+ let d = 1.0 + real * c;
670670+ let e = real * real + imag * imag;
671671+672672+ let b0 = 1.0 / d;
673673+ let b1 = 2.0 / d;
674674+ let b2 = 1.0 / d;
675675+676676+ let a0 = 1.0;
677677+ let a1 = 2.0 * (1.0 - e * c * c) / d;
678678+ let a2 = (1.0 - 2.0 * real * c + e * c * c) / d;
679679+680680+ self.b.push(vec![b0, b1, b2]);
681681+ self.a.push(vec![a0, a1, a2]);
682682+ }
683683+ }
684684+685685+ #[wasm_bindgen]
686686+ pub fn process(&mut self, input: f32) -> f32 {
687687+ let num_sections = self.order / 2;
688688+ let mut output = input;
689689+690690+ for i in 0..num_sections {
691691+ let x = output;
692692+693693+ output =
694694+ self.b[i][0] * x + self.b[i][1] * self.filter.x1 + self.b[i][2] * self.filter.x2
695695+ - self.a[i][1] * self.filter.y1
696696+ - self.a[i][2] * self.filter.y2;
697697+698698+ self.filter.x2 = self.filter.x1;
699699+ self.filter.x1 = x;
700700+ self.filter.y2 = self.filter.y1;
701701+ self.filter.y1 = output;
702702+ }
703703+704704+ output
705705+ }
706706+707707+ #[wasm_bindgen]
708708+ pub fn reset(&mut self) {
709709+ self.filter.reset();
710710+ }
711711+}
712712+713713+#[wasm_bindgen]
714714+pub struct LinkwitzRileyFilter {
715715+ filter: AudioFilter,
716716+ // For 8th order (48 dB/Oct) we need 4 biquad sections
717717+ a: Vec<Vec<f32>>,
718718+ b: Vec<Vec<f32>>,
719719+ state: Vec<Vec<f32>>, // x1, x2, y1, y2 for each section
720720+}
721721+722722+#[wasm_bindgen]
723723+impl LinkwitzRileyFilter {
724724+ #[wasm_bindgen(constructor)]
725725+ pub fn new(sample_rate: f32, cutoff_frequency: f32, filter_type: FilterType) -> Self {
726726+ let mut filter = Self {
727727+ filter: AudioFilter::new(sample_rate),
728728+ a: vec![vec![0.0; 3]; 4], // 4 biquad sections, each with 3 coefficients
729729+ b: vec![vec![0.0; 3]; 4],
730730+ state: vec![vec![0.0; 4]; 4], // 4 states per section
731731+ };
732732+ filter.set_parameters(cutoff_frequency, filter_type);
733733+ filter
734734+ }
735735+736736+ #[wasm_bindgen]
737737+ pub fn set_parameters(&mut self, cutoff_frequency: f32, filter_type: FilterType) {
738738+ let omega_c = 2.0 * std::f32::consts::PI * cutoff_frequency / self.filter.sample_rate;
739739+ let k = omega_c.tan();
740740+741741+ // Butterworth poles for 8th order (48 dB/Oct)
742742+ let poles = [
743743+ (-0.9808, 0.1951),
744744+ (-0.9808, -0.1951),
745745+ (-0.8315, 0.5556),
746746+ (-0.8315, -0.5556),
747747+ (-0.5556, 0.8315),
748748+ (-0.5556, -0.8315),
749749+ (-0.1951, 0.9808),
750750+ (-0.1951, -0.9808),
751751+ ];
752752+753753+ // Process each section (4 sections for 8th order)
754754+ for i in 0..4 {
755755+ let (real, imag) = poles[i * 2];
756756+757757+ // Bilinear transform
758758+ let d = 1.0 + real * k + (real * real + imag * imag) * k * k;
759759+760760+ match filter_type {
761761+ FilterType::LowPass => {
762762+ let k_squared = k * k;
763763+ self.b[i][0] = k_squared / d;
764764+ self.b[i][1] = 2.0 * k_squared / d;
765765+ self.b[i][2] = k_squared / d;
766766+ self.a[i][0] = 1.0;
767767+ self.a[i][1] = 2.0 * (k_squared - 1.0) / d;
768768+ self.a[i][2] = (1.0 - real * k + (real * real + imag * imag) * k * k) / d;
769769+ }
770770+ FilterType::HighPass => {
771771+ self.b[i][0] = 1.0 / d;
772772+ self.b[i][1] = -2.0 / d;
773773+ self.b[i][2] = 1.0 / d;
774774+ self.a[i][0] = 1.0;
775775+ self.a[i][1] = 2.0 * (k * k - 1.0) / d;
776776+ self.a[i][2] = (1.0 - real * k + (real * real + imag * imag) * k * k) / d;
777777+ }
778778+ }
779779+ }
780780+ }
781781+782782+ #[wasm_bindgen]
783783+ pub fn process(&mut self, input: f32) -> f32 {
784784+ let mut output = input;
785785+786786+ for i in 0..4 {
787787+ let x = output;
788788+789789+ // Apply the biquad filter
790790+ output = self.b[i][0] * x
791791+ + self.b[i][1] * self.state[i][0]
792792+ + self.b[i][2] * self.state[i][1]
793793+ - self.a[i][1] * self.state[i][2]
794794+ - self.a[i][2] * self.state[i][3];
795795+796796+ // Update states
797797+ self.state[i][1] = self.state[i][0];
798798+ self.state[i][0] = x;
799799+ self.state[i][3] = self.state[i][2];
800800+ self.state[i][2] = output;
801801+ }
802802+803803+ output
804804+ }
805805+806806+ #[wasm_bindgen]
807807+ pub fn reset(&mut self) {
808808+ for i in 0..4 {
809809+ for j in 0..4 {
810810+ self.state[i][j] = 0.0;
811811+ }
812812+ }
813813+ }
814814+}
815815+816816+#[wasm_bindgen]
817817+pub enum FilterType {
818818+ LowPass,
819819+ HighPass,
820820+}
821821+822822+#[wasm_bindgen]
823823+pub struct Playlist {
824824+ tracks: Vec<String>,
825825+ current_track: usize,
826826+}
827827+828828+#[wasm_bindgen]
829829+impl Playlist {
830830+ #[wasm_bindgen(constructor)]
831831+ pub fn new() -> Self {
832832+ Self {
833833+ tracks: Vec::new(),
834834+ current_track: 0,
835835+ }
836836+ }
837837+838838+ #[wasm_bindgen]
839839+ pub fn add_track(&mut self, track: &str) -> usize {
840840+ self.tracks.push(track.to_string());
841841+ self.tracks.len()
842842+ }
843843+844844+ #[wasm_bindgen]
845845+ pub fn next_track(&mut self) -> String {
846846+ if self.tracks.is_empty() {
847847+ return String::new();
848848+ }
849849+850850+ self.current_track = (self.current_track + 1) % self.tracks.len();
851851+ self.tracks[self.current_track].clone()
852852+ }
853853+854854+ #[wasm_bindgen]
855855+ pub fn previous_track(&mut self) -> String {
856856+ if self.tracks.is_empty() {
857857+ return String::new();
858858+ }
859859+860860+ if self.current_track == 0 {
861861+ self.current_track = self.tracks.len() - 1;
862862+ } else {
863863+ self.current_track -= 1;
864864+ }
865865+866866+ self.tracks[self.current_track].clone()
867867+ }
868868+869869+ #[wasm_bindgen]
870870+ pub fn current_track(&self) -> String {
871871+ if self.tracks.is_empty() {
872872+ return String::new();
873873+ }
874874+875875+ self.tracks[self.current_track].clone()
876876+ }
877877+878878+ #[wasm_bindgen]
879879+ pub fn clear(&mut self) {
880880+ self.tracks.clear();
881881+ self.current_track = 0;
882882+ }
883883+884884+ #[wasm_bindgen]
885885+ pub fn size(&self) -> usize {
886886+ self.tracks.len()
887887+ }
888888+889889+ #[wasm_bindgen]
890890+ pub fn current_index(&self) -> usize {
891891+ self.current_track
892892+ }
893893+894894+ #[wasm_bindgen]
895895+ pub fn set_current_index(&mut self, index: usize) {
896896+ if index < self.tracks.len() {
897897+ self.current_track = index;
898898+ }
899899+ }
900900+901901+ #[wasm_bindgen]
902902+ pub fn remove_track(&mut self, index: usize) {
903903+ if index < self.tracks.len() {
904904+ self.tracks.remove(index);
905905+ }
906906+ }
907907+908908+ #[wasm_bindgen]
909909+ pub fn get_track(&self, index: usize) -> String {
910910+ if index < self.tracks.len() {
911911+ self.tracks[index].clone()
912912+ } else {
913913+ String::new()
914914+ }
915915+ }
916916+917917+ #[wasm_bindgen]
918918+919919+ pub fn get_tracks(&self) -> js_sys::Array {
920920+ self.tracks.iter().map(|t| JsValue::from_str(t)).collect()
921921+ }
922922+923923+ #[wasm_bindgen]
924924+ pub fn shuffle(&mut self) {
925925+ let mut rng = thread_rng();
926926+ self.tracks.shuffle(&mut rng);
927927+ }
928928+929929+ #[wasm_bindgen]
930930+ pub fn insert_track(&mut self, index: usize, track: &str) {
931931+ if index <= self.tracks.len() {
932932+ self.tracks.insert(index, track.to_string());
933933+ }
934934+ }
935935+936936+ #[wasm_bindgen]
937937+ pub fn get_next_track(&self) -> String {
938938+ if self.tracks.is_empty() {
939939+ return String::new();
940940+ }
941941+942942+ let next_track = (self.current_track + 1) % self.tracks.len();
943943+ self.tracks[next_track].clone()
944944+ }
945945+946946+ #[wasm_bindgen]
947947+ pub fn get_next_tracks(&self) -> js_sys::Array {
948948+ if self.tracks.is_empty() {
949949+ return js_sys::Array::new();
950950+ }
951951+952952+ let current_track = self.current_track;
953953+ let next_tracks: Vec<String> = self
954954+ .tracks
955955+ .iter()
956956+ .skip(current_track + 1)
957957+ .cloned()
958958+ .collect();
959959+ next_tracks.iter().map(|t| JsValue::from_str(t)).collect()
960960+ }
961961+}
962962+963963+#[cfg(test)]
964964+mod tests {
965965+ use super::*;
966966+967967+ #[test]
968968+ fn test_playlist_add_and_get_tracks() {
969969+ let mut playlist = Playlist::new();
970970+ playlist.add_track("Track 1");
971971+ playlist.add_track("Track 2");
972972+ playlist.add_track("Track 3");
973973+974974+ assert_eq!(playlist.size(), 3);
975975+ assert_eq!(playlist.get_track(0), "Track 1");
976976+ assert_eq!(playlist.get_track(1), "Track 2");
977977+ assert_eq!(playlist.get_track(2), "Track 3");
978978+ }
979979+980980+ #[test]
981981+ fn test_playlist_navigation() {
982982+ let mut playlist = Playlist::new();
983983+ playlist.add_track("Track 1");
984984+ playlist.add_track("Track 2");
985985+ playlist.add_track("Track 3");
986986+987987+ assert_eq!(playlist.current_track(), "Track 1");
988988+ assert_eq!(playlist.next_track(), "Track 2");
989989+ assert_eq!(playlist.next_track(), "Track 3");
990990+ assert_eq!(playlist.next_track(), "Track 1");
991991+992992+ assert_eq!(playlist.previous_track(), "Track 3");
993993+ assert_eq!(playlist.previous_track(), "Track 2");
994994+ assert_eq!(playlist.previous_track(), "Track 1");
995995+ }
996996+997997+ #[test]
998998+ fn test_playlist_clear() {
999999+ let mut playlist = Playlist::new();
10001000+ playlist.add_track("Track 1");
10011001+ playlist.add_track("Track 2");
10021002+10031003+ assert_eq!(playlist.size(), 2);
10041004+ playlist.clear();
10051005+ assert_eq!(playlist.size(), 0);
10061006+ assert_eq!(playlist.current_track(), "");
10071007+ }
10081008+10091009+ #[test]
10101010+ fn test_playlist_remove_track() {
10111011+ let mut playlist = Playlist::new();
10121012+ playlist.add_track("Track 1");
10131013+ playlist.add_track("Track 2");
10141014+ playlist.add_track("Track 3");
10151015+10161016+ playlist.remove_track(1);
10171017+ assert_eq!(playlist.size(), 2);
10181018+ assert_eq!(playlist.get_track(0), "Track 1");
10191019+ assert_eq!(playlist.get_track(1), "Track 3");
10201020+ }
10211021+10221022+ #[test]
10231023+ fn test_playlist_insert_track() {
10241024+ let mut playlist = Playlist::new();
10251025+ playlist.add_track("Track 1");
10261026+ playlist.add_track("Track 3");
10271027+10281028+ playlist.insert_track(1, "Track 2");
10291029+ assert_eq!(playlist.size(), 3);
10301030+ assert_eq!(playlist.get_track(0), "Track 1");
10311031+ assert_eq!(playlist.get_track(1), "Track 2");
10321032+ assert_eq!(playlist.get_track(2), "Track 3");
10331033+ }
10341034+10351035+ #[test]
10361036+ fn test_playlist_set_current_index() {
10371037+ let mut playlist = Playlist::new();
10381038+ playlist.add_track("Track 1");
10391039+ playlist.add_track("Track 2");
10401040+ playlist.add_track("Track 3");
10411041+10421042+ playlist.set_current_index(2);
10431043+ assert_eq!(playlist.current_track(), "Track 3");
10441044+10451045+ playlist.set_current_index(0);
10461046+ assert_eq!(playlist.current_track(), "Track 1");
10471047+ }
10481048+10491049+ #[test]
10501050+ fn test_playlist_get_next_track() {
10511051+ let mut playlist = Playlist::new();
10521052+ playlist.add_track("Track 1");
10531053+ playlist.add_track("Track 2");
10541054+ playlist.add_track("Track 3");
10551055+10561056+ assert_eq!(playlist.get_next_track(), "Track 2");
10571057+ playlist.next_track();
10581058+ assert_eq!(playlist.get_next_track(), "Track 3");
10591059+ }
10601060+}