Add RecordedAudioTrack

This commit is contained in:
Skyler Lehmkuhl 2025-01-29 04:41:59 -05:00
parent e12c2e8877
commit 749caa14a5
9 changed files with 800 additions and 30 deletions

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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")]
@ -34,6 +55,10 @@ pub trait Track: Send {
fn render_video(&self, _timestamp: Timestamp, _playing: bool) -> Option<Frame> {
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<f32>,
original_sample_rate: u32,
resampled_data: Vec<f32>,
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<AudioBuffer>,
target_sample_rate: Option<u32>,
}
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<f32>) {
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<Vec<f32>> {
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(&current_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<f32> {
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<dyn Error>>;
}
pub struct GenericAudioLoader;
impl AudioLoader for GenericAudioLoader {
fn load_audio(
&self,
track: &mut RecordedAudioTrack,
start_time: Timestamp,
audio_data: &[u8],
) -> Result<(), Box<dyn Error>> {
decode_audio(track, start_time, audio_data)
}
}
fn decode_audio(
track: &mut RecordedAudioTrack,
start_time: Timestamp,
audio_data: &[u8],
) -> Result<(), Box<dyn Error>> {
// 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 {
@ -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> {

View File

@ -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<usize> for SampleCount{
fn eq(&self, other: &usize) -> bool {
self.0 == *other
}
}
// Overloading operators for more natural usage
@ -138,6 +169,14 @@ impl Sub<Duration> for Timestamp {
}
}
impl Sub<Timestamp> for Timestamp {
type Output = Duration;
fn sub(self, other: Timestamp) -> Duration {
self.subtract_timestamp(other)
}
}
impl AddAssign<Duration> for Timestamp {
fn add_assign(&mut self, duration: Duration) {
self.0 += duration.0;
@ -150,6 +189,33 @@ impl SubAssign<Duration> 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<SampleCount> for SampleCount {
fn add_assign(&mut self, other: SampleCount) {
self.0 += other.0;
}
}
impl SubAssign<SampleCount> for SampleCount {
fn sub_assign(&mut self, other: SampleCount) {
self.0 -= other.0;
}
}
/// Represents a video frame.
#[derive(Debug, Clone)]
pub struct Frame {

View File

@ -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;
}

View File

@ -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) {

Binary file not shown.

View File

@ -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;