Skip to content

Instantly share code, notes, and snippets.

@boyswan
Created March 11, 2024 16:29
Show Gist options
  • Save boyswan/a8d1ba4a2a1ab3a1c375a00ab9c6de10 to your computer and use it in GitHub Desktop.
Save boyswan/a8d1ba4a2a1ab3a1c375a00ab9c6de10 to your computer and use it in GitHub Desktop.
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