Created
March 11, 2024 16:29
-
-
Save boyswan/a8d1ba4a2a1ab3a1c375a00ab9c6de10 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use js_sys::{Array, Object, Reflect}; | |
use leptos::html::Div; | |
use leptos::*; | |
use wasm_bindgen::prelude::*; | |
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement}; | |
// Step 1: Create the WaveSurfer Binding | |
#[wasm_bindgen] | |
extern "C" { | |
pub type WaveSurfer; | |
#[wasm_bindgen(static_method_of = WaveSurfer)] | |
fn create(options: &JsValue) -> WaveSurfer; | |
#[wasm_bindgen(method)] | |
fn load(this: &WaveSurfer, url: &str); | |
#[wasm_bindgen(method)] | |
fn play(this: &WaveSurfer); | |
#[wasm_bindgen(js_namespace = ["WaveSurfer", "Timeline"], js_name = create)] | |
fn create_timeline(params: &JsValue) -> JsValue; | |
} | |
fn ctx_render_function(channels: JsValue, ctx: JsValue) { | |
let channels_array = js_sys::Array::from(&channels); | |
let ctx: CanvasRenderingContext2d = ctx.dyn_into().unwrap(); | |
let top_channel_js = js_sys::Float32Array::from(channels_array.get(0)); | |
let top_channel: Vec<f32> = top_channel_js.to_vec(); | |
let bottom_channel_js = js_sys::Float32Array::from(channels_array.get(1)); | |
let bottom_channel: Vec<f32> = bottom_channel_js.to_vec(); | |
let length = top_channel.len() as f64; | |
let canvas: HtmlCanvasElement = ctx.canvas().unwrap(); | |
let width = canvas.width() as f64; | |
let height = canvas.height() as f64; | |
let half_height = height / 2.0; | |
let pixel_ratio = web_sys::window().unwrap().device_pixel_ratio(); | |
let bar_width = 4.0 * pixel_ratio; | |
let bar_gap = 2.0; | |
let bar_radius = 4.0; | |
let bar_index_scale = width / (bar_width + bar_gap) / length; | |
let v_scale = 1.0; | |
let mut prev_x = 0.0; | |
let mut max_top = 0.0; | |
let mut max_bottom = 0.0; | |
for i in 0..=length as usize { | |
ctx.begin_path(); | |
let x = (i as f64 * bar_index_scale).round(); | |
// Omitted: User and AI message index logic, adapt as necessary | |
// For demonstration, we'll set a default fill style | |
ctx.set_fill_style(&JsValue::from_str("#999")); | |
if x > prev_x { | |
let top_bar_height = (max_top * half_height * v_scale).round(); | |
let bottom_bar_height = (max_bottom * half_height * v_scale).round(); | |
let bar_height = top_bar_height + bottom_bar_height.max(1.0); | |
let y = half_height - top_bar_height; | |
let _ = ctx.round_rect_with_f64( | |
prev_x * (bar_width + bar_gap), | |
y, | |
bar_width, | |
bar_height, | |
bar_radius, | |
); | |
prev_x = x; | |
max_top = 0.0; | |
max_bottom = 0.0; | |
} | |
let magnitude_top = top_channel[i].abs() as f64; | |
let magnitude_bottom = bottom_channel[i].abs() as f64; | |
if magnitude_top > max_top { | |
max_top = magnitude_top; | |
} | |
if magnitude_bottom > max_bottom { | |
max_bottom = magnitude_bottom; | |
} | |
ctx.fill(); | |
ctx.close_path(); | |
} | |
} | |
#[component] | |
fn App() -> impl IntoView { | |
let audio_ref = create_node_ref::<Div>(); | |
create_effect(move |_| { | |
if let Some(audio_element) = audio_ref.get() { | |
let options = js_sys::Object::new(); | |
Reflect::set(&options, &"container".into(), &audio_element).unwrap(); | |
Reflect::set( | |
&options, | |
&JsValue::from_str("dragToSeek"), | |
&JsValue::from_bool(true), | |
) | |
.unwrap(); | |
Reflect::set( | |
&options, | |
&JsValue::from_str("cursorColor"), | |
&JsValue::from_str("var(--orange-400)"), | |
) | |
.unwrap(); | |
Reflect::set( | |
&options, | |
&JsValue::from_str("cursorWidth"), | |
&JsValue::from_f64(2.0), | |
) | |
.unwrap(); | |
Reflect::set( | |
&options, | |
&JsValue::from_str("progressColor"), | |
&JsValue::from_str("rgba(0, 0, 0, 0)"), | |
) | |
.unwrap(); | |
let render_function = Closure::wrap(Box::new(move |channels: JsValue, ctx: JsValue| { | |
ctx_render_function(channels, ctx) | |
}) as Box<dyn Fn(JsValue, JsValue)>); | |
// Set the render function in the options | |
Reflect::set( | |
&options, | |
&JsValue::from_str("renderFunction"), | |
render_function.as_ref().unchecked_ref(), | |
) | |
.unwrap(); | |
// Prevent the closure from being garbage collected | |
render_function.forget(); | |
let plugins_array = Array::new(); | |
let timeline_options = js_sys::Object::new(); | |
let timelines_plugin = create_timeline(&timeline_options); | |
plugins_array.push(&timelines_plugin); | |
Reflect::set(&options, &JsValue::from_str("plugins"), &plugins_array).unwrap(); | |
// Create the WaveSurfer instance | |
let wavesurfer = WaveSurfer::create(&options.into()); | |
// Load an audio file; replace with your audio URL | |
wavesurfer.load("https://sound-effects-media.bbcrewind.co.uk/mp3/NHU05104095.mp3"); | |
} | |
}); | |
view! { | |
<div node_ref={audio_ref} class="audio-container"></div> | |
} | |
} | |
fn main() { | |
mount_to_body(App) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment