Skip to content

Commit 61ff8ad

Browse files
dilawarDilawar Singh
andauthored
Plot audio data on canvas (#5)
* Can record audio now * Show pcm data in console * Got the data. I am so cool! * Now plot it on canvas * Plot raw data * Increase the amplitude * Adds screenshot --------- Co-authored-by: Dilawar Singh <dilawar@dognosis.tech>
1 parent fd6bbac commit 61ff8ad

File tree

6 files changed

+95
-16
lines changed

6 files changed

+95
-16
lines changed

leptos/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ edition = "2024"
66
[dependencies]
77
codee = { version = "0.3.0", features = ["json_serde"] }
88
console_error_panic_hook = "0.1.7"
9+
futures = "0.3.31"
10+
gloo-file = { version = "0.3.0", features = ["futures"] }
911
leptos = { version = "0.7.7", features = ["csr", "tracing"] }
1012
leptos-qr-scanner = { git = "https://github.com/dilawar/leptos-qr-scanner" }
1113
leptos-use = { version = "0.15.7", features = [
@@ -22,3 +24,9 @@ thaw = { version = "0.4.4", features = ["csr"] }
2224
tracing.workspace = true
2325
tracing-subscriber.workspace = true
2426
tracing-subscriber-wasm = "0.1.0"
27+
web-sys = { version = "0.3.77", features = [
28+
"BlobEvent",
29+
"CanvasRenderingContext2d",
30+
"MediaRecorder",
31+
"AudioNode",
32+
] }

leptos/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22

33
A simple SPA using leptos.
44

5+
![](https://file.notion.so/f/f/5f1a3a94-a29f-407d-80af-617105cb793d/8938aed8-802c-4a23-9d47-10cd593ef1ca/image.png?table=block&id=1c0e579b-ff89-80df-abb6-c7766ae48094&spaceId=5f1a3a94-a29f-407d-80af-617105cb793d&expirationTimestamp=1742860800000&signature=FwK8fJ19MquzJKhRT0vRtm7JaOl7yjWOgEYxAz95gtE&downloadName=image.png)
6+
57
- A QR Scanner
68
- A Audio Recorder

leptos/app.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,11 @@ main {
3838
min-height: 400px;
3939
border: 1px dotted gray;
4040
}
41+
42+
.canvas-372e0a1 {
43+
border: 1px dotted blue;
44+
border-radius: 10px;
45+
width: 100%;
46+
max-height: 100px;
47+
background-color: ivory;
48+
}

leptos/src/_app.module.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,11 @@ main {
3838
min-height: 400px;
3939
border: 1px dotted gray;
4040
}
41+
42+
.canvas {
43+
border: 1px dotted blue;
44+
border-radius: 10px;
45+
width: 100%;
46+
max-height: 100px;
47+
background-color: ivory;
48+
}

leptos/src/components/audio.rs

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,22 @@
33
use leptos::prelude::*;
44
use leptos_use::{use_user_media_with_options, UseUserMediaOptions, UseUserMediaReturn};
55
use thaw::*;
6+
use web_sys::wasm_bindgen::{closure::Closure, JsCast, JsValue};
7+
use web_sys::MediaRecorder;
68

79
use crate::css::styles;
810

11+
// Record audio
12+
//
13+
// Thanks you <https://github.com/wayeast/mediarecorder/blob/master/src/lib.rs> and chatgpt sucked
14+
// lemons.
915
#[component]
1016
pub fn AudioStream() -> impl IntoView {
1117
let node = NodeRef::<leptos::html::Audio>::new();
18+
let canvas_node = NodeRef::<leptos::html::Canvas>::new();
1219

13-
let start_rec = RwSignal::new(true);
14-
let result = RwSignal::new_local("".to_string());
20+
let start_rec = RwSignal::new(false);
21+
let async_blob = RwSignal::new(None);
1522

1623
// Create options to fetch only audio stream.
1724
let options = UseUserMediaOptions::default().video(false).audio(true);
@@ -23,7 +30,35 @@ pub fn AudioStream() -> impl IntoView {
2330
..
2431
} = use_user_media_with_options(options);
2532

26-
// start/stop recording
33+
let on_data_available = Closure::wrap(Box::new(move |data: JsValue| {
34+
if let Ok(blob) = data.dyn_into::<web_sys::BlobEvent>() {
35+
if let Some(data) = blob.data() {
36+
// convert to gloo_file Blog.
37+
let blob = gloo_file::Blob::from(data);
38+
*async_blob.write() = Some(LocalResource::new(move || {
39+
gloo_file::futures::read_as_bytes(&blob)
40+
}));
41+
}
42+
} else {
43+
tracing::info!(" bad data");
44+
}
45+
}) as Box<dyn FnMut(JsValue)>);
46+
47+
Effect::new(move |_| {
48+
node.get().map(|v| match stream.get() {
49+
Some(Ok(s)) => {
50+
tracing::info!("Setting stream {s:?} to src...");
51+
v.set_src_object(Some(&s));
52+
let recorder = MediaRecorder::new_with_media_stream(&s).unwrap();
53+
recorder.set_ondataavailable(Some(on_data_available.as_ref().unchecked_ref()));
54+
recorder.start_with_time_slice(500).unwrap();
55+
}
56+
Some(Err(e)) => tracing::error!("Failed to get media stream: {e:?}"),
57+
None => tracing::debug!("No stream yet"),
58+
});
59+
});
60+
61+
// start/stop recording
2762
let _effect = Effect::watch(
2863
move || start_rec.get(),
2964
move |val, _prev, _| {
@@ -38,24 +73,42 @@ pub fn AudioStream() -> impl IntoView {
3873
true, /* Trigger it as soon as possible */
3974
);
4075

76+
// Watch async_blob
4177
Effect::new(move |_| {
42-
tracing::debug!("State of the recording: {}.", start_rec.get_untracked());
43-
node.get().map(|v| match stream.get() {
44-
Some(Ok(s)) => {
45-
tracing::debug!("Setting stream {s:?} to src...");
46-
v.set_src_object(Some(&s));
47-
},
48-
Some(Err(e)) => tracing::error!("Failed to get media stream: {e:?}"),
49-
None => tracing::debug!("No stream yet"),
50-
});
78+
let ctx = canvas_node
79+
.get()
80+
.unwrap()
81+
.get_context("2d")
82+
.unwrap()
83+
.unwrap()
84+
.dyn_into::<web_sys::CanvasRenderingContext2d>()
85+
.unwrap();
86+
87+
if let Some(blob_resource) = async_blob.get() {
88+
if let Some(Ok(data)) = blob_resource.read().as_deref() {
89+
tracing::info!("Got blob of buffer {data:?}");
90+
ctx.reset();
91+
ctx.begin_path();
92+
for (i, v) in data.iter().enumerate() {
93+
ctx.line_to(
94+
i as f64,
95+
*v as f64 / 5.0 + 50.0, /* 50 is half of height of canvas */
96+
)
97+
}
98+
ctx.stroke(); // render
99+
}
100+
}
51101
});
52102

53103
view! {
54104
<Space vertical=true>
55105
// Eventually I was to draw something related to audio stream here.
56-
<canvas />
57-
<audio node_ref=node controls />
106+
<canvas node_ref=canvas_node class=styles::canvas />
107+
<audio node_ref=node controls />
58108
<Switch checked=start_rec label="Start Record" />
109+
<div>
110+
"Record and plot every half second of data"
111+
</div>
59112
</Space>
60113
}
61114
}

leptos/src/components/qr.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ pub fn QrScanner() -> impl IntoView {
2020
active=scan
2121
on_scan=move |a| {
2222
tracing::info!("Found: {}", &a);
23-
if ! multiple.get_untracked() {
23+
if !multiple.get_untracked() {
2424
result.set(vec![a]);
2525
} else {
2626
let vals = result.read_untracked();
27-
if ! vals.contains(&a) {
27+
if !vals.contains(&a) {
2828
result.write().push(a)
2929
}
3030
}

0 commit comments

Comments
 (0)