Start work on audio handler in Rust

This commit is contained in:
Skyler Lehmkuhl 2025-01-24 16:27:19 -05:00
parent 573b564ff5
commit f534ca7e5d
5 changed files with 1434 additions and 0 deletions

1054
lightningbeam-core/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
[package]
name = "lightningbeam-core"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = "0.2"
cpal = "0.14"
[dependencies.web-sys]
version = "0.3.22"
features = ["console"]
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so it's only enabled
# in debug mode.
[target."cfg(debug_assertions)".dependencies]
console_error_panic_hook = "0.1.5"
[features]
default = ["native"]
native = []
wasm = []

View File

@ -0,0 +1,105 @@
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{SampleFormat, SampleRate, Stream, StreamConfig};
use cpal::{BufferSize, SupportedBufferSize};
use std::sync::{Arc, Mutex};
use crate::AudioOutput;
pub struct CpalAudioOutput {
stream: Option<Stream>,
buffer: Arc<Mutex<Vec<f32>>>, // Shared buffer for audio chunks
sample_rate: u32,
channels: u16,
}
impl CpalAudioOutput {
pub fn new(sample_rate: u32, channels: u16) -> Self {
Self {
stream: None,
buffer: Arc::new(Mutex::new(Vec::new())),
sample_rate,
channels,
}
}
}
impl AudioOutput for CpalAudioOutput {
fn start(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let host = cpal::default_host();
let device = host
.default_output_device()
.ok_or("No output device available")?;
let supported_config = device
.default_output_config().unwrap();
// .with_sample_rate(SampleRate(self.sample_rate));
let config = StreamConfig {
channels: self.channels,
sample_rate: SampleRate(self.sample_rate),
buffer_size: match supported_config.buffer_size() {
SupportedBufferSize::Range { min, max: _ } => BufferSize::Fixed(*min),
SupportedBufferSize::Unknown => BufferSize::Default,
},
};
let buffer = self.buffer.clone();
let sample_format = supported_config.sample_format();
let stream = match sample_format {
SampleFormat::F32 => device.build_output_stream(
&config,
move |data: &mut [f32], _| {
let mut buffer = buffer.lock().unwrap();
for (out_sample, buffer_sample) in data.iter_mut().zip(buffer.iter()) {
*out_sample = *buffer_sample;
}
buffer.clear(); // Clear buffer after playback
},
move |err| {
eprintln!("Audio stream error: {:?}", err);
},
),
SampleFormat::I16 => device.build_output_stream(
&config,
move |data: &mut [i16], _| {
let mut buffer = buffer.lock().unwrap();
for (out_sample, buffer_sample) in data.iter_mut().zip(buffer.iter()) {
*out_sample = (*buffer_sample * i16::MAX as f32) as i16;
}
buffer.clear();
},
move |err| {
eprintln!("Audio stream error: {:?}", err);
},
),
SampleFormat::U16 => device.build_output_stream(
&config,
move |data: &mut [u16], _| {
let mut buffer = buffer.lock().unwrap();
for (out_sample, buffer_sample) in data.iter_mut().zip(buffer.iter()) {
*out_sample = ((*buffer_sample + 1.0) * 0.5 * u16::MAX as f32) as u16;
}
buffer.clear();
},
move |err| {
eprintln!("Audio stream error: {:?}", err);
},
),
};
// If the stream creation failed, return the error
let stream = stream.map_err(|e| {
format!(
"Failed to build output stream for sample format {:?}: {:?}",
sample_format, e
)
})?;
stream.play()?;
self.stream = Some(stream);
Ok(())
}
fn play_chunk(&mut self, chunk: Vec<f32>) {
let mut buffer = self.buffer.lock().unwrap();
buffer.extend(chunk);
}
}

View File

@ -0,0 +1,111 @@
mod time;
use time::{Timestamp, Duration, Frame};
mod audio;
use audio::{CpalAudioOutput};
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
pub trait AudioTrack {
/// Render a chunk of audio for the given timestamp and duration.
fn render_chunk(&self, timestamp: Timestamp, duration: Duration) -> Vec<f32>;
/// Get the sample rate of the audio track.
fn sample_rate(&self) -> u32;
}
pub trait VideoTrack {
/// Render a frame for the given timestamp.
fn render_frame(&self, timestamp: Timestamp) -> Frame;
/// Get the frame rate of the video track.
fn frame_rate(&self) -> f64;
}
pub struct TrackManager {
audio_tracks: Vec<Box<dyn AudioTrack>>,
video_tracks: Vec<Box<dyn VideoTrack>>,
sample_rate: u32,
frame_duration: Duration, // Duration of each frame in seconds (e.g., 1/60 for 60 FPS)
}
impl TrackManager {
pub fn new(sample_rate: u32, frame_duration: f64) -> Self {
Self {
audio_tracks: Vec::new(),
video_tracks: Vec::new(),
sample_rate,
frame_duration: Duration::new(frame_duration),
}
}
pub fn add_audio_track(&mut self, track: Box<dyn AudioTrack>) {
self.audio_tracks.push(track);
}
pub fn add_video_track(&mut self, track: Box<dyn VideoTrack>) {
self.video_tracks.push(track);
}
pub fn play(&mut self, timestamp: Timestamp, audio_output: &mut dyn AudioOutput, video_output: &mut dyn FrameTarget) {
let mut timestamp = timestamp.clone();
// Main playback loop
loop {
// Render and play audio chunks
let mut audio_mix: Vec<f32> = vec![0.0; self.frame_duration.to_samples(self.sample_rate) as usize];
for track in &mut self.audio_tracks {
let chunk = track.render_chunk(timestamp, self.frame_duration);
for (i, sample) in chunk.iter().enumerate() {
audio_mix[i] += sample; // Simple mixing (sum of samples)
}
}
audio_output.play_chunk(audio_mix);
// Render video frames
for track in &self.video_tracks {
let track_frame = track.render_frame(timestamp);
}
// Update timestamp
timestamp += self.frame_duration;
// Break condition (e.g., end of tracks)
if self.audio_tracks.iter().all(|t| t.render_chunk(timestamp, self.frame_duration).is_empty()) {
break;
}
}
}
}
pub trait AudioOutput {
fn start(&mut self) -> Result<(), Box<dyn std::error::Error>>;
fn play_chunk(&mut self, chunk: Vec<f32>);
}
pub trait FrameTarget {
fn draw(&mut self, frame: &[u8], width: u32, height: u32);
}
#[cfg(test)]
mod tests {
use super::*;
// #[test]
// fn it_works() {
// let result = add(2, 2);
// assert_eq!(result, 4);
// }
}
// This is like the `main` function, except for JavaScript.
#[cfg(feature="wasm")]
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
// This provides better error messages in debug mode.
// It's disabled in release mode so it doesn't bloat up the file size.
#[cfg(debug_assertions)]
console_error_panic_hook::set_once();
Ok(())
}

View File

@ -0,0 +1,136 @@
/// A strongly-typed representation of a timestamp (seconds).
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize)]
pub struct Timestamp(f64);
/// A strongly-typed representation of a duration (seconds).
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize)]
pub struct Duration(f64);
impl Timestamp {
/// Create a new timestamp in seconds.
pub fn new(seconds: f64) -> Self {
Timestamp(seconds)
}
/// Create a new timestamp from milliseconds.
pub fn from_millis(milliseconds: u64) -> Self {
Timestamp(milliseconds as f64 / 1000.0)
}
/// Get the value in seconds.
pub fn as_seconds(&self) -> f64 {
self.0
}
/// Get the value in milliseconds.
pub fn as_millis(&self) -> u64 {
(self.0 * 1000.0).round() as u64
}
/// Add a duration to a timestamp, producing a new timestamp.
pub fn add_duration(&self, duration: Duration) -> Timestamp {
Timestamp(self.0 + duration.0)
}
/// Subtract a duration from a timestamp, producing a new timestamp.
pub fn subtract_duration(&self, duration: Duration) -> Timestamp {
Timestamp(self.0 - duration.0)
}
/// Subtract another timestamp, producing a duration.
pub fn subtract_timestamp(&self, other: Timestamp) -> Duration {
Duration(self.0 - other.0)
}
}
impl Duration {
/// Create a new duration in seconds.
pub fn new(seconds: f64) -> Self {
Duration(seconds)
}
/// Create a new duration from milliseconds.
pub fn from_millis(milliseconds: u64) -> Self {
Duration(milliseconds as f64 / 1000.0)
}
/// Create a new duration from frames, given a frame rate.
pub fn from_frames(frames: u64, frame_rate: f64) -> Self {
Duration(frames as f64 / frame_rate)
}
/// Get the value in seconds.
pub fn as_seconds(&self) -> f64 {
self.0
}
/// Get the value in milliseconds.
pub fn as_millis(&self) -> u64 {
(self.0 * 1000.0).round() as u64
}
/// Get the number of frames for this duration, given a frame rate.
pub fn to_frames(&self, frame_rate: f64) -> u64 {
(self.0 * frame_rate).round() as u64
}
/// Get the number of samples in this duration at a given sample rate
pub fn to_samples(&self, sample_rate: u32) -> u64 {
(self.0 * sample_rate as f64).round() as u64
}
/// Add two durations together.
pub fn add(&self, other: Duration) -> Duration {
Duration(self.0 + other.0)
}
/// Subtract one duration from another.
pub fn subtract(&self, other: Duration) -> Duration {
Duration(self.0 - other.0)
}
}
// Overloading operators for more natural usage
use std::ops::{Add, Sub, AddAssign, SubAssign};
impl Add<Duration> for Timestamp {
type Output = Timestamp;
fn add(self, duration: Duration) -> Timestamp {
self.add_duration(duration)
}
}
impl Sub<Duration> for Timestamp {
type Output = Timestamp;
fn sub(self, duration: Duration) -> Timestamp {
self.subtract_duration(duration)
}
}
impl AddAssign<Duration> for Timestamp {
fn add_assign(&mut self, duration: Duration) {
self.0 += duration.0;
}
}
impl SubAssign<Duration> for Timestamp {
fn sub_assign(&mut self, duration: Duration) {
self.0 -= duration.0;
}
}
/// Represents a video frame.
#[derive(Debug, Clone)]
pub struct Frame {
pub width: u32,
pub height: u32,
pub data: Vec<u8>, // RGBA pixel data
}
impl Frame {
pub fn new(width: u32, height: u32, data: Vec<u8>) -> Self {
Frame { width, height, data }
}
}