From 749caa14a5ca393336d9370fdd97518f860718cc Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Wed, 29 Jan 2025 04:41:59 -0500 Subject: [PATCH] Add RecordedAudioTrack --- lightningbeam-core/Cargo.lock | 387 ++++++++++++++++++++++++ lightningbeam-core/Cargo.toml | 12 +- lightningbeam-core/build.sh | 5 +- lightningbeam-core/src/lib.rs | 350 +++++++++++++++++++-- lightningbeam-core/src/time.rs | 66 ++++ src/pkg/lightningbeam_core.d.ts | 2 +- src/pkg/lightningbeam_core.js | 6 +- src/pkg/lightningbeam_core_bg.wasm | Bin 137627 -> 137320 bytes src/pkg/lightningbeam_core_bg.wasm.d.ts | 2 +- 9 files changed, 800 insertions(+), 30 deletions(-) diff --git a/lightningbeam-core/Cargo.lock b/lightningbeam-core/Cargo.lock index d3c70be..c2984ce 100644 --- a/lightningbeam-core/Cargo.lock +++ b/lightningbeam-core/Cargo.lock @@ -39,6 +39,12 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "autocfg" version = "1.4.0" @@ -81,6 +87,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytemuck" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" + [[package]] name = "bytes" version = "1.9.0" @@ -211,12 +223,27 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + [[package]] name = "glob" version = "0.3.2" @@ -229,6 +256,12 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + [[package]] name = "indexmap" version = "2.7.1" @@ -289,6 +322,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.169" @@ -312,9 +351,15 @@ dependencies = [ "anyhow", "console_error_panic_hook", "cpal", + "hound", + "js-sys", "log", + "minimp3", + "rubato", "serde", + "symphonia", "wasm-bindgen", + "wasm-bindgen-futures", "wasm-logger", "web-sys", ] @@ -325,6 +370,15 @@ version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + [[package]] name = "mach2" version = "0.4.2" @@ -346,6 +400,26 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "minimp3" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985438f75febf74c392071a975a29641b420dd84431135a6e6db721de4b74372" +dependencies = [ + "minimp3-sys", + "slice-deque", + "thiserror", +] + +[[package]] +name = "minimp3-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e21c73734c69dc95696c9ed8926a2b393171d98b3f5f5935686a26a487ab9b90" +dependencies = [ + "cc", +] + [[package]] name = "ndk" version = "0.8.0" @@ -385,6 +459,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.4.2" @@ -396,6 +479,15 @@ dependencies = [ "syn", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -461,6 +553,15 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -489,6 +590,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "realfft" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390252372b7f2aac8360fc5e72eba10136b166d6faeed97e6d0c8324eb99b2b1" +dependencies = [ + "rustfft", +] + [[package]] name = "regex" version = "1.11.1" @@ -518,12 +628,39 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rubato" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6dd52e80cfc21894deadf554a5673002938ae4625f7a283e536f9cf7c17b0d5" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "realfft", +] + [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustfft" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43806561bc506d0c5d160643ad742e3161049ac01027b5e6d7524091fd401d86" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", + "version_check", +] + [[package]] name = "rustversion" version = "1.0.19" @@ -565,6 +702,218 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "slice-deque" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ef6ee280cdefba6d2d0b4b78a84a1c1a3f3a4cec98c2d4231c8bc225de0f25" +dependencies = [ + "libc", + "mach", + "winapi", +] + +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "symphonia" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-aac", + "symphonia-codec-adpcm", + "symphonia-codec-alac", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-caf", + "symphonia-format-isomp4", + "symphonia-format-mkv", + "symphonia-format-ogg", + "symphonia-format-riff", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-adpcm" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94e1feac3327cd616e973d5be69ad36b3945f16b06f19c6773fc3ac0b426a0f" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-alac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d8a6666649a08412906476a8b0efd9b9733e241180189e9f92b09c08d0e38f3" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-core" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-caf" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e43c99c696a388295a29fe71b133079f5d8b18041cf734c5459c35ad9097af50" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-format-isomp4" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-mkv" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb43471a100f7882dc9937395bd5ebee8329298e766250b15b3875652fe3d6f" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50" +dependencies = [ + "extended", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "syn" version = "2.0.96" @@ -613,12 +962,28 @@ dependencies = [ "winnow", ] +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + [[package]] name = "unicode-ident" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "walkdir" version = "2.5.0" @@ -721,6 +1086,22 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.9" @@ -730,6 +1111,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows" version = "0.54.0" diff --git a/lightningbeam-core/Cargo.toml b/lightningbeam-core/Cargo.toml index a6badae..ca67190 100644 --- a/lightningbeam-core/Cargo.toml +++ b/lightningbeam-core/Cargo.toml @@ -13,10 +13,20 @@ cpal = { version = "0.15", features = ["wasm-bindgen"] } anyhow = "1.0" wasm-logger = "0.2" log = "0.4" +rubato = "0.14.0" +symphonia = { version = "0.5", features = ["all"] } [dependencies.web-sys] version = "0.3.22" -features = ["console"] +features = ["console", "AudioContext"] + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +minimp3 = "0.5.1" # Only include minimp3 for native platforms +hound = "3.5.1" # Only include hound for native platforms + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen-futures = "0.4" +js-sys = "0.3" # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires diff --git a/lightningbeam-core/build.sh b/lightningbeam-core/build.sh index efac65a..4992bb3 100755 --- a/lightningbeam-core/build.sh +++ b/lightningbeam-core/build.sh @@ -1,3 +1,6 @@ #!/bin/bash -cd core +echo "Building native..." +cargo build +echo +echo "Building wasm..." wasm-pack build --target web --out-dir ../src/pkg --features wasm diff --git a/lightningbeam-core/src/lib.rs b/lightningbeam-core/src/lib.rs index 85d5020..227d597 100644 --- a/lightningbeam-core/src/lib.rs +++ b/lightningbeam-core/src/lib.rs @@ -3,8 +3,29 @@ use time::{Timestamp, Duration, Frame, SampleCount}; mod audio; use audio::{CpalAudioOutput}; use log::{Level, LevelFilter, Log, Metadata, Record, SetLoggerError}; +use rubato::{FftFixedIn, Resampler}; +use std::io::Cursor; use std::sync::{Arc, Mutex}; +use std::error::Error; + +use symphonia::core::{ + audio::AudioBufferRef, + audio::Signal, + codecs::{DecoderOptions}, + formats::{FormatOptions}, + io::MediaSourceStream, + meta::MetadataOptions, + probe::Hint, +}; + +#[cfg(not(target_arch = "wasm32"))] +use std::io; +#[cfg(not(target_arch = "wasm32"))] +use std::io::Write; + + +#[cfg(target_arch = "wasm32")] use std::fmt; #[cfg(feature = "wasm")] @@ -20,7 +41,7 @@ pub trait Track: Send { fn get_name(&self) -> &str { "Unnamed Track" } - + fn set_name(&mut self, _name: String) { } /// Render audio for the given timestamp and duration. @@ -34,6 +55,10 @@ pub trait Track: Send { fn render_video(&self, _timestamp: Timestamp, _playing: bool) -> Option { None } + + // Set the sample rate of any audio this track might contain + fn set_sample_rate(&mut self, _sample_rate: u32) { + } } pub struct TrackManager { @@ -154,34 +179,288 @@ impl Track for SineWaveTrack { } } +#[derive(Debug, Clone)] +struct AudioBuffer { + original_data: Vec, + original_sample_rate: u32, + resampled_data: Vec, + start_time: Timestamp, +} + +impl AudioBuffer { + fn duration(&self) -> Duration { + if self.resampled_data.is_empty() { + Duration::from_seconds(0.0) + } else { + Duration::from_seconds( + self.resampled_data.len() as f64 / + self.original_sample_rate as f64 + ) + } + } +} + + +pub struct RecordedAudioTrack { + name: String, + buffers: Vec, + target_sample_rate: Option, +} + + +impl RecordedAudioTrack { + pub fn new(name: &str) -> Self { + Self { + name: name.to_string(), + buffers: Vec::new(), + target_sample_rate: None, + } + } + + pub fn add_buffer(&mut self, start_time: Timestamp, sample_rate: u32, data: Vec) { + let resampled_data = match self.target_sample_rate { + Some(target_rate) if sample_rate != target_rate => + self::resample(&data, sample_rate, target_rate), + Some(_target_rate) => + data.clone(), // Already at target rate + None => + Vec::new(), // Will be resampled later + }; + + self.buffers.push(AudioBuffer { + original_data: data, + original_sample_rate: sample_rate, + resampled_data, + start_time, + }); + + // Keep buffers sorted by start time + self.buffers.sort_by(|a, b| a.start_time.partial_cmp(&b.start_time).unwrap()); + } +} + +impl Track for RecordedAudioTrack { + fn get_name(&self) -> &str { + &self.name + } + fn set_sample_rate(&mut self, target_rate: u32) { + self.target_sample_rate = Some(target_rate); + + for buffer in &mut self.buffers { + if buffer.original_sample_rate == target_rate { + buffer.resampled_data = buffer.original_data.clone(); + } else { + buffer.resampled_data = self::resample( + &buffer.original_data, + buffer.original_sample_rate, + target_rate + ); + } + } + } + + fn render_audio( + &mut self, + timestamp: Timestamp, + duration: SampleCount, + sample_rate: u32, + playing: bool, + ) -> Option> { + if !playing || self.target_sample_rate != Some(sample_rate) { + return Some(vec![0.0; duration.as_usize()]); + } + + // let chunk_samples = duration.as_usize(); + let mut output = vec![0.0; duration.as_usize()]; + let mut remaining_samples = duration; + let mut current_time = timestamp; + + // Find the first buffer that overlaps with the requested time + let mut buffer_index = match self.buffers.binary_search_by(|b| { + b.start_time.partial_cmp(¤t_time).unwrap() + }) { + Ok(i) => i, + Err(i) if i > 0 => i - 1, // Check previous buffer if timestamp is between buffers + _ => 0, + }; + + while remaining_samples.as_usize() > 0 && buffer_index < self.buffers.len() { + let buffer = &self.buffers[buffer_index]; + + // Calculate overlap with current buffer + let buffer_start = buffer.start_time; + let buffer_end = buffer_start + buffer.duration(); + + if current_time >= buffer_end { + // Move to next buffer + buffer_index += 1; + continue; + } + + // Calculate how many samples we can take from this buffer + let buffer_offset = ((current_time - buffer_start).as_seconds() * sample_rate as f64) as usize; + let available_samples = SampleCount::new(buffer.resampled_data.len().saturating_sub(buffer_offset)); + let samples_to_take = remaining_samples.min(available_samples); + + if samples_to_take == 0 { + // No more samples in this buffer + buffer_index += 1; + continue; + } + + // Copy samples from buffer to output + let output_offset = duration - remaining_samples; + output[output_offset.as_usize()..(output_offset + samples_to_take).as_usize()] + .copy_from_slice(&buffer.resampled_data[buffer_offset..buffer_offset + samples_to_take.as_usize()]); + + // Update state + remaining_samples -= samples_to_take; + current_time += samples_to_take.to_duration(sample_rate); + } + + Some(output) + } +} + +fn resample(input: &[f32], input_rate: u32, output_rate: u32) -> Vec { + if input_rate == output_rate { + return input.to_vec(); + } + + let input_rate = input_rate.try_into().unwrap(); + let output_rate = output_rate.try_into().unwrap(); + let chunk_size = input.len(); + + let mut resampler = FftFixedIn::new( + output_rate, + input_rate, + chunk_size, + 1, // channel count + 2, // fft size + ).unwrap(); + + let output = resampler.process(&[input], None).unwrap(); + output[0].clone() +} + +pub trait AudioLoader { + fn load_audio( + &self, + track: &mut RecordedAudioTrack, + start_time: Timestamp, + audio_data: &[u8], + ) -> Result<(), Box>; +} + +pub struct GenericAudioLoader; + +impl AudioLoader for GenericAudioLoader { + fn load_audio( + &self, + track: &mut RecordedAudioTrack, + start_time: Timestamp, + audio_data: &[u8], + ) -> Result<(), Box> { + decode_audio(track, start_time, audio_data) + } +} + +fn decode_audio( + track: &mut RecordedAudioTrack, + start_time: Timestamp, + audio_data: &[u8], +) -> Result<(), Box> { + // Create a media source from the byte slice + let mss = MediaSourceStream::new( + Box::new(Cursor::new(audio_data.to_vec())), + Default::default(), + ); + + // Use a fresh hint (no extension specified) for format detection + let hint = Hint::new(); + + // Probe the media source for a supported format + let probed = symphonia::default::get_probe() + .format(&hint, mss, &FormatOptions::default(), &MetadataOptions::default())?; + + // Get the format reader + let mut format = probed.format; + + // Find the first supported audio track + let default_track = format + .tracks() + .iter() + .find(|t| t.codec_params.codec != symphonia::core::codecs::CODEC_TYPE_NULL) + .ok_or("No supported audio track found")?; + + // Create a decoder for the track + let mut decoder = symphonia::default::get_codecs() + .make(&default_track.codec_params, &DecoderOptions::default())?; + + // Get the sample rate from the track + let sample_rate = default_track.codec_params.sample_rate.ok_or("Unknown sample rate")?; + let mut decoded_samples = Vec::new(); + + // Decode loop + loop { + let packet = match format.next_packet() { + Ok(packet) => packet, + Err(_) => break, // End of stream + }; + + match decoder.decode(&packet)? { + AudioBufferRef::F32(buf) => { + for i in 0..buf.frames() { + for c in 0..buf.spec().channels.count() { + decoded_samples.push(buf.chan(c)[i]); + } + } + } + AudioBufferRef::S16(buf) => { + for i in 0..buf.frames() { + for c in 0..buf.spec().channels.count() { + decoded_samples.push(buf.chan(c)[i] as f32 / 32768.0); + } + } + } + _ => return Err("Unsupported audio format".into()), + } + } + + // Add the decoded audio to the track + track.add_buffer(start_time, sample_rate, decoded_samples); + + Ok(()) +} + #[cfg(feature="wasm")] #[wasm_bindgen] pub struct JsTrack { - name: String, + name: String, } #[cfg(feature="wasm")] #[wasm_bindgen] impl JsTrack { - #[wasm_bindgen(getter)] - pub fn name(&self) -> String { - self.name.clone() - } + #[wasm_bindgen(getter)] + pub fn name(&self) -> String { + self.name.clone() + } } #[cfg(feature="wasm")] impl fmt::Display for JsTrack { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "JsTrack {{ name: {} }}", self.name) + write!(f, "JsTrack {{ name: {} }}", self.name) } } #[cfg(feature="wasm")] #[wasm_bindgen] impl JsTrack { - #[wasm_bindgen(js_name = toString)] - pub fn to_string(&self) -> String { - format!("{}", self) // Calls the Display implementation - } + #[wasm_bindgen(js_name = toString)] + pub fn to_string(&self) -> String { + format!("{}", self) // Calls the Display implementation + } } #[cfg(feature="wasm")] @@ -222,7 +501,7 @@ impl CoreInterface { pub fn resume_audio(&mut self) -> Result<(), JsValue> { // Call this on user gestures if audio gets suspended self.cpal_audio_output.resume() - .map_err(|e| JsValue::from_str(&format!("Failed to resume audio: {}", e))) + .map_err(|e| JsValue::from_str(&format!("Failed to resume audio: {}", e))) } pub fn add_sine_track(&mut self, frequency: f32) -> Result<(), String> { if frequency.is_nan() || frequency.is_infinite() || frequency <= 0.0 { @@ -232,10 +511,10 @@ impl CoreInterface { let mut track_manager = self.track_manager.lock().unwrap(); let sine_track = SineWaveTrack::new(frequency); track_manager.add_track(Box::new(sine_track)); - + Ok(()) } - + pub fn get_timestamp(&mut self) -> f64 { self.cpal_audio_output.get_timestamp().as_seconds() } @@ -245,7 +524,7 @@ impl CoreInterface { tracks .iter() .map(|track| JsTrack { - name: track.get_name().to_string(), + name: track.get_name().to_string(), }) .collect() } @@ -261,17 +540,42 @@ impl Log for PlainTextLogger { fn log(&self, record: &Record) { if self.enabled(record.metadata()) { - console::log_1(&format!( - "{} [{}:{}] {}", - record.level(), - record.file().unwrap_or("unknown"), - record.line().unwrap_or(0), - record.args() - ).into()); + #[cfg(target_arch = "wasm32")] + { + // WASM: Log to the JS console + console::log_1( + &format!( + "{} [{}:{}] {}", + record.level(), + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.args() + ) + .into(), + ); + } + + #[cfg(not(target_arch = "wasm32"))] + { + // Native: Log to stderr + let _ = writeln!( + io::stderr(), + "{} [{}:{}] {}", + record.level(), + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.args() + ); + } } } - fn flush(&self) {} + fn flush(&self) { + #[cfg(not(target_arch = "wasm32"))] + { + let _ = io::stderr().flush(); + } + } } pub fn init_plain_text_logger() -> Result<(), SetLoggerError> { diff --git a/lightningbeam-core/src/time.rs b/lightningbeam-core/src/time.rs index c014ced..7458686 100644 --- a/lightningbeam-core/src/time.rs +++ b/lightningbeam-core/src/time.rs @@ -54,6 +54,14 @@ impl Timestamp { pub fn set(&mut self, other: Timestamp) { self.0 = other.as_seconds(); } + + pub fn max(&self, other: Timestamp) -> Timestamp { + Timestamp(self.0.max(other.0)) + } + + pub fn min(&self, other: Timestamp) -> Timestamp { + Timestamp(self.0.min(other.0)) + } } impl Duration { @@ -62,6 +70,11 @@ impl Duration { Duration(seconds) } + /// Create a new duration from seconds. (dummy method) + pub fn from_seconds(seconds: f64) -> Self { + Duration(seconds) + } + /// Create a new duration from milliseconds. pub fn from_millis(milliseconds: u64) -> Self { Duration(milliseconds as f64 / 1000.0) @@ -117,6 +130,24 @@ impl SampleCount { pub fn as_usize(&self) -> usize { self.0 } + + pub fn to_duration(&self, sample_rate: u32) -> Duration { + Duration((self.0 as f64) / (sample_rate as f64)) + } + + pub fn max(&self, other: SampleCount) -> SampleCount { + SampleCount(self.0.max(other.0)) + } + + pub fn min(&self, other: SampleCount) -> SampleCount { + SampleCount(self.0.min(other.0)) + } +} + +impl PartialEq for SampleCount{ + fn eq(&self, other: &usize) -> bool { + self.0 == *other + } } // Overloading operators for more natural usage @@ -138,6 +169,14 @@ impl Sub for Timestamp { } } +impl Sub for Timestamp { + type Output = Duration; + + fn sub(self, other: Timestamp) -> Duration { + self.subtract_timestamp(other) + } +} + impl AddAssign for Timestamp { fn add_assign(&mut self, duration: Duration) { self.0 += duration.0; @@ -150,6 +189,33 @@ impl SubAssign for Timestamp { } } + +impl Add for SampleCount { + type Output = SampleCount; + fn add(self, other: SampleCount) -> SampleCount { + SampleCount(self.0 + other.0) + } +} + +impl Sub for SampleCount { + type Output = SampleCount; + fn sub(self, other: SampleCount) -> SampleCount { + SampleCount(self.0 - other.0) + } +} + +impl AddAssign for SampleCount { + fn add_assign(&mut self, other: SampleCount) { + self.0 += other.0; + } +} + +impl SubAssign for SampleCount { + fn sub_assign(&mut self, other: SampleCount) { + self.0 -= other.0; + } +} + /// Represents a video frame. #[derive(Debug, Clone)] pub struct Frame { diff --git a/src/pkg/lightningbeam_core.d.ts b/src/pkg/lightningbeam_core.d.ts index 34d7254..3b8c0e8 100644 --- a/src/pkg/lightningbeam_core.d.ts +++ b/src/pkg/lightningbeam_core.d.ts @@ -42,7 +42,7 @@ export interface InitOutput { readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; readonly __wbindgen_add_to_stack_pointer: (a: number) => number; readonly __wbindgen_free: (a: number, b: number, c: number) => void; - readonly _dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h03a328ab39659ec3: (a: number, b: number) => void; + readonly _dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hfe92e343adbc229b: (a: number, b: number) => void; readonly __wbindgen_start: () => void; } diff --git a/src/pkg/lightningbeam_core.js b/src/pkg/lightningbeam_core.js index cc982c5..1df0b89 100644 --- a/src/pkg/lightningbeam_core.js +++ b/src/pkg/lightningbeam_core.js @@ -250,7 +250,7 @@ export function main_js() { } function __wbg_adapter_18(arg0, arg1) { - wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h03a328ab39659ec3(arg0, arg1); + wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hfe92e343adbc229b(arg0, arg1); } const CoreInterfaceFinalization = (typeof FinalizationRegistry === 'undefined') @@ -562,8 +562,8 @@ function __wbg_get_imports() { const ret = false; return ret; }; - imports.wbg.__wbindgen_closure_wrapper87 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 31, __wbg_adapter_18); + imports.wbg.__wbindgen_closure_wrapper151 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 73, __wbg_adapter_18); return addHeapObject(ret); }; imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { diff --git a/src/pkg/lightningbeam_core_bg.wasm b/src/pkg/lightningbeam_core_bg.wasm index 1a0b9fad1e0042ae6a68d1084b2083f7d7b332d8..2d2ab1d3a5a35baef4c0bff45a64f17356355b42 100644 GIT binary patch delta 20790 zcmch9dt6o3691lc&f#?qdQep4A^QL#iVsv&R8+R5y?myom0FrsqWMa#ZV%9Wkg1Uy zU07IJR+MDep^hfqFwwYIEG;TCD=RC#KEk3xqx^km@3W6b>)zk*uOEJfwPx0=$DTE7 z*39gUPkX~ol!t91I;j&~rB9@``qdQUT}YqUDM23=5^L+;tGj-8h{sMs5&G(oL`OK; z6h+@2(lK4J+3X6n4G)V@ln@}oL5gDIiek6fY$Qks#Su%*NYNdk{Zq;M0T)*lumc

BJ42Hq$YBqqxl}_n`r6P}X^Nf`7EO2PZSBX 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 nXXo#iokOZ{3u9P@7Fj9qCs%Y5J%Y3 number; export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; export const __wbindgen_add_to_stack_pointer: (a: number) => number; export const __wbindgen_free: (a: number, b: number, c: number) => void; -export const _dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h03a328ab39659ec3: (a: number, b: number) => void; +export const _dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hfe92e343adbc229b: (a: number, b: number) => void; export const __wbindgen_start: () => void;