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