Start work on audio handler in Rust
This commit is contained in:
parent
573b564ff5
commit
f534ca7e5d
File diff suppressed because it is too large
Load Diff
|
|
@ -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 = []
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue