Work on daw backend
This commit is contained in:
parent
87d2036f07
commit
9414bdcd74
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "daw-backend"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cpal = "0.15"
|
||||||
|
symphonia = { version = "0.5", features = ["all"] }
|
||||||
|
rtrb = "0.3"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 3
|
||||||
|
lto = true
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 1 # Faster compile times while still reasonable performance
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,49 @@
|
||||||
|
/// Clip ID type
|
||||||
|
pub type ClipId = u32;
|
||||||
|
|
||||||
|
/// Audio clip that references data in the AudioPool
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Clip {
|
||||||
|
pub id: ClipId,
|
||||||
|
pub audio_pool_index: usize,
|
||||||
|
pub start_time: f64, // Position on timeline in seconds
|
||||||
|
pub duration: f64, // Clip duration in seconds
|
||||||
|
pub offset: f64, // Offset into audio file in seconds
|
||||||
|
pub gain: f32, // Clip-level gain
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clip {
|
||||||
|
/// Create a new clip
|
||||||
|
pub fn new(
|
||||||
|
id: ClipId,
|
||||||
|
audio_pool_index: usize,
|
||||||
|
start_time: f64,
|
||||||
|
duration: f64,
|
||||||
|
offset: f64,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
audio_pool_index,
|
||||||
|
start_time,
|
||||||
|
duration,
|
||||||
|
offset,
|
||||||
|
gain: 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this clip is active at a given timeline position
|
||||||
|
pub fn is_active_at(&self, time_seconds: f64) -> bool {
|
||||||
|
let clip_end = self.start_time + self.duration;
|
||||||
|
time_seconds >= self.start_time && time_seconds < clip_end
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the end time of this clip on the timeline
|
||||||
|
pub fn end_time(&self) -> f64 {
|
||||||
|
self.start_time + self.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set clip gain
|
||||||
|
pub fn set_gain(&mut self, gain: f32) {
|
||||||
|
self.gain = gain.max(0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,269 @@
|
||||||
|
use crate::audio::clip::ClipId;
|
||||||
|
use crate::audio::pool::AudioPool;
|
||||||
|
use crate::audio::track::{Track, TrackId};
|
||||||
|
use crate::command::{AudioEvent, Command};
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Audio engine for Phase 4: timeline with clips and audio pool
|
||||||
|
pub struct Engine {
|
||||||
|
tracks: Vec<Track>,
|
||||||
|
audio_pool: AudioPool,
|
||||||
|
playhead: u64, // Playhead position in samples
|
||||||
|
sample_rate: u32,
|
||||||
|
playing: bool,
|
||||||
|
channels: u32,
|
||||||
|
|
||||||
|
// Lock-free communication
|
||||||
|
command_rx: rtrb::Consumer<Command>,
|
||||||
|
event_tx: rtrb::Producer<AudioEvent>,
|
||||||
|
|
||||||
|
// Shared playhead for UI reads
|
||||||
|
playhead_atomic: Arc<AtomicU64>,
|
||||||
|
|
||||||
|
// Event counter for periodic position updates
|
||||||
|
frames_since_last_event: usize,
|
||||||
|
event_interval_frames: usize,
|
||||||
|
|
||||||
|
// Mix buffer for combining tracks
|
||||||
|
mix_buffer: Vec<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Engine {
|
||||||
|
/// Create a new Engine with communication channels
|
||||||
|
pub fn new(
|
||||||
|
sample_rate: u32,
|
||||||
|
channels: u32,
|
||||||
|
command_rx: rtrb::Consumer<Command>,
|
||||||
|
event_tx: rtrb::Producer<AudioEvent>,
|
||||||
|
) -> Self {
|
||||||
|
let event_interval_frames = (sample_rate as usize * channels as usize) / 10; // Update 10 times per second
|
||||||
|
|
||||||
|
Self {
|
||||||
|
tracks: Vec::new(),
|
||||||
|
audio_pool: AudioPool::new(),
|
||||||
|
playhead: 0,
|
||||||
|
sample_rate,
|
||||||
|
playing: false,
|
||||||
|
channels,
|
||||||
|
command_rx,
|
||||||
|
event_tx,
|
||||||
|
playhead_atomic: Arc::new(AtomicU64::new(0)),
|
||||||
|
frames_since_last_event: 0,
|
||||||
|
event_interval_frames,
|
||||||
|
mix_buffer: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a track to the engine
|
||||||
|
pub fn add_track(&mut self, track: Track) -> TrackId {
|
||||||
|
let id = track.id;
|
||||||
|
self.tracks.push(track);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get mutable reference to audio pool
|
||||||
|
pub fn audio_pool_mut(&mut self) -> &mut AudioPool {
|
||||||
|
&mut self.audio_pool
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get reference to audio pool
|
||||||
|
pub fn audio_pool(&self) -> &AudioPool {
|
||||||
|
&self.audio_pool
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a handle for controlling playback from the UI thread
|
||||||
|
pub fn get_controller(&self, command_tx: rtrb::Producer<Command>) -> EngineController {
|
||||||
|
EngineController {
|
||||||
|
command_tx,
|
||||||
|
playhead: Arc::clone(&self.playhead_atomic),
|
||||||
|
sample_rate: self.sample_rate,
|
||||||
|
channels: self.channels,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process audio callback - called from the audio thread
|
||||||
|
pub fn process(&mut self, output: &mut [f32]) {
|
||||||
|
// Process all pending commands
|
||||||
|
while let Ok(cmd) = self.command_rx.pop() {
|
||||||
|
self.handle_command(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.playing {
|
||||||
|
// Ensure mix buffer is sized correctly
|
||||||
|
if self.mix_buffer.len() != output.len() {
|
||||||
|
self.mix_buffer.resize(output.len(), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear mix buffer
|
||||||
|
self.mix_buffer.fill(0.0);
|
||||||
|
|
||||||
|
// Convert playhead from samples to seconds for timeline-based rendering
|
||||||
|
let playhead_seconds = self.playhead as f64 / (self.sample_rate as f64 * self.channels as f64);
|
||||||
|
|
||||||
|
// Check if any track is soloed
|
||||||
|
let any_solo = self.tracks.iter().any(|t| t.solo);
|
||||||
|
|
||||||
|
// Mix all active tracks using timeline-based rendering
|
||||||
|
for track in &self.tracks {
|
||||||
|
if track.is_active(any_solo) {
|
||||||
|
track.render(
|
||||||
|
&mut self.mix_buffer,
|
||||||
|
&self.audio_pool,
|
||||||
|
playhead_seconds,
|
||||||
|
self.sample_rate,
|
||||||
|
self.channels,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy mix to output
|
||||||
|
output.copy_from_slice(&self.mix_buffer);
|
||||||
|
|
||||||
|
// Update playhead
|
||||||
|
self.playhead += output.len() as u64;
|
||||||
|
|
||||||
|
// Update atomic playhead for UI reads
|
||||||
|
self.playhead_atomic
|
||||||
|
.store(self.playhead, Ordering::Relaxed);
|
||||||
|
|
||||||
|
// Send periodic position updates
|
||||||
|
self.frames_since_last_event += output.len() / self.channels as usize;
|
||||||
|
if self.frames_since_last_event >= self.event_interval_frames / self.channels as usize
|
||||||
|
{
|
||||||
|
let position_seconds =
|
||||||
|
self.playhead as f64 / (self.sample_rate as f64 * self.channels as f64);
|
||||||
|
let _ = self
|
||||||
|
.event_tx
|
||||||
|
.push(AudioEvent::PlaybackPosition(position_seconds));
|
||||||
|
self.frames_since_last_event = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not playing, output silence
|
||||||
|
output.fill(0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle a command from the UI thread
|
||||||
|
fn handle_command(&mut self, cmd: Command) {
|
||||||
|
match cmd {
|
||||||
|
Command::Play => {
|
||||||
|
self.playing = true;
|
||||||
|
}
|
||||||
|
Command::Stop => {
|
||||||
|
self.playing = false;
|
||||||
|
self.playhead = 0;
|
||||||
|
self.playhead_atomic.store(0, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
Command::Pause => {
|
||||||
|
self.playing = false;
|
||||||
|
}
|
||||||
|
Command::Seek(seconds) => {
|
||||||
|
let samples = (seconds * self.sample_rate as f64 * self.channels as f64) as u64;
|
||||||
|
self.playhead = samples;
|
||||||
|
self.playhead_atomic
|
||||||
|
.store(self.playhead, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
Command::SetTrackVolume(track_id, volume) => {
|
||||||
|
if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
|
||||||
|
track.set_volume(volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::SetTrackMute(track_id, muted) => {
|
||||||
|
if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
|
||||||
|
track.set_muted(muted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::SetTrackSolo(track_id, solo) => {
|
||||||
|
if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
|
||||||
|
track.set_solo(solo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::MoveClip(track_id, clip_id, new_start_time) => {
|
||||||
|
if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
|
||||||
|
if let Some(clip) = track.clips.iter_mut().find(|c| c.id == clip_id) {
|
||||||
|
clip.start_time = new_start_time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current sample rate
|
||||||
|
pub fn sample_rate(&self) -> u32 {
|
||||||
|
self.sample_rate
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get number of channels
|
||||||
|
pub fn channels(&self) -> u32 {
|
||||||
|
self.channels
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get number of tracks
|
||||||
|
pub fn track_count(&self) -> usize {
|
||||||
|
self.tracks.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Controller for the engine that can be used from the UI thread
|
||||||
|
pub struct EngineController {
|
||||||
|
command_tx: rtrb::Producer<Command>,
|
||||||
|
playhead: Arc<AtomicU64>,
|
||||||
|
sample_rate: u32,
|
||||||
|
channels: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EngineController {
|
||||||
|
/// Start or resume playback
|
||||||
|
pub fn play(&mut self) {
|
||||||
|
let _ = self.command_tx.push(Command::Play);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pause playback
|
||||||
|
pub fn pause(&mut self) {
|
||||||
|
let _ = self.command_tx.push(Command::Pause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop playback and reset to beginning
|
||||||
|
pub fn stop(&mut self) {
|
||||||
|
let _ = self.command_tx.push(Command::Stop);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Seek to a specific position in seconds
|
||||||
|
pub fn seek(&mut self, seconds: f64) {
|
||||||
|
let _ = self.command_tx.push(Command::Seek(seconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set track volume (0.0 = silence, 1.0 = unity gain)
|
||||||
|
pub fn set_track_volume(&mut self, track_id: TrackId, volume: f32) {
|
||||||
|
let _ = self
|
||||||
|
.command_tx
|
||||||
|
.push(Command::SetTrackVolume(track_id, volume));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set track mute state
|
||||||
|
pub fn set_track_mute(&mut self, track_id: TrackId, muted: bool) {
|
||||||
|
let _ = self.command_tx.push(Command::SetTrackMute(track_id, muted));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set track solo state
|
||||||
|
pub fn set_track_solo(&mut self, track_id: TrackId, solo: bool) {
|
||||||
|
let _ = self.command_tx.push(Command::SetTrackSolo(track_id, solo));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move a clip to a new timeline position
|
||||||
|
pub fn move_clip(&mut self, track_id: TrackId, clip_id: ClipId, new_start_time: f64) {
|
||||||
|
let _ = self.command_tx.push(Command::MoveClip(track_id, clip_id, new_start_time));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current playhead position in samples
|
||||||
|
pub fn get_playhead_samples(&self) -> u64 {
|
||||||
|
self.playhead.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current playhead position in seconds
|
||||||
|
pub fn get_playhead_seconds(&self) -> f64 {
|
||||||
|
let samples = self.playhead.load(Ordering::Relaxed);
|
||||||
|
samples as f64 / (self.sample_rate as f64 * self.channels as f64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
pub mod clip;
|
||||||
|
pub mod engine;
|
||||||
|
pub mod pool;
|
||||||
|
pub mod track;
|
||||||
|
|
||||||
|
pub use clip::{Clip, ClipId};
|
||||||
|
pub use engine::{Engine, EngineController};
|
||||||
|
pub use pool::{AudioFile as PoolAudioFile, AudioPool};
|
||||||
|
pub use track::{Track, TrackId};
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Audio file stored in the pool
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AudioFile {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub data: Vec<f32>, // Interleaved samples
|
||||||
|
pub channels: u32,
|
||||||
|
pub sample_rate: u32,
|
||||||
|
pub frames: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioFile {
|
||||||
|
/// Create a new AudioFile
|
||||||
|
pub fn new(path: PathBuf, data: Vec<f32>, channels: u32, sample_rate: u32) -> Self {
|
||||||
|
let frames = (data.len() / channels as usize) as u64;
|
||||||
|
Self {
|
||||||
|
path,
|
||||||
|
data,
|
||||||
|
channels,
|
||||||
|
sample_rate,
|
||||||
|
frames,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get duration in seconds
|
||||||
|
pub fn duration_seconds(&self) -> f64 {
|
||||||
|
self.frames as f64 / self.sample_rate as f64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pool of shared audio files
|
||||||
|
pub struct AudioPool {
|
||||||
|
files: Vec<AudioFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioPool {
|
||||||
|
/// Create a new empty audio pool
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
files: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an audio file to the pool and return its index
|
||||||
|
pub fn add_file(&mut self, file: AudioFile) -> usize {
|
||||||
|
let index = self.files.len();
|
||||||
|
self.files.push(file);
|
||||||
|
index
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an audio file by index
|
||||||
|
pub fn get_file(&self, index: usize) -> Option<&AudioFile> {
|
||||||
|
self.files.get(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get number of files in the pool
|
||||||
|
pub fn file_count(&self) -> usize {
|
||||||
|
self.files.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render audio from a file in the pool with sample rate and channel conversion
|
||||||
|
/// start_time_seconds: position in the audio file to start reading from (in seconds)
|
||||||
|
/// Returns the number of samples actually rendered
|
||||||
|
pub fn render_from_file(
|
||||||
|
&self,
|
||||||
|
pool_index: usize,
|
||||||
|
output: &mut [f32],
|
||||||
|
start_time_seconds: f64,
|
||||||
|
gain: f32,
|
||||||
|
engine_sample_rate: u32,
|
||||||
|
engine_channels: u32,
|
||||||
|
) -> usize {
|
||||||
|
if let Some(audio_file) = self.files.get(pool_index) {
|
||||||
|
// Calculate starting frame position in the source file (frame = one sample per channel)
|
||||||
|
let src_start_frame = start_time_seconds * audio_file.sample_rate as f64;
|
||||||
|
|
||||||
|
// Calculate sample rate conversion ratio (frames)
|
||||||
|
let rate_ratio = audio_file.sample_rate as f64 / engine_sample_rate as f64;
|
||||||
|
|
||||||
|
let src_channels = audio_file.channels;
|
||||||
|
let dst_channels = engine_channels;
|
||||||
|
|
||||||
|
// Render frame by frame
|
||||||
|
let output_frames = output.len() / dst_channels as usize;
|
||||||
|
let mut rendered_frames = 0;
|
||||||
|
|
||||||
|
for frame_idx in 0..output_frames {
|
||||||
|
// Calculate the corresponding frame in the source file
|
||||||
|
let src_frame_pos = src_start_frame + (frame_idx as f64 * rate_ratio);
|
||||||
|
let src_frame_idx = src_frame_pos as usize;
|
||||||
|
|
||||||
|
// Check bounds
|
||||||
|
if src_frame_idx >= audio_file.frames as usize {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate source sample index (interleaved)
|
||||||
|
let src_sample_idx = src_frame_idx * src_channels as usize;
|
||||||
|
|
||||||
|
// Check bounds for interpolation
|
||||||
|
if src_sample_idx + src_channels as usize > audio_file.data.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linear interpolation for better quality
|
||||||
|
let frac = src_frame_pos - src_frame_idx as f64;
|
||||||
|
let next_frame_idx = src_frame_idx + 1;
|
||||||
|
let next_sample_idx = next_frame_idx * src_channels as usize;
|
||||||
|
let can_interpolate = next_sample_idx + src_channels as usize <= audio_file.data.len() && frac > 0.0;
|
||||||
|
|
||||||
|
// Read and convert channels
|
||||||
|
for dst_ch in 0..dst_channels {
|
||||||
|
let sample = if src_channels == dst_channels {
|
||||||
|
// Same number of channels - direct mapping
|
||||||
|
let ch = dst_ch as usize;
|
||||||
|
let s0 = audio_file.data[src_sample_idx + ch];
|
||||||
|
if can_interpolate {
|
||||||
|
let s1 = audio_file.data[next_sample_idx + ch];
|
||||||
|
s0 + (s1 - s0) * frac as f32
|
||||||
|
} else {
|
||||||
|
s0
|
||||||
|
}
|
||||||
|
} else if src_channels == 1 && dst_channels > 1 {
|
||||||
|
// Mono to multi-channel - duplicate to all channels
|
||||||
|
let s0 = audio_file.data[src_sample_idx];
|
||||||
|
if can_interpolate {
|
||||||
|
let s1 = audio_file.data[next_sample_idx];
|
||||||
|
s0 + (s1 - s0) * frac as f32
|
||||||
|
} else {
|
||||||
|
s0
|
||||||
|
}
|
||||||
|
} else if src_channels > 1 && dst_channels == 1 {
|
||||||
|
// Multi-channel to mono - average all source channels
|
||||||
|
let mut sum = 0.0f32;
|
||||||
|
for src_ch in 0..src_channels {
|
||||||
|
let s0 = audio_file.data[src_sample_idx + src_ch as usize];
|
||||||
|
let s = if can_interpolate {
|
||||||
|
let s1 = audio_file.data[next_sample_idx + src_ch as usize];
|
||||||
|
s0 + (s1 - s0) * frac as f32
|
||||||
|
} else {
|
||||||
|
s0
|
||||||
|
};
|
||||||
|
sum += s;
|
||||||
|
}
|
||||||
|
sum / src_channels as f32
|
||||||
|
} else {
|
||||||
|
// Mismatched channels - use modulo for simple mapping
|
||||||
|
let src_ch = (dst_ch % src_channels) as usize;
|
||||||
|
let s0 = audio_file.data[src_sample_idx + src_ch];
|
||||||
|
if can_interpolate {
|
||||||
|
let s1 = audio_file.data[next_sample_idx + src_ch];
|
||||||
|
s0 + (s1 - s0) * frac as f32
|
||||||
|
} else {
|
||||||
|
s0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mix into output with gain
|
||||||
|
let output_idx = frame_idx * dst_channels as usize + dst_ch as usize;
|
||||||
|
output[output_idx] += sample * gain;
|
||||||
|
}
|
||||||
|
|
||||||
|
rendered_frames += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
rendered_frames * dst_channels as usize
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AudioPool {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
use super::clip::Clip;
|
||||||
|
use super::pool::AudioPool;
|
||||||
|
|
||||||
|
/// Track ID type
|
||||||
|
pub type TrackId = u32;
|
||||||
|
|
||||||
|
/// Audio track for Phase 4 with clips
|
||||||
|
pub struct Track {
|
||||||
|
pub id: TrackId,
|
||||||
|
pub name: String,
|
||||||
|
pub clips: Vec<Clip>,
|
||||||
|
pub volume: f32,
|
||||||
|
pub muted: bool,
|
||||||
|
pub solo: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Track {
|
||||||
|
/// Create a new track with default settings
|
||||||
|
pub fn new(id: TrackId, name: String) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
clips: Vec::new(),
|
||||||
|
volume: 1.0,
|
||||||
|
muted: false,
|
||||||
|
solo: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a clip to this track
|
||||||
|
pub fn add_clip(&mut self, clip: Clip) {
|
||||||
|
self.clips.push(clip);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set track volume (0.0 = silence, 1.0 = unity gain, >1.0 = amplification)
|
||||||
|
pub fn set_volume(&mut self, volume: f32) {
|
||||||
|
self.volume = volume.max(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set mute state
|
||||||
|
pub fn set_muted(&mut self, muted: bool) {
|
||||||
|
self.muted = muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set solo state
|
||||||
|
pub fn set_solo(&mut self, solo: bool) {
|
||||||
|
self.solo = solo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this track should be audible given the solo state of all tracks
|
||||||
|
pub fn is_active(&self, any_solo: bool) -> bool {
|
||||||
|
!self.muted && (!any_solo || self.solo)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render this track into the output buffer at a given timeline position
|
||||||
|
/// Returns the number of samples actually rendered
|
||||||
|
pub fn render(
|
||||||
|
&self,
|
||||||
|
output: &mut [f32],
|
||||||
|
pool: &AudioPool,
|
||||||
|
playhead_seconds: f64,
|
||||||
|
sample_rate: u32,
|
||||||
|
channels: u32,
|
||||||
|
) -> usize {
|
||||||
|
let buffer_duration_seconds = output.len() as f64 / (sample_rate as f64 * channels as f64);
|
||||||
|
let buffer_end_seconds = playhead_seconds + buffer_duration_seconds;
|
||||||
|
|
||||||
|
let mut rendered = 0;
|
||||||
|
|
||||||
|
// Render all active clips
|
||||||
|
for clip in &self.clips {
|
||||||
|
// Check if clip overlaps with current buffer time range
|
||||||
|
if clip.start_time < buffer_end_seconds && clip.end_time() > playhead_seconds {
|
||||||
|
rendered += self.render_clip(
|
||||||
|
clip,
|
||||||
|
output,
|
||||||
|
pool,
|
||||||
|
playhead_seconds,
|
||||||
|
sample_rate,
|
||||||
|
channels,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rendered
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a single clip into the output buffer
|
||||||
|
fn render_clip(
|
||||||
|
&self,
|
||||||
|
clip: &Clip,
|
||||||
|
output: &mut [f32],
|
||||||
|
pool: &AudioPool,
|
||||||
|
playhead_seconds: f64,
|
||||||
|
sample_rate: u32,
|
||||||
|
channels: u32,
|
||||||
|
) -> usize {
|
||||||
|
let buffer_duration_seconds = output.len() as f64 / (sample_rate as f64 * channels as f64);
|
||||||
|
let buffer_end_seconds = playhead_seconds + buffer_duration_seconds;
|
||||||
|
|
||||||
|
// Determine the time range we need to render (intersection of buffer and clip)
|
||||||
|
let render_start_seconds = playhead_seconds.max(clip.start_time);
|
||||||
|
let render_end_seconds = buffer_end_seconds.min(clip.end_time());
|
||||||
|
|
||||||
|
// If no overlap, return early
|
||||||
|
if render_start_seconds >= render_end_seconds {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate offset into the output buffer (in interleaved samples)
|
||||||
|
let output_offset_seconds = render_start_seconds - playhead_seconds;
|
||||||
|
let output_offset_samples = (output_offset_seconds * sample_rate as f64 * channels as f64) as usize;
|
||||||
|
|
||||||
|
// Calculate position within the clip's audio file (in seconds)
|
||||||
|
let clip_position_seconds = render_start_seconds - clip.start_time + clip.offset;
|
||||||
|
|
||||||
|
// Calculate how many samples to render in the output
|
||||||
|
let render_duration_seconds = render_end_seconds - render_start_seconds;
|
||||||
|
let samples_to_render = (render_duration_seconds * sample_rate as f64 * channels as f64) as usize;
|
||||||
|
let samples_to_render = samples_to_render.min(output.len() - output_offset_samples);
|
||||||
|
|
||||||
|
// Get the slice of output buffer to write to
|
||||||
|
if output_offset_samples + samples_to_render > output.len() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output_slice = &mut output[output_offset_samples..output_offset_samples + samples_to_render];
|
||||||
|
|
||||||
|
// Calculate combined gain
|
||||||
|
let combined_gain = clip.gain * self.volume;
|
||||||
|
|
||||||
|
// Render from pool with sample rate conversion
|
||||||
|
// Pass the time position in seconds, let the pool handle sample rate conversion
|
||||||
|
pool.render_from_file(
|
||||||
|
clip.audio_pool_index,
|
||||||
|
output_slice,
|
||||||
|
clip_position_seconds,
|
||||||
|
combined_gain,
|
||||||
|
sample_rate,
|
||||||
|
channels,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
pub use types::{AudioEvent, Command};
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
use crate::audio::{ClipId, TrackId};
|
||||||
|
|
||||||
|
/// Commands sent from UI/control thread to audio thread
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Command {
|
||||||
|
// Transport commands
|
||||||
|
/// Start playback
|
||||||
|
Play,
|
||||||
|
/// Stop playback and reset to beginning
|
||||||
|
Stop,
|
||||||
|
/// Pause playback (maintains position)
|
||||||
|
Pause,
|
||||||
|
/// Seek to a specific position in seconds
|
||||||
|
Seek(f64),
|
||||||
|
|
||||||
|
// Track management commands
|
||||||
|
/// Set track volume (0.0 = silence, 1.0 = unity gain)
|
||||||
|
SetTrackVolume(TrackId, f32),
|
||||||
|
/// Set track mute state
|
||||||
|
SetTrackMute(TrackId, bool),
|
||||||
|
/// Set track solo state
|
||||||
|
SetTrackSolo(TrackId, bool),
|
||||||
|
|
||||||
|
// Clip management commands
|
||||||
|
/// Move a clip to a new timeline position
|
||||||
|
MoveClip(TrackId, ClipId, f64),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Events sent from audio thread back to UI/control thread
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum AudioEvent {
|
||||||
|
/// Current playback position in seconds
|
||||||
|
PlaybackPosition(f64),
|
||||||
|
/// Playback has stopped (reached end of audio)
|
||||||
|
PlaybackStopped,
|
||||||
|
/// Audio buffer underrun detected
|
||||||
|
BufferUnderrun,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
use std::path::Path;
|
||||||
|
use symphonia::core::audio::SampleBuffer;
|
||||||
|
use symphonia::core::codecs::DecoderOptions;
|
||||||
|
use symphonia::core::errors::Error;
|
||||||
|
use symphonia::core::formats::FormatOptions;
|
||||||
|
use symphonia::core::io::MediaSourceStream;
|
||||||
|
use symphonia::core::meta::MetadataOptions;
|
||||||
|
use symphonia::core::probe::Hint;
|
||||||
|
|
||||||
|
pub struct AudioFile {
|
||||||
|
pub data: Vec<f32>,
|
||||||
|
pub channels: u32,
|
||||||
|
pub sample_rate: u32,
|
||||||
|
pub frames: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioFile {
|
||||||
|
/// Load an audio file from disk and decode it to interleaved f32 samples
|
||||||
|
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, String> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
// Open the media source
|
||||||
|
let file = std::fs::File::open(path)
|
||||||
|
.map_err(|e| format!("Failed to open file: {}", e))?;
|
||||||
|
|
||||||
|
let mss = MediaSourceStream::new(Box::new(file), Default::default());
|
||||||
|
|
||||||
|
// Create a probe hint using the file extension
|
||||||
|
let mut hint = Hint::new();
|
||||||
|
if let Some(extension) = path.extension() {
|
||||||
|
if let Some(ext_str) = extension.to_str() {
|
||||||
|
hint.with_extension(ext_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Probe the media source
|
||||||
|
let probed = symphonia::default::get_probe()
|
||||||
|
.format(&hint, mss, &FormatOptions::default(), &MetadataOptions::default())
|
||||||
|
.map_err(|e| format!("Failed to probe file: {}", e))?;
|
||||||
|
|
||||||
|
let mut format = probed.format;
|
||||||
|
|
||||||
|
// Find the default audio track
|
||||||
|
let track = format
|
||||||
|
.tracks()
|
||||||
|
.iter()
|
||||||
|
.find(|t| t.codec_params.codec != symphonia::core::codecs::CODEC_TYPE_NULL)
|
||||||
|
.ok_or_else(|| "No audio tracks found".to_string())?;
|
||||||
|
|
||||||
|
let track_id = track.id;
|
||||||
|
|
||||||
|
// Get audio parameters
|
||||||
|
let codec_params = &track.codec_params;
|
||||||
|
let channels = codec_params.channels
|
||||||
|
.ok_or_else(|| "Channel count not specified".to_string())?
|
||||||
|
.count() as u32;
|
||||||
|
let sample_rate = codec_params.sample_rate
|
||||||
|
.ok_or_else(|| "Sample rate not specified".to_string())?;
|
||||||
|
|
||||||
|
// Create decoder
|
||||||
|
let mut decoder = symphonia::default::get_codecs()
|
||||||
|
.make(&codec_params, &DecoderOptions::default())
|
||||||
|
.map_err(|e| format!("Failed to create decoder: {}", e))?;
|
||||||
|
|
||||||
|
// Decode all packets
|
||||||
|
let mut audio_data = Vec::new();
|
||||||
|
let mut sample_buf = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let packet = match format.next_packet() {
|
||||||
|
Ok(packet) => packet,
|
||||||
|
Err(Error::ResetRequired) => {
|
||||||
|
return Err("Decoder reset required (not implemented)".to_string());
|
||||||
|
}
|
||||||
|
Err(Error::IoError(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
|
||||||
|
// End of file
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(format!("Failed to read packet: {}", e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Skip packets for other tracks
|
||||||
|
if packet.track_id() != track_id {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the packet
|
||||||
|
match decoder.decode(&packet) {
|
||||||
|
Ok(decoded) => {
|
||||||
|
// Initialize sample buffer on first packet
|
||||||
|
if sample_buf.is_none() {
|
||||||
|
let spec = *decoded.spec();
|
||||||
|
let duration = decoded.capacity() as u64;
|
||||||
|
sample_buf = Some(SampleBuffer::<f32>::new(duration, spec));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy decoded audio to sample buffer
|
||||||
|
if let Some(ref mut buf) = sample_buf {
|
||||||
|
buf.copy_interleaved_ref(decoded);
|
||||||
|
audio_data.extend_from_slice(buf.samples());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(Error::DecodeError(e)) => {
|
||||||
|
eprintln!("Decode error: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(format!("Decode failed: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let frames = (audio_data.len() / channels as usize) as u64;
|
||||||
|
|
||||||
|
Ok(AudioFile {
|
||||||
|
data: audio_data,
|
||||||
|
channels,
|
||||||
|
sample_rate,
|
||||||
|
frames,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod audio_file;
|
||||||
|
|
||||||
|
pub use audio_file::AudioFile;
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
// DAW Backend - Phase 4: Clips & Timeline
|
||||||
|
//
|
||||||
|
// A DAW backend with timeline-based playback, clips, and audio pool.
|
||||||
|
// Supports multiple tracks, mixing, per-track volume/mute/solo, and shared audio data.
|
||||||
|
// Uses lock-free command queues, cpal for audio I/O, and symphonia for audio file decoding.
|
||||||
|
|
||||||
|
pub mod audio;
|
||||||
|
pub mod command;
|
||||||
|
pub mod io;
|
||||||
|
|
||||||
|
// Re-export commonly used types
|
||||||
|
pub use audio::{AudioPool, Clip, ClipId, Engine, EngineController, PoolAudioFile, Track, TrackId};
|
||||||
|
pub use command::{AudioEvent, Command};
|
||||||
|
pub use io::AudioFile;
|
||||||
|
|
@ -0,0 +1,420 @@
|
||||||
|
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||||
|
use daw_backend::{AudioEvent, AudioFile, Clip, Engine, PoolAudioFile, Track};
|
||||||
|
use std::env;
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Get audio file paths from command line arguments
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
if args.len() < 2 {
|
||||||
|
eprintln!("Usage: {} <audio_file1> [audio_file2] [audio_file3] ...", args[0]);
|
||||||
|
eprintln!("Example: {} track1.wav track2.wav", args[0]);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("DAW Backend - Phase 4: Clips & Timeline\n");
|
||||||
|
|
||||||
|
// Load all audio files
|
||||||
|
let mut audio_files = Vec::new();
|
||||||
|
let mut max_sample_rate = 0;
|
||||||
|
let mut max_channels = 0;
|
||||||
|
|
||||||
|
for (i, path) in args.iter().skip(1).enumerate() {
|
||||||
|
println!("Loading file {}: {}", i + 1, path);
|
||||||
|
match AudioFile::load(path) {
|
||||||
|
Ok(audio_file) => {
|
||||||
|
let duration = audio_file.frames as f64 / audio_file.sample_rate as f64;
|
||||||
|
println!(
|
||||||
|
" {} Hz, {} channels, {} frames ({:.2}s)",
|
||||||
|
audio_file.sample_rate, audio_file.channels, audio_file.frames, duration
|
||||||
|
);
|
||||||
|
|
||||||
|
max_sample_rate = max_sample_rate.max(audio_file.sample_rate);
|
||||||
|
max_channels = max_channels.max(audio_file.channels);
|
||||||
|
|
||||||
|
audio_files.push((
|
||||||
|
Path::new(path)
|
||||||
|
.file_name()
|
||||||
|
.unwrap()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
PathBuf::from(path),
|
||||||
|
audio_file,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(" Error loading {}: {}", path, e);
|
||||||
|
eprintln!(" Skipping this file...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if audio_files.is_empty() {
|
||||||
|
eprintln!("No audio files loaded. Exiting.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\nProject settings:");
|
||||||
|
println!(" Sample rate: {} Hz", max_sample_rate);
|
||||||
|
println!(" Channels: {}", max_channels);
|
||||||
|
println!(" Files: {}", audio_files.len());
|
||||||
|
|
||||||
|
// Initialize cpal
|
||||||
|
let host = cpal::default_host();
|
||||||
|
let device = host
|
||||||
|
.default_output_device()
|
||||||
|
.ok_or("No output device available")?;
|
||||||
|
println!("\nUsing audio device: {}", device.name()?);
|
||||||
|
|
||||||
|
// Get the default output config to determine sample format
|
||||||
|
let default_config = device.default_output_config()?;
|
||||||
|
let sample_format = default_config.sample_format();
|
||||||
|
|
||||||
|
// Create a custom config matching the project settings
|
||||||
|
let config = cpal::StreamConfig {
|
||||||
|
channels: max_channels as u16,
|
||||||
|
sample_rate: cpal::SampleRate(max_sample_rate),
|
||||||
|
buffer_size: cpal::BufferSize::Default,
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("Output config: {:?} with format {:?}", config, sample_format);
|
||||||
|
|
||||||
|
// Create lock-free command and event queues
|
||||||
|
let (command_tx, command_rx) = rtrb::RingBuffer::<daw_backend::Command>::new(256);
|
||||||
|
let (event_tx, event_rx) = rtrb::RingBuffer::<AudioEvent>::new(256);
|
||||||
|
|
||||||
|
// Create the audio engine
|
||||||
|
let mut engine = Engine::new(max_sample_rate, max_channels, command_rx, event_tx);
|
||||||
|
|
||||||
|
// Add all files to the audio pool and create tracks with clips
|
||||||
|
let mut track_ids = Vec::new();
|
||||||
|
let mut clip_info = Vec::new(); // Store (track_id, clip_id, name, duration)
|
||||||
|
let mut max_duration = 0.0f64;
|
||||||
|
let mut clip_id_counter = 0u32;
|
||||||
|
|
||||||
|
println!("\nCreating tracks and clips:");
|
||||||
|
for (i, (name, path, audio_file)) in audio_files.into_iter().enumerate() {
|
||||||
|
let duration = audio_file.frames as f64 / audio_file.sample_rate as f64;
|
||||||
|
max_duration = max_duration.max(duration);
|
||||||
|
|
||||||
|
// Add audio file to pool
|
||||||
|
let pool_file = PoolAudioFile::new(
|
||||||
|
path,
|
||||||
|
audio_file.data,
|
||||||
|
audio_file.channels,
|
||||||
|
audio_file.sample_rate,
|
||||||
|
);
|
||||||
|
let pool_index = engine.audio_pool_mut().add_file(pool_file);
|
||||||
|
|
||||||
|
// Create track
|
||||||
|
let track_id = i as u32;
|
||||||
|
let mut track = Track::new(track_id, name.clone());
|
||||||
|
|
||||||
|
// Create clip that plays the entire file starting at time 0
|
||||||
|
let clip_id = clip_id_counter;
|
||||||
|
let clip = Clip::new(
|
||||||
|
clip_id,
|
||||||
|
pool_index,
|
||||||
|
0.0, // start at beginning of timeline
|
||||||
|
duration, // full duration
|
||||||
|
0.0, // no offset into file
|
||||||
|
);
|
||||||
|
clip_id_counter += 1;
|
||||||
|
|
||||||
|
track.add_clip(clip);
|
||||||
|
engine.add_track(track);
|
||||||
|
track_ids.push(track_id);
|
||||||
|
clip_info.push((track_id, clip_id, name.clone(), duration));
|
||||||
|
|
||||||
|
println!(" Track {}: {} (clip {} at 0.0s, duration {:.2}s)", i, name, clip_id, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\nTimeline duration: {:.2}s", max_duration);
|
||||||
|
|
||||||
|
let mut controller = engine.get_controller(command_tx);
|
||||||
|
|
||||||
|
// Wrap engine in Arc<Mutex> for thread-safe access
|
||||||
|
let engine = Arc::new(Mutex::new(engine));
|
||||||
|
|
||||||
|
// Build the output stream
|
||||||
|
let stream = match sample_format {
|
||||||
|
cpal::SampleFormat::F32 => build_stream::<f32>(&device, &config, engine)?,
|
||||||
|
cpal::SampleFormat::I16 => build_stream::<i16>(&device, &config, engine)?,
|
||||||
|
cpal::SampleFormat::U16 => build_stream::<u16>(&device, &config, engine)?,
|
||||||
|
_ => return Err("Unsupported sample format".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the audio stream
|
||||||
|
stream.play()?;
|
||||||
|
println!("\nAudio stream started!");
|
||||||
|
print_help();
|
||||||
|
print_status(0.0, max_duration, &track_ids);
|
||||||
|
|
||||||
|
// Spawn event listener thread
|
||||||
|
let event_rx = Arc::new(Mutex::new(event_rx));
|
||||||
|
let event_rx_clone = Arc::clone(&event_rx);
|
||||||
|
let _event_thread = thread::spawn(move || {
|
||||||
|
loop {
|
||||||
|
thread::sleep(Duration::from_millis(50));
|
||||||
|
let mut rx = event_rx_clone.lock().unwrap();
|
||||||
|
while let Ok(event) = rx.pop() {
|
||||||
|
match event {
|
||||||
|
AudioEvent::PlaybackPosition(pos) => {
|
||||||
|
// Clear the line and show position
|
||||||
|
print!("\r\x1b[K");
|
||||||
|
print!("Position: {:.2}s / {:.2}s", pos, max_duration);
|
||||||
|
print!(" [");
|
||||||
|
let bar_width = 30;
|
||||||
|
let filled = ((pos / max_duration) * bar_width as f64) as usize;
|
||||||
|
for i in 0..bar_width {
|
||||||
|
if i < filled {
|
||||||
|
print!("=");
|
||||||
|
} else if i == filled {
|
||||||
|
print!(">");
|
||||||
|
} else {
|
||||||
|
print!(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print!("]");
|
||||||
|
io::stdout().flush().ok();
|
||||||
|
}
|
||||||
|
AudioEvent::PlaybackStopped => {
|
||||||
|
print!("\r\x1b[K");
|
||||||
|
println!("Playback stopped (end of timeline)");
|
||||||
|
print!("> ");
|
||||||
|
io::stdout().flush().ok();
|
||||||
|
}
|
||||||
|
AudioEvent::BufferUnderrun => {
|
||||||
|
eprintln!("\nWarning: Buffer underrun detected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simple command loop
|
||||||
|
loop {
|
||||||
|
let mut input = String::new();
|
||||||
|
print!("\r\x1b[K> ");
|
||||||
|
io::stdout().flush()?;
|
||||||
|
io::stdin().read_line(&mut input)?;
|
||||||
|
let input = input.trim();
|
||||||
|
|
||||||
|
// Parse input
|
||||||
|
if input.is_empty() {
|
||||||
|
controller.play();
|
||||||
|
println!("Playing...");
|
||||||
|
} else if input == "q" || input == "quit" {
|
||||||
|
println!("Quitting...");
|
||||||
|
break;
|
||||||
|
} else if input == "s" || input == "stop" {
|
||||||
|
controller.stop();
|
||||||
|
println!("Stopped (reset to beginning)");
|
||||||
|
} else if input == "p" || input == "play" {
|
||||||
|
controller.play();
|
||||||
|
println!("Playing...");
|
||||||
|
} else if input == "pause" {
|
||||||
|
controller.pause();
|
||||||
|
println!("Paused");
|
||||||
|
} else if input.starts_with("seek ") {
|
||||||
|
// Parse seek time
|
||||||
|
if let Ok(seconds) = input[5..].trim().parse::<f64>() {
|
||||||
|
if seconds >= 0.0 {
|
||||||
|
controller.seek(seconds);
|
||||||
|
println!("Seeking to {:.2}s", seconds);
|
||||||
|
} else {
|
||||||
|
println!("Invalid seek time (must be >= 0.0)");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Invalid seek format. Usage: seek <seconds>");
|
||||||
|
}
|
||||||
|
} else if input.starts_with("volume ") {
|
||||||
|
// Parse: volume <track_id> <volume>
|
||||||
|
let parts: Vec<&str> = input.split_whitespace().collect();
|
||||||
|
if parts.len() == 3 {
|
||||||
|
if let (Ok(track_id), Ok(volume)) = (parts[1].parse::<u32>(), parts[2].parse::<f32>()) {
|
||||||
|
if track_ids.contains(&track_id) {
|
||||||
|
controller.set_track_volume(track_id, volume);
|
||||||
|
println!("Set track {} volume to {:.2}", track_id, volume);
|
||||||
|
} else {
|
||||||
|
println!("Invalid track ID. Available tracks: {:?}", track_ids);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Invalid format. Usage: volume <track_id> <volume>");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Usage: volume <track_id> <volume>");
|
||||||
|
}
|
||||||
|
} else if input.starts_with("mute ") {
|
||||||
|
// Parse: mute <track_id>
|
||||||
|
if let Ok(track_id) = input[5..].trim().parse::<u32>() {
|
||||||
|
if track_ids.contains(&track_id) {
|
||||||
|
controller.set_track_mute(track_id, true);
|
||||||
|
println!("Muted track {}", track_id);
|
||||||
|
} else {
|
||||||
|
println!("Invalid track ID. Available tracks: {:?}", track_ids);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Usage: mute <track_id>");
|
||||||
|
}
|
||||||
|
} else if input.starts_with("unmute ") {
|
||||||
|
// Parse: unmute <track_id>
|
||||||
|
if let Ok(track_id) = input[7..].trim().parse::<u32>() {
|
||||||
|
if track_ids.contains(&track_id) {
|
||||||
|
controller.set_track_mute(track_id, false);
|
||||||
|
println!("Unmuted track {}", track_id);
|
||||||
|
} else {
|
||||||
|
println!("Invalid track ID. Available tracks: {:?}", track_ids);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Usage: unmute <track_id>");
|
||||||
|
}
|
||||||
|
} else if input.starts_with("solo ") {
|
||||||
|
// Parse: solo <track_id>
|
||||||
|
if let Ok(track_id) = input[5..].trim().parse::<u32>() {
|
||||||
|
if track_ids.contains(&track_id) {
|
||||||
|
controller.set_track_solo(track_id, true);
|
||||||
|
println!("Soloed track {}", track_id);
|
||||||
|
} else {
|
||||||
|
println!("Invalid track ID. Available tracks: {:?}", track_ids);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Usage: solo <track_id>");
|
||||||
|
}
|
||||||
|
} else if input.starts_with("unsolo ") {
|
||||||
|
// Parse: unsolo <track_id>
|
||||||
|
if let Ok(track_id) = input[7..].trim().parse::<u32>() {
|
||||||
|
if track_ids.contains(&track_id) {
|
||||||
|
controller.set_track_solo(track_id, false);
|
||||||
|
println!("Unsoloed track {}", track_id);
|
||||||
|
} else {
|
||||||
|
println!("Invalid track ID. Available tracks: {:?}", track_ids);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Usage: unsolo <track_id>");
|
||||||
|
}
|
||||||
|
} else if input.starts_with("move ") {
|
||||||
|
// Parse: move <track_id> <clip_id> <new_start_time>
|
||||||
|
let parts: Vec<&str> = input.split_whitespace().collect();
|
||||||
|
if parts.len() == 4 {
|
||||||
|
if let (Ok(track_id), Ok(clip_id), Ok(time)) =
|
||||||
|
(parts[1].parse::<u32>(), parts[2].parse::<u32>(), parts[3].parse::<f64>()) {
|
||||||
|
// Validate track and clip exist
|
||||||
|
if let Some((_tid, _cid, name, _)) = clip_info.iter().find(|(t, c, _, _)| *t == track_id && *c == clip_id) {
|
||||||
|
controller.move_clip(track_id, clip_id, time);
|
||||||
|
println!("Moved clip {} ('{}') on track {} to {:.2}s", clip_id, name, track_id, time);
|
||||||
|
} else {
|
||||||
|
println!("Invalid track ID or clip ID");
|
||||||
|
println!("Available clips:");
|
||||||
|
for (tid, cid, name, dur) in &clip_info {
|
||||||
|
println!(" Track {}, Clip {} ('{}', duration {:.2}s)", tid, cid, name, dur);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Invalid format. Usage: move <track_id> <clip_id> <time>");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Usage: move <track_id> <clip_id> <time>");
|
||||||
|
}
|
||||||
|
} else if input == "tracks" {
|
||||||
|
println!("Available tracks: {:?}", track_ids);
|
||||||
|
} else if input == "clips" {
|
||||||
|
println!("Available clips:");
|
||||||
|
for (tid, cid, name, dur) in &clip_info {
|
||||||
|
println!(" Track {}, Clip {} ('{}', duration {:.2}s)", tid, cid, name, dur);
|
||||||
|
}
|
||||||
|
} else if input == "help" || input == "h" {
|
||||||
|
print_help();
|
||||||
|
} else {
|
||||||
|
println!("Unknown command: {}. Type 'help' for commands.", input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop the stream to stop playback
|
||||||
|
drop(stream);
|
||||||
|
println!("Goodbye!");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_help() {
|
||||||
|
println!("\nTransport Commands:");
|
||||||
|
println!(" ENTER - Play");
|
||||||
|
println!(" p, play - Play");
|
||||||
|
println!(" pause - Pause");
|
||||||
|
println!(" s, stop - Stop and reset to beginning");
|
||||||
|
println!(" seek <time> - Seek to position in seconds (e.g. 'seek 10.5')");
|
||||||
|
println!("\nTrack Commands:");
|
||||||
|
println!(" tracks - List all track IDs");
|
||||||
|
println!(" volume <id> <v> - Set track volume (e.g. 'volume 0 0.5' for 50%)");
|
||||||
|
println!(" mute <id> - Mute a track");
|
||||||
|
println!(" unmute <id> - Unmute a track");
|
||||||
|
println!(" solo <id> - Solo a track (only soloed tracks play)");
|
||||||
|
println!(" unsolo <id> - Unsolo a track");
|
||||||
|
println!("\nClip Commands:");
|
||||||
|
println!(" clips - List all clips");
|
||||||
|
println!(" move <t> <c> <s> - Move clip to new timeline position");
|
||||||
|
println!(" (e.g. 'move 0 0 5.0' moves clip 0 on track 0 to 5.0s)");
|
||||||
|
println!("\nOther:");
|
||||||
|
println!(" h, help - Show this help");
|
||||||
|
println!(" q, quit - Quit");
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_status(position: f64, duration: f64, track_ids: &[u32]) {
|
||||||
|
println!("Position: {:.2}s / {:.2}s", position, duration);
|
||||||
|
println!("Tracks: {:?}", track_ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_stream<T>(
|
||||||
|
device: &cpal::Device,
|
||||||
|
config: &cpal::StreamConfig,
|
||||||
|
engine: Arc<Mutex<Engine>>,
|
||||||
|
) -> Result<cpal::Stream, Box<dyn std::error::Error>>
|
||||||
|
where
|
||||||
|
T: cpal::Sample + cpal::SizedSample + cpal::FromSample<f32>,
|
||||||
|
{
|
||||||
|
let err_fn = |err| eprintln!("Audio stream error: {}", err);
|
||||||
|
|
||||||
|
// Preallocate a large buffer for format conversion to avoid allocations in audio callback
|
||||||
|
// Size it generously to handle typical buffer sizes (up to 8192 samples = 2048 frames * stereo * 2x safety)
|
||||||
|
let conversion_buffer = Arc::new(Mutex::new(vec![0.0f32; 16384]));
|
||||||
|
|
||||||
|
let stream = device.build_output_stream(
|
||||||
|
config,
|
||||||
|
move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
|
||||||
|
// Lock the engine
|
||||||
|
let mut engine = engine.lock().unwrap();
|
||||||
|
|
||||||
|
// Lock and reuse the conversion buffer (no allocation - buffer is pre-sized)
|
||||||
|
let mut f32_buffer = conversion_buffer.lock().unwrap();
|
||||||
|
|
||||||
|
// Safety check - if buffer is too small, we have a problem
|
||||||
|
if f32_buffer.len() < data.len() {
|
||||||
|
eprintln!("ERROR: Audio buffer size {} exceeds preallocated buffer size {}",
|
||||||
|
data.len(), f32_buffer.len());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a slice of the preallocated buffer
|
||||||
|
let buffer_slice = &mut f32_buffer[..data.len()];
|
||||||
|
buffer_slice.fill(0.0);
|
||||||
|
|
||||||
|
engine.process(buffer_slice);
|
||||||
|
|
||||||
|
// Convert f32 samples to output format
|
||||||
|
for (i, sample) in data.iter_mut().enumerate() {
|
||||||
|
*sample = cpal::Sample::from_sample(buffer_slice[i]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
err_fn,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(stream)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc_fingerprint":5864375271433249048,"outputs":{"15729799797837862367":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/skyler/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""},"4614504638168534921":{"success":true,"status":"","code":0,"stdout":"rustc 1.84.1 (e71f9a9a9 2025-01-27)\nbinary: rustc\ncommit-hash: e71f9a9a98b0faf423844bf0ba7438f29dc27d58\ncommit-date: 2025-01-27\nhost: x86_64-unknown-linux-gnu\nrelease: 1.84.1\nLLVM version: 19.1.5\n","stderr":""}},"successes":{}}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
Signature: 8a477f597d28d172789f06886806bc55
|
||||||
|
# This file is a cache directory tag created by cargo.
|
||||||
|
# For information about cache directory tags see https://bford.info/cachedir/
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
efc2abc372d112a8
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"[]","declared_features":"[]","target":17253877621571734627,"profile":16672456661551808484,"path":15263473472518641127,"deps":[[3119047509502865042,"bitflags",false,936065690411072237],[6068812510688121446,"alsa_sys",false,12520680405424975009],[12313105504269591815,"libc",false,17139254919623587491],[17664902098715829447,"cfg_if",false,13693283442466042573]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/alsa-07eb439cf9e281ec/dep-lib-alsa","checksum":false}}],"rustflags":[],"metadata":1233940552707869453,"config":2202906307356721367,"compile_kind":0}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
01b118c574f54313
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[6068812510688121446,"build_script_build",false,5023443866294375154]],"local":[{"RerunIfEnvChanged":{"var":"ALSA_NO_PKG_CONFIG","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_x86_64-unknown-linux-gnu","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_x86_64_unknown_linux_gnu","val":null}},{"RerunIfEnvChanged":{"var":"HOST_PKG_CONFIG","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG","val":null}},{"RerunIfEnvChanged":{"var":"ALSA_STATIC","val":null}},{"RerunIfEnvChanged":{"var":"ALSA_DYNAMIC","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_ALL_STATIC","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_ALL_DYNAMIC","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_PATH_x86_64-unknown-linux-gnu","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_PATH_x86_64_unknown_linux_gnu","val":null}},{"RerunIfEnvChanged":{"var":"HOST_PKG_CONFIG_PATH","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_PATH","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_LIBDIR_x86_64-unknown-linux-gnu","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_LIBDIR_x86_64_unknown_linux_gnu","val":null}},{"RerunIfEnvChanged":{"var":"HOST_PKG_CONFIG_LIBDIR","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_LIBDIR","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_SYSROOT_DIR_x86_64-unknown-linux-gnu","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_SYSROOT_DIR_x86_64_unknown_linux_gnu","val":null}},{"RerunIfEnvChanged":{"var":"HOST_PKG_CONFIG_SYSROOT_DIR","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_SYSROOT_DIR","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_SYSROOT_DIR","val":null}},{"RerunIfEnvChanged":{"var":"SYSROOT","val":null}},{"RerunIfEnvChanged":{"var":"ALSA_STATIC","val":null}},{"RerunIfEnvChanged":{"var":"ALSA_DYNAMIC","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_ALL_STATIC","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_ALL_DYNAMIC","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_x86_64-unknown-linux-gnu","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_x86_64_unknown_linux_gnu","val":null}},{"RerunIfEnvChanged":{"var":"HOST_PKG_CONFIG","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG","val":null}},{"RerunIfEnvChanged":{"var":"ALSA_STATIC","val":null}},{"RerunIfEnvChanged":{"var":"ALSA_DYNAMIC","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_ALL_STATIC","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_ALL_DYNAMIC","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_PATH_x86_64-unknown-linux-gnu","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_PATH_x86_64_unknown_linux_gnu","val":null}},{"RerunIfEnvChanged":{"var":"HOST_PKG_CONFIG_PATH","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_PATH","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_LIBDIR_x86_64-unknown-linux-gnu","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_LIBDIR_x86_64_unknown_linux_gnu","val":null}},{"RerunIfEnvChanged":{"var":"HOST_PKG_CONFIG_LIBDIR","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_LIBDIR","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_SYSROOT_DIR_x86_64-unknown-linux-gnu","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_SYSROOT_DIR_x86_64_unknown_linux_gnu","val":null}},{"RerunIfEnvChanged":{"var":"HOST_PKG_CONFIG_SYSROOT_DIR","val":null}},{"RerunIfEnvChanged":{"var":"PKG_CONFIG_SYSROOT_DIR","val":null}}],"rustflags":[],"metadata":0,"config":0,"compile_kind":0}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
f25297bf94dbb645
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"[]","declared_features":"[]","target":13708040221295731214,"profile":13232757476167777671,"path":11281270452639959780,"deps":[[7218239883571173546,"pkg_config",false,627968502664637823]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/alsa-sys-292e10cb118b63a0/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"metadata":16337494563604273723,"config":2202906307356721367,"compile_kind":0}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
a1ac8cc77d64c2ad
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"[]","declared_features":"[]","target":5808970637369783550,"profile":16672456661551808484,"path":5267533092008046462,"deps":[[6068812510688121446,"build_script_build",false,1388222992031985921],[12313105504269591815,"libc",false,17139254919623587491]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/alsa-sys-2a4f1b5b9ddd6a15/dep-lib-alsa_sys","checksum":false}}],"rustflags":[],"metadata":16337494563604273723,"config":2202906307356721367,"compile_kind":0}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
afc4bfc0995f2524
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"[\"default\", \"std\"]","declared_features":"[\"borsh\", \"default\", \"serde\", \"std\", \"zeroize\"]","target":2695225908652729879,"profile":16672456661551808484,"path":3825848541173903799,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/arrayvec-7bcc8a1d7a5c02ca/dep-lib-arrayvec","checksum":false}}],"rustflags":[],"metadata":5019420986621020735,"config":2202906307356721367,"compile_kind":0}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
edfa6133c992fd0c
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"[]","declared_features":"[\"arbitrary\", \"bytemuck\", \"example_generated\", \"serde\", \"std\"]","target":4580331566439999389,"profile":16672456661551808484,"path":15287770119151306872,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/bitflags-0ae614ad006941ff/dep-lib-bitflags","checksum":false}}],"rustflags":[],"metadata":14564035643000669268,"config":2202906307356721367,"compile_kind":0}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
b92268bca71f240d
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"[\"default\"]","declared_features":"[\"compiler_builtins\", \"core\", \"default\", \"example_generated\", \"rustc-dep-of-std\"]","target":202096439108023897,"profile":16672456661551808484,"path":9707657108018286451,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/bitflags-b2981de1f55f2ddb/dep-lib-bitflags","checksum":false}}],"rustflags":[],"metadata":14564035643000669268,"config":2202906307356721367,"compile_kind":0}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
c964e5b071fc9ec1
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"[]","declared_features":"[\"aarch64_simd\", \"align_offset\", \"alloc_uninit\", \"avx512_simd\", \"bytemuck_derive\", \"const_zeroed\", \"derive\", \"extern_crate_alloc\", \"extern_crate_std\", \"impl_core_error\", \"latest_stable_rust\", \"min_const_generics\", \"must_cast\", \"must_cast_extra\", \"nightly_docs\", \"nightly_float\", \"nightly_portable_simd\", \"nightly_stdsimd\", \"pod_saturating\", \"track_caller\", \"transparentwrapper_extra\", \"unsound_ptr_pod_impl\", \"wasm_simd\", \"zeroable_atomics\", \"zeroable_maybe_uninit\", \"zeroable_unwind_fn\"]","target":13889761946161992078,"profile":2576720359223898410,"path":6578211936250456348,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/bytemuck-0c42aec3449ada1e/dep-lib-bytemuck","checksum":false}}],"rustflags":[],"metadata":5417891915809776353,"config":2202906307356721367,"compile_kind":0}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
cd32eda4bd5008be
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"[]","declared_features":"[\"core\", \"rustc-dep-of-std\"]","target":11092925554354248609,"profile":16672456661551808484,"path":1268454759411133288,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/cfg-if-6b5d4268dd6b1d06/dep-lib-cfg_if","checksum":false}}],"rustflags":[],"metadata":11443632179419052932,"config":2202906307356721367,"compile_kind":0}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
90068b4b799b4460
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"[]","declared_features":"[\"asio\", \"asio-sys\", \"jack\", \"num-traits\", \"oboe-shared-stdcxx\", \"wasm-bindgen\"]","target":1073436701803485107,"profile":16672456661551808484,"path":14847338652081046964,"deps":[[1063624604953407536,"build_script_build",false,8891794042185684842],[10152219098058873194,"dasp_sample",false,14248900223657003451],[12313105504269591815,"libc",false,17139254919623587491],[15802253903530241862,"alsa",false,12110972638791975663]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/cpal-398d04d5b6a4a349/dep-lib-cpal","checksum":false}}],"rustflags":[],"metadata":3729206225701025435,"config":2202906307356721367,"compile_kind":0}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
cf7e864a41b58c80
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"[]","declared_features":"[\"asio\", \"asio-sys\", \"jack\", \"num-traits\", \"oboe-shared-stdcxx\", \"wasm-bindgen\"]","target":9652763411108993936,"profile":13232757476167777671,"path":7190939237906826957,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/cpal-f41e9b8a3905b034/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"metadata":3729206225701025435,"config":2202906307356721367,"compile_kind":0}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
6a97691492ff657b
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[1063624604953407536,"build_script_build",false,9262977825622097615]],"local":[{"RerunIfEnvChanged":{"var":"CPAL_ASIO_DIR","val":null}}],"rustflags":[],"metadata":0,"config":0,"compile_kind":0}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
bb490f6f4243bec5
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":12892376904506784995,"profile":16672456661551808484,"path":2530262269858978323,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/dasp_sample-027ac5431d66da8b/dep-lib-dasp_sample","checksum":false}}],"rustflags":[],"metadata":15891645068391474568,"config":2202906307356721367,"compile_kind":0}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
24747b6cdebc3336
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"[]","declared_features":"[]","target":18422595111168000093,"profile":16526934715089055661,"path":10602529704205407992,"deps":[[1063624604953407536,"cpal",false,6936840271318156944],[7971264019667955623,"symphonia",false,1744930397742156336],[17939939644154445961,"daw_backend",false,13898417669016405786]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/daw-backend-6a363250abbbed9b/dep-bin-daw-backend","checksum":false}}],"rustflags":[],"metadata":7797948686568424061,"config":2202906307356721367,"compile_kind":0}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
87b66ae7311a7954
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"[]","declared_features":"[]","target":752497957460699340,"profile":16526934715089055661,"path":17777289886553719987,"deps":[[1063624604953407536,"cpal",false,6936840271318156944],[5780768746580775414,"rtrb",false,8787996382249001427],[7971264019667955623,"symphonia",false,1744930397742156336]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/daw-backend-a770293ea46b2024/dep-lib-daw_backend","checksum":false}}],"rustflags":[],"metadata":7797948686568424061,"config":2202906307356721367,"compile_kind":0}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
e6c890495605d37d
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"[]","declared_features":"[]","target":18422595111168000093,"profile":16526934715089055661,"path":10602529704205407992,"deps":[[1063624604953407536,"cpal",false,6936840271318156944],[5780768746580775414,"rtrb",false,8787996382249001427],[7971264019667955623,"symphonia",false,1744930397742156336],[17939939644154445961,"daw_backend",false,6086925173006186119]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/daw-backend-ad53dff3f6cb6940/dep-bin-daw-backend","checksum":false}}],"rustflags":[],"metadata":7797948686568424061,"config":2202906307356721367,"compile_kind":0}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
1a47bca53b19e1c0
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"[]","declared_features":"[]","target":752497957460699340,"profile":16526934715089055661,"path":17777289886553719987,"deps":[[1063624604953407536,"cpal",false,6936840271318156944],[7971264019667955623,"symphonia",false,1744930397742156336]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/daw-backend-f1860103215383f6/dep-lib-daw_backend","checksum":false}}],"rustflags":[],"metadata":7797948686568424061,"config":2202906307356721367,"compile_kind":0}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
f5a59024aee69529
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"[\"alloc\", \"default\"]","declared_features":"[\"alloc\", \"any_all_workaround\", \"default\", \"fast-big5-hanzi-encode\", \"fast-gb-hanzi-encode\", \"fast-hangul-encode\", \"fast-hanja-encode\", \"fast-kanji-encode\", \"fast-legacy-encode\", \"less-slow-big5-hanzi-encode\", \"less-slow-gb-hanzi-encode\", \"less-slow-kanji-encode\", \"serde\", \"simd-accel\"]","target":5079274106931611199,"profile":16672456661551808484,"path":17964847688851721702,"deps":[[17664902098715829447,"cfg_if",false,13693283442466042573]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/encoding_rs-ab8ec7f46881aef1/dep-lib-encoding_rs","checksum":false}}],"rustflags":[],"metadata":10075669053249481654,"config":2202906307356721367,"compile_kind":0}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
28219a49b8789695
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"[]","declared_features":"[]","target":13768523742385470389,"profile":16672456661551808484,"path":1128130351984163184,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/extended-c9220c49ea497722/dep-lib-extended","checksum":false}}],"rustflags":[],"metadata":868397670421919837,"config":2202906307356721367,"compile_kind":0}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
06fc0ba7df4b89c7
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"[]","declared_features":"[\"spin\", \"spin_no_std\"]","target":3612849059666211517,"profile":16672456661551808484,"path":13102287418376965538,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/lazy_static-5464cf59c7798be8/dep-lib-lazy_static","checksum":false}}],"rustflags":[],"metadata":111743654650316589,"config":2202906307356721367,"compile_kind":0}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
37fe7a9db7e38a7d
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":13207435774680941178,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[12313105504269591815,"build_script_build",false,365172591850775182]],"local":[{"RerunIfChanged":{"output":"debug/build/libc-b35409cf8cf111a9/output","paths":["build.rs"]}},{"RerunIfEnvChanged":{"var":"RUST_LIBC_UNSTABLE_FREEBSD_VERSION","val":null}},{"RerunIfEnvChanged":{"var":"RUST_LIBC_UNSTABLE_MUSL_V1_2_3","val":null}},{"RerunIfEnvChanged":{"var":"RUST_LIBC_UNSTABLE_LINUX_TIME_BITS64","val":null}},{"RerunIfEnvChanged":{"var":"RUST_LIBC_UNSTABLE_GNU_FILE_OFFSET_BITS","val":null}},{"RerunIfEnvChanged":{"var":"RUST_LIBC_UNSTABLE_GNU_TIME_BITS","val":null}}],"rustflags":[],"metadata":0,"config":0,"compile_kind":0}
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
This file has an mtime of when this was started.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
a3b607af8adddaed
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue