-
Notifications
You must be signed in to change notification settings - Fork 477
feat: implement input stream support for web backends #1044
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Both backends were returning empty configs or panicking when trying to use microphone input. Now properly implements getUserMedia() with async support.
|
finally we have something for web even if it's just a draft. thank you thank you ❤️ we have been waiting this for literally years. ⛑️ |
|
@Tahinli do let me know if this works OK or better yet: take over for any fixes and enhancements as required. I don't use this feature myself, so anyone that does will be in the best position to implement it appropriately. |
PR wasn't working alone and since I'm not very good at JavaScript I got help from AI. After these fixes I'm able to get audio correctly. Some people are little sensitive about AI so I wanted to inform from the beginning.
Cargo.toml - needs to add wasm-bindgen-futures here: And update the feature:
At line 617, the processor is created with 0 output channels: Changing to 1 output channel fixes the issue.
The MediaStreamAudioSourceNode and ScriptProcessorNode are created inside the async Quick fix: A proper fix would store these in the Stream struct so they can be cleaned up when the Additional Notes
Test Code use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;
#[wasm_bindgen(start)]
pub fn main() {
console_error_panic_hook::set_once();
log("CPAL Web Input Test initialized");
}
/// Loopback test - hear your own voice!
#[wasm_bindgen]
pub fn test_loopback() {
log("Starting loopback test (hear your voice)...");
let host = cpal::default_host();
log(&format!("Host ID: {:?}", host.id()));
let device = match host.default_input_device() {
Some(d) => d,
None => {
log("No input device found!");
return;
}
};
let config = match device.default_input_config() {
Ok(c) => c,
Err(e) => {
log(&format!("No default input config: {:?}", e));
return;
}
};
log(&format!(
"Config: {} channels, {} Hz",
config.channels(),
config.sample_rate().0
));
// Shared ring buffer for audio data
let buffer_size = config.sample_rate().0 as usize; // 1 second buffer
let ring_buffer: Arc<Mutex<VecDeque<f32>>> = Arc::new(Mutex::new(VecDeque::with_capacity(buffer_size)));
let ring_buffer_input = ring_buffer.clone();
let ring_buffer_output = ring_buffer.clone();
// Build input stream
let input_stream = device.build_input_stream(
&config.clone().into(),
move |data: &[f32], _: &cpal::InputCallbackInfo| {
if let Ok(mut buf) = ring_buffer_input.try_lock() {
for &sample in data {
if buf.len() >= buffer_size {
buf.pop_front();
}
buf.push_back(sample);
}
}
},
move |err| {
web_sys::console::error_1(&format!("Input error: {:?}", err).into());
},
None,
);
// Build output stream
let output_stream = device.build_output_stream(
&config.into(),
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
if let Ok(mut buf) = ring_buffer_output.try_lock() {
for sample in data.iter_mut() {
*sample = buf.pop_front().unwrap_or(0.0);
}
} else {
// If we can't lock, output silence
for sample in data.iter_mut() {
*sample = 0.0;
}
}
},
move |err| {
web_sys::console::error_1(&format!("Output error: {:?}", err).into());
},
None,
);
match (input_stream, output_stream) {
(Ok(input), Ok(output)) => {
log("Streams created successfully!");
if let Err(e) = input.play() {
log(&format!("Failed to play input: {:?}", e));
return;
}
if let Err(e) = output.play() {
log(&format!("Failed to play output: {:?}", e));
return;
}
log("🎤➡️🔊 Loopback active! Speak into your mic...");
log("(Use headphones to avoid feedback!)");
// Keep streams alive
std::mem::forget(input);
std::mem::forget(output);
}
(Err(e), _) => log(&format!("Failed to create input stream: {:?}", e)),
(_, Err(e)) => log(&format!("Failed to create output stream: {:?}", e)),
}
}
#[wasm_bindgen]
pub fn check_webaudio() {
log("Checking WebAudio availability...");
// Check AudioContext directly via JS
let global = js_sys::global();
let audio_ctx = js_sys::Reflect::get(&global, &JsValue::from_str("AudioContext"));
log(&format!("AudioContext from global: {:?}", audio_ctx.map(|v: JsValue| v.is_truthy())));
// Check window.AudioContext
if let Some(window) = web_sys::window() {
log("window object: available");
let audio_ctx_from_window = js_sys::Reflect::get(&window, &JsValue::from_str("AudioContext"));
log(&format!("AudioContext from window: {:?}", audio_ctx_from_window.map(|v: JsValue| v.is_truthy())));
} else {
log("window object: NOT available");
}
// Try creating an AudioContext directly
match web_sys::AudioContext::new() {
Ok(ctx) => {
log(&format!("Created AudioContext: sample_rate={}", ctx.sample_rate()));
let _ = ctx.close();
}
Err(e) => {
log(&format!("Failed to create AudioContext: {:?}", e));
}
}
}
#[wasm_bindgen]
pub fn test_input_stream() {
log("Starting input stream test...");
// First check WebAudio
check_webaudio();
let host = cpal::default_host();
log(&format!("Host ID: {:?}", host.id()));
// List input devices
match host.input_devices() {
Ok(devices) => {
let devices: Vec<_> = devices.collect();
log(&format!("Found {} input device(s)", devices.len()));
for (i, device) in devices.iter().enumerate() {
if let Ok(name) = device.name() {
log(&format!(" Device {}: {}", i, name));
}
}
}
Err(e) => {
log(&format!("Error listing input devices: {:?}", e));
}
}
// Try to get default input device
match host.default_input_device() {
Some(device) => {
let name = device.name().unwrap_or_else(|_| "Unknown".to_string());
log(&format!("Default input device: {}", name));
// Get supported input configs
match device.supported_input_configs() {
Ok(configs) => {
let configs: Vec<_> = configs.collect();
log(&format!("Supported input configs: {}", configs.len()));
for (i, config) in configs.iter().enumerate() {
log(&format!(
" Config {}: channels={}, sample_rate={}-{}, format={:?}",
i,
config.channels(),
config.min_sample_rate().0,
config.max_sample_rate().0,
config.sample_format()
));
}
}
Err(e) => {
log(&format!("Error getting supported configs: {:?}", e));
}
}
// Try to get default input config
match device.default_input_config() {
Ok(config) => {
log(&format!(
"Default input config: channels={}, sample_rate={}, format={:?}",
config.channels(),
config.sample_rate().0,
config.sample_format()
));
// Try to build an input stream
log("Attempting to build input stream...");
let sample_count = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
let sample_count_clone = sample_count.clone();
let stream = device.build_input_stream(
&config.into(),
move |data: &[f32], _: &cpal::InputCallbackInfo| {
let count = sample_count_clone.fetch_add(
data.len() as u32,
std::sync::atomic::Ordering::Relaxed,
);
// Log first 10 callbacks, then every ~1 second
if count < 10 * 2048 || count % 48000 < data.len() as u32 {
let max_amplitude = data
.iter()
.map(|s| s.abs())
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or(0.0);
web_sys::console::log_1(
&format!(
"🎤 Audio data: {} samples, len={}, max amp: {:.4}",
count, data.len(), max_amplitude
)
.into(),
);
}
},
move |err| {
// Make errors very visible
web_sys::console::error_1(
&format!("❌ INPUT STREAM ERROR: {:?}", err).into(),
);
},
None,
);
match stream {
Ok(stream) => {
log("Input stream created successfully!");
if let Err(e) = stream.play() {
log(&format!("Error playing stream: {:?}", e));
} else {
log("Stream is now playing! Speak into your microphone...");
// Keep stream alive by leaking it (for testing purposes)
std::mem::forget(stream);
}
}
Err(e) => {
log(&format!("Error building input stream: {:?}", e));
}
}
}
Err(e) => {
log(&format!("Error getting default input config: {:?}", e));
}
}
}
None => {
log("No default input device found");
}
}
}
fn log(msg: &str) {
web_sys::console::log_1(&msg.into());
} |
Implements missing microphone/input support for WebAudio and Emscripten backends. Previously, calling any input-related methods would return empty results or panic.
Implementation
Uses
navigator.mediaDevices.getUserMedia()to request microphone access, then creates aMediaStreamAudioSourceNodeconnected to aScriptProcessorNodefor audio capture. The async getUserMedia Promise is handled viawasm_bindgen_futures::spawn_local(), allowing the synchronous API to return immediately while permission is requested in the background.Status
This is very preliminary and mostly untested code. Please test and report back here.