feat(leptos): Phase 4 Task 6 - PCM audio playback + mic transmit hooks
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
pub mod ring_buffer;
|
||||
pub mod pcm_decoder;
|
||||
@@ -0,0 +1,69 @@
|
||||
/// PCM Frame decoded from binary WebSocket data
|
||||
/// Format: [u32 userId (4 bytes)][i16 samples (N bytes)]
|
||||
pub struct PcmFrame {
|
||||
pub user_id: u32,
|
||||
pub samples: Vec<f32>, // Normalized to [-1.0, 1.0]
|
||||
}
|
||||
|
||||
/// Decode a binary WebSocket message into PCM frames
|
||||
/// Returns None if data is too short or malformed
|
||||
pub fn decode_pcm_frame(data: &[u8]) -> Option<PcmFrame> {
|
||||
if data.len() < 4 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let user_id = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
|
||||
let sample_bytes = &data[4..];
|
||||
let sample_count = sample_bytes.len() / 2;
|
||||
|
||||
if sample_count == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let samples = decode_i16_samples(sample_bytes);
|
||||
Some(PcmFrame { user_id, samples })
|
||||
}
|
||||
|
||||
/// Decode raw i16 PCM bytes to normalized f32 samples [-1.0, 1.0]
|
||||
pub fn decode_i16_samples(data: &[u8]) -> Vec<f32> {
|
||||
let count = data.len() / 2;
|
||||
let mut out = Vec::with_capacity(count);
|
||||
|
||||
for i in 0..count {
|
||||
let offset = i * 2;
|
||||
if offset + 1 < data.len() {
|
||||
let sample = i16::from_le_bytes([data[offset], data[offset + 1]]);
|
||||
out.push((sample as f32) / 32768.0);
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// Encode f32 samples [-1.0, 1.0] to base64 for WebSocket transmission
|
||||
/// Uses JavaScript btoa for encoding
|
||||
pub fn encode_samples_to_base64(samples: &[f32]) -> String {
|
||||
// Convert f32 samples to i16 bytes
|
||||
let mut bytes = Vec::with_capacity(samples.len() * 2);
|
||||
for &sample in samples {
|
||||
let clamped = sample.max(-1.0).min(1.0);
|
||||
let int_sample = (clamped * 32767.0) as i16;
|
||||
bytes.extend_from_slice(&int_sample.to_le_bytes());
|
||||
}
|
||||
encode_bytes_base64(&bytes)
|
||||
}
|
||||
|
||||
/// Encode raw bytes to base64 using JavaScript's btoa
|
||||
fn encode_bytes_base64(data: &[u8]) -> String {
|
||||
// Build binary string for btoa
|
||||
let binary: String = data.iter().map(|&b| b as char).collect();
|
||||
|
||||
// Call btoa from JavaScript via js_sys::eval
|
||||
let js_code = format!("btoa('{}')", binary.replace('\'', "\\'"));
|
||||
js_sys::eval(&js_code)
|
||||
.ok()
|
||||
.and_then(|r| r.as_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
@@ -0,0 +1,124 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// AudioRingBuffer — Fixed-size circular buffer for real-time PCM streaming
|
||||
/// Provides thread-safe write/read with automatic overwrite protection
|
||||
pub struct AudioRingBuffer {
|
||||
buffer: Vec<f32>,
|
||||
capacity: usize,
|
||||
write_pos: usize,
|
||||
read_pos: usize,
|
||||
available: usize,
|
||||
}
|
||||
|
||||
impl AudioRingBuffer {
|
||||
/// Create a new ring buffer with given capacity (in samples)
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
Self {
|
||||
buffer: vec![0.0; capacity],
|
||||
capacity,
|
||||
write_pos: 0,
|
||||
read_pos: 0,
|
||||
available: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Write samples to the ring buffer. Overwrites oldest data if full.
|
||||
pub fn write(&mut self, samples: &[f32]) {
|
||||
let mut written = 0;
|
||||
while written < samples.len() {
|
||||
let chunk = (samples.len() - written).min(self.capacity - self.write_pos);
|
||||
let src = &samples[written..written + chunk];
|
||||
let dest = &mut self.buffer[self.write_pos..self.write_pos + chunk];
|
||||
dest.copy_from_slice(src);
|
||||
written += chunk;
|
||||
self.write_pos = (self.write_pos + chunk) % self.capacity;
|
||||
self.available = (self.available + chunk).min(self.capacity);
|
||||
// If we overwrote unread data, advance read_pos
|
||||
if self.available == self.capacity {
|
||||
self.read_pos = self.write_pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read up to `max_samples` from the buffer. Returns the samples read.
|
||||
pub fn read(&mut self, max_samples: usize) -> Vec<f32> {
|
||||
let to_read = max_samples.min(self.available);
|
||||
let mut out = Vec::with_capacity(to_read);
|
||||
let mut remaining = to_read;
|
||||
|
||||
while remaining > 0 {
|
||||
let chunk = remaining.min(self.capacity - self.read_pos);
|
||||
out.extend_from_slice(&self.buffer[self.read_pos..self.read_pos + chunk]);
|
||||
remaining -= chunk;
|
||||
self.read_pos = (self.read_pos + chunk) % self.capacity;
|
||||
}
|
||||
|
||||
self.available -= to_read;
|
||||
out
|
||||
}
|
||||
|
||||
/// Number of samples available to read
|
||||
pub fn available_samples(&self) -> usize {
|
||||
self.available
|
||||
}
|
||||
|
||||
/// Clear all buffered data
|
||||
pub fn clear(&mut self) {
|
||||
self.write_pos = 0;
|
||||
self.read_pos = 0;
|
||||
self.available = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread-safe wrapper around AudioRingBuffer
|
||||
pub struct SharedRingBuffer {
|
||||
inner: Arc<Mutex<AudioRingBuffer>>,
|
||||
}
|
||||
|
||||
impl SharedRingBuffer {
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(AudioRingBuffer::new(capacity))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&self, samples: &[f32]) {
|
||||
if let Ok(mut guard) = self.inner.lock() {
|
||||
guard.write(samples);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&self, max_samples: usize) -> Vec<f32> {
|
||||
if let Ok(mut guard) = self.inner.lock() {
|
||||
guard.read(max_samples)
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn available_samples(&self) -> usize {
|
||||
if let Ok(guard) = self.inner.lock() {
|
||||
guard.available_samples()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&self) {
|
||||
if let Ok(mut guard) = self.inner.lock() {
|
||||
guard.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clone_inner(&self) -> Arc<Mutex<AudioRingBuffer>> {
|
||||
self.inner.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for SharedRingBuffer {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user