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;