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 1a0b9fa..2d2ab1d 100644 Binary files a/src/pkg/lightningbeam_core_bg.wasm and b/src/pkg/lightningbeam_core_bg.wasm differ diff --git a/src/pkg/lightningbeam_core_bg.wasm.d.ts b/src/pkg/lightningbeam_core_bg.wasm.d.ts index c0d6b2f..7a3debd 100644 --- a/src/pkg/lightningbeam_core_bg.wasm.d.ts +++ b/src/pkg/lightningbeam_core_bg.wasm.d.ts @@ -20,5 +20,5 @@ export const __wbindgen_malloc: (a: number, b: number) => 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;