midi import in daw backend
This commit is contained in:
parent
e45659ddfd
commit
7ef562917a
|
|
@ -196,6 +196,31 @@ dependencies = [
|
|||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "dasp_sample"
|
||||
version = "0.11.0"
|
||||
|
|
@ -207,6 +232,7 @@ name = "daw-backend"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"midly",
|
||||
"rtrb",
|
||||
"symphonia",
|
||||
]
|
||||
|
|
@ -372,6 +398,15 @@ version = "2.7.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "midly"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "207d755f4cb882d20c4da58d707ca9130a0c9bc5061f657a4f299b8e36362b7a"
|
||||
dependencies = [
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
|
|
@ -527,6 +562,26 @@ version = "5.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.2"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ edition = "2021"
|
|||
cpal = "0.15"
|
||||
symphonia = { version = "0.5", features = ["all"] }
|
||||
rtrb = "0.3"
|
||||
midly = "0.5"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -0,0 +1,72 @@
|
|||
use daw_backend::load_midi_file;
|
||||
|
||||
fn main() {
|
||||
let clip = load_midi_file("darude-sandstorm.mid", 0, 44100).unwrap();
|
||||
|
||||
println!("Clip duration: {:.2}s", clip.duration);
|
||||
println!("Total events: {}", clip.events.len());
|
||||
println!("\nEvent summary:");
|
||||
|
||||
let mut note_on_count = 0;
|
||||
let mut note_off_count = 0;
|
||||
let mut other_count = 0;
|
||||
|
||||
for event in &clip.events {
|
||||
if event.is_note_on() {
|
||||
note_on_count += 1;
|
||||
} else if event.is_note_off() {
|
||||
note_off_count += 1;
|
||||
} else {
|
||||
other_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
println!(" Note On events: {}", note_on_count);
|
||||
println!(" Note Off events: {}", note_off_count);
|
||||
println!(" Other events: {}", other_count);
|
||||
|
||||
// Show events around 28 seconds
|
||||
println!("\nEvents around 28 seconds (27-29s):");
|
||||
let sample_rate = 44100.0;
|
||||
let start_sample = (27.0 * sample_rate) as u64;
|
||||
let end_sample = (29.0 * sample_rate) as u64;
|
||||
|
||||
for (i, event) in clip.events.iter().enumerate() {
|
||||
if event.timestamp >= start_sample && event.timestamp <= end_sample {
|
||||
let time_sec = event.timestamp as f64 / sample_rate;
|
||||
let event_type = if event.is_note_on() {
|
||||
"NoteOn"
|
||||
} else if event.is_note_off() {
|
||||
"NoteOff"
|
||||
} else {
|
||||
"Other"
|
||||
};
|
||||
println!(" [{:4}] {:.3}s: {} ch={} note={} vel={}",
|
||||
i, time_sec, event_type, event.channel(), event.data1, event.data2);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for stuck notes - note ons without corresponding note offs
|
||||
println!("\nChecking for unmatched notes...");
|
||||
let mut active_notes = std::collections::HashMap::new();
|
||||
|
||||
for (i, event) in clip.events.iter().enumerate() {
|
||||
if event.is_note_on() {
|
||||
let key = (event.channel(), event.data1);
|
||||
active_notes.insert(key, i);
|
||||
} else if event.is_note_off() {
|
||||
let key = (event.channel(), event.data1);
|
||||
active_notes.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
if !active_notes.is_empty() {
|
||||
println!("Found {} notes that never got note-off events:", active_notes.len());
|
||||
for ((ch, note), event_idx) in active_notes.iter().take(10) {
|
||||
let time_sec = clip.events[*event_idx].timestamp as f64 / sample_rate;
|
||||
println!(" Note {} on channel {} at {:.2}s (event #{})", note, ch, time_sec, event_idx);
|
||||
}
|
||||
} else {
|
||||
println!("All notes have matching note-off events!");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
use daw_backend::load_midi_file;
|
||||
|
||||
fn main() {
|
||||
let clip = load_midi_file("darude-sandstorm.mid", 0, 44100).unwrap();
|
||||
|
||||
println!("Clip duration: {:.3}s", clip.duration);
|
||||
println!("Total events: {}", clip.events.len());
|
||||
|
||||
// Show the last 30 events
|
||||
println!("\nLast 30 events:");
|
||||
let sample_rate = 44100.0;
|
||||
let start_idx = clip.events.len().saturating_sub(30);
|
||||
|
||||
for (i, event) in clip.events.iter().enumerate().skip(start_idx) {
|
||||
let time_sec = event.timestamp as f64 / sample_rate;
|
||||
let event_type = if event.is_note_on() {
|
||||
"NoteOn "
|
||||
} else if event.is_note_off() {
|
||||
"NoteOff"
|
||||
} else {
|
||||
"Other "
|
||||
};
|
||||
println!(" [{:4}] {:.3}s: {} ch={} note={:3} vel={:3}",
|
||||
i, time_sec, event_type, event.channel(), event.data1, event.data2);
|
||||
}
|
||||
|
||||
// Find notes that are still active at the end of the clip
|
||||
println!("\nNotes active at end of clip ({:.3}s):", clip.duration);
|
||||
let mut active_notes = std::collections::HashMap::new();
|
||||
|
||||
for event in &clip.events {
|
||||
let time_sec = event.timestamp as f64 / sample_rate;
|
||||
|
||||
if event.is_note_on() {
|
||||
let key = (event.channel(), event.data1);
|
||||
active_notes.insert(key, time_sec);
|
||||
} else if event.is_note_off() {
|
||||
let key = (event.channel(), event.data1);
|
||||
active_notes.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
if !active_notes.is_empty() {
|
||||
println!("Found {} notes still active after all events:", active_notes.len());
|
||||
for ((ch, note), start_time) in &active_notes {
|
||||
println!(" Channel {} Note {} started at {:.3}s (no note-off before clip end)",
|
||||
ch, note, start_time);
|
||||
}
|
||||
} else {
|
||||
println!("All notes are turned off by the end!");
|
||||
}
|
||||
|
||||
// Check maximum polyphony
|
||||
println!("\nAnalyzing polyphony...");
|
||||
let mut max_polyphony = 0;
|
||||
let mut current_notes = std::collections::HashSet::new();
|
||||
|
||||
for event in &clip.events {
|
||||
if event.is_note_on() {
|
||||
let key = (event.channel(), event.data1);
|
||||
current_notes.insert(key);
|
||||
max_polyphony = max_polyphony.max(current_notes.len());
|
||||
} else if event.is_note_off() {
|
||||
let key = (event.channel(), event.data1);
|
||||
current_notes.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
println!("Maximum simultaneous notes: {}", max_polyphony);
|
||||
println!("Available synth voices: 16");
|
||||
if max_polyphony > 16 {
|
||||
println!("WARNING: Polyphony exceeds available voices! Voice stealing will occur.");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
/// Pool of reusable audio buffers for recursive group rendering
|
||||
///
|
||||
/// This pool allows groups to acquire temporary buffers for submixing
|
||||
/// child tracks without allocating memory in the audio thread.
|
||||
pub struct BufferPool {
|
||||
buffers: Vec<Vec<f32>>,
|
||||
available: Vec<usize>,
|
||||
buffer_size: usize,
|
||||
}
|
||||
|
||||
impl BufferPool {
|
||||
/// Create a new buffer pool
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `initial_capacity` - Number of buffers to pre-allocate
|
||||
/// * `buffer_size` - Size of each buffer in samples
|
||||
pub fn new(initial_capacity: usize, buffer_size: usize) -> Self {
|
||||
let mut buffers = Vec::with_capacity(initial_capacity);
|
||||
let mut available = Vec::with_capacity(initial_capacity);
|
||||
|
||||
// Pre-allocate buffers
|
||||
for i in 0..initial_capacity {
|
||||
buffers.push(vec![0.0; buffer_size]);
|
||||
available.push(i);
|
||||
}
|
||||
|
||||
Self {
|
||||
buffers,
|
||||
available,
|
||||
buffer_size,
|
||||
}
|
||||
}
|
||||
|
||||
/// Acquire a buffer from the pool
|
||||
///
|
||||
/// Returns a zeroed buffer ready for use. If no buffers are available,
|
||||
/// allocates a new one (though this should be avoided in the audio thread).
|
||||
pub fn acquire(&mut self) -> Vec<f32> {
|
||||
if let Some(idx) = self.available.pop() {
|
||||
// Reuse an existing buffer
|
||||
let mut buf = std::mem::take(&mut self.buffers[idx]);
|
||||
buf.fill(0.0);
|
||||
buf
|
||||
} else {
|
||||
// No buffers available, allocate a new one
|
||||
// This should be rare if the pool is sized correctly
|
||||
vec![0.0; self.buffer_size]
|
||||
}
|
||||
}
|
||||
|
||||
/// Release a buffer back to the pool
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `buffer` - The buffer to return to the pool
|
||||
pub fn release(&mut self, buffer: Vec<f32>) {
|
||||
// Only add to pool if it's the correct size
|
||||
if buffer.len() == self.buffer_size {
|
||||
let idx = self.buffers.len();
|
||||
self.buffers.push(buffer);
|
||||
self.available.push(idx);
|
||||
}
|
||||
// Otherwise, drop the buffer (wrong size, shouldn't happen normally)
|
||||
}
|
||||
|
||||
/// Get the configured buffer size
|
||||
pub fn buffer_size(&self) -> usize {
|
||||
self.buffer_size
|
||||
}
|
||||
|
||||
/// Get the number of available buffers
|
||||
pub fn available_count(&self) -> usize {
|
||||
self.available.len()
|
||||
}
|
||||
|
||||
/// Get the total number of buffers in the pool
|
||||
pub fn total_count(&self) -> usize {
|
||||
self.buffers.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BufferPool {
|
||||
fn default() -> Self {
|
||||
// Default: 8 buffers of 4096 samples (enough for 85ms at 48kHz stereo)
|
||||
Self::new(8, 4096)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,19 @@
|
|||
use crate::audio::buffer_pool::BufferPool;
|
||||
use crate::audio::clip::ClipId;
|
||||
use crate::audio::midi::{MidiClip, MidiClipId, MidiEvent};
|
||||
use crate::audio::pool::AudioPool;
|
||||
use crate::audio::project::Project;
|
||||
use crate::audio::track::{Track, TrackId};
|
||||
use crate::command::{AudioEvent, Command};
|
||||
use crate::effects::{Effect, GainEffect, PanEffect, SimpleEQ};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Audio engine for Phase 4: timeline with clips and audio pool
|
||||
/// Audio engine for Phase 6: hierarchical tracks with groups
|
||||
pub struct Engine {
|
||||
tracks: Vec<Track>,
|
||||
project: Project,
|
||||
audio_pool: AudioPool,
|
||||
buffer_pool: BufferPool,
|
||||
playhead: u64, // Playhead position in samples
|
||||
sample_rate: u32,
|
||||
playing: bool,
|
||||
|
|
@ -25,8 +30,11 @@ pub struct Engine {
|
|||
frames_since_last_event: usize,
|
||||
event_interval_frames: usize,
|
||||
|
||||
// Mix buffer for combining tracks
|
||||
// Mix buffer for output
|
||||
mix_buffer: Vec<f32>,
|
||||
|
||||
// ID counters
|
||||
next_midi_clip_id: MidiClipId,
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
|
|
@ -39,9 +47,13 @@ impl Engine {
|
|||
) -> Self {
|
||||
let event_interval_frames = (sample_rate as usize * channels as usize) / 10; // Update 10 times per second
|
||||
|
||||
// Calculate a reasonable buffer size for the pool (typical audio callback size * channels)
|
||||
let buffer_size = 512 * channels as usize;
|
||||
|
||||
Self {
|
||||
tracks: Vec::new(),
|
||||
project: Project::new(),
|
||||
audio_pool: AudioPool::new(),
|
||||
buffer_pool: BufferPool::new(8, buffer_size), // 8 buffers should handle deep nesting
|
||||
playhead: 0,
|
||||
sample_rate,
|
||||
playing: false,
|
||||
|
|
@ -52,16 +64,55 @@ impl Engine {
|
|||
frames_since_last_event: 0,
|
||||
event_interval_frames,
|
||||
mix_buffer: Vec::new(),
|
||||
next_midi_clip_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a track to the engine
|
||||
/// Add an audio track to the engine
|
||||
pub fn add_track(&mut self, track: Track) -> TrackId {
|
||||
let id = track.id;
|
||||
self.tracks.push(track);
|
||||
// For backwards compatibility, we'll extract the track data and add it to the project
|
||||
let name = track.name.clone();
|
||||
let id = self.project.add_audio_track(name, None);
|
||||
|
||||
// Copy over the track properties
|
||||
if let Some(node) = self.project.get_track_mut(id) {
|
||||
if let crate::audio::track::TrackNode::Audio(audio_track) = node {
|
||||
audio_track.clips = track.clips;
|
||||
audio_track.effects = track.effects;
|
||||
audio_track.volume = track.volume;
|
||||
audio_track.muted = track.muted;
|
||||
audio_track.solo = track.solo;
|
||||
}
|
||||
}
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Add an audio track by name
|
||||
pub fn add_audio_track(&mut self, name: String) -> TrackId {
|
||||
self.project.add_audio_track(name, None)
|
||||
}
|
||||
|
||||
/// Add a group track by name
|
||||
pub fn add_group_track(&mut self, name: String) -> TrackId {
|
||||
self.project.add_group_track(name, None)
|
||||
}
|
||||
|
||||
/// Add a MIDI track by name
|
||||
pub fn add_midi_track(&mut self, name: String) -> TrackId {
|
||||
self.project.add_midi_track(name, None)
|
||||
}
|
||||
|
||||
/// Get access to the project
|
||||
pub fn project(&self) -> &Project {
|
||||
&self.project
|
||||
}
|
||||
|
||||
/// Get mutable access to the project
|
||||
pub fn project_mut(&mut self) -> &mut Project {
|
||||
&mut self.project
|
||||
}
|
||||
|
||||
/// Get mutable reference to audio pool
|
||||
pub fn audio_pool_mut(&mut self) -> &mut AudioPool {
|
||||
&mut self.audio_pool
|
||||
|
|
@ -95,27 +146,24 @@ impl Engine {
|
|||
self.mix_buffer.resize(output.len(), 0.0);
|
||||
}
|
||||
|
||||
// Clear mix buffer
|
||||
self.mix_buffer.fill(0.0);
|
||||
// Ensure buffer pool has the correct buffer size
|
||||
if self.buffer_pool.buffer_size() != output.len() {
|
||||
// Reallocate buffer pool with correct size if needed
|
||||
self.buffer_pool = BufferPool::new(8, output.len());
|
||||
}
|
||||
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Render the entire project hierarchy into the mix buffer
|
||||
self.project.render(
|
||||
&mut self.mix_buffer,
|
||||
&self.audio_pool,
|
||||
&mut self.buffer_pool,
|
||||
playhead_seconds,
|
||||
self.sample_rate,
|
||||
self.channels,
|
||||
);
|
||||
|
||||
// Copy mix to output
|
||||
output.copy_from_slice(&self.mix_buffer);
|
||||
|
|
@ -165,27 +213,175 @@ impl Engine {
|
|||
.store(self.playhead, Ordering::Relaxed);
|
||||
}
|
||||
Command::SetTrackVolume(track_id, volume) => {
|
||||
if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) {
|
||||
if let Some(track) = self.project.get_track_mut(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) {
|
||||
if let Some(track) = self.project.get_track_mut(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) {
|
||||
if let Some(track) = self.project.get_track_mut(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(crate::audio::track::TrackNode::Audio(track)) = self.project.get_track_mut(track_id) {
|
||||
if let Some(clip) = track.clips.iter_mut().find(|c| c.id == clip_id) {
|
||||
clip.start_time = new_start_time;
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::AddGainEffect(track_id, gain_db) => {
|
||||
// Get the track node and handle audio tracks, MIDI tracks, and groups
|
||||
match self.project.get_track_mut(track_id) {
|
||||
Some(crate::audio::track::TrackNode::Audio(track)) => {
|
||||
if let Some(effect) = track.effects.iter_mut().find(|e| e.name() == "Gain") {
|
||||
effect.set_parameter(0, gain_db);
|
||||
} else {
|
||||
track.add_effect(Box::new(GainEffect::with_gain_db(gain_db)));
|
||||
}
|
||||
}
|
||||
Some(crate::audio::track::TrackNode::Midi(track)) => {
|
||||
if let Some(effect) = track.effects.iter_mut().find(|e| e.name() == "Gain") {
|
||||
effect.set_parameter(0, gain_db);
|
||||
} else {
|
||||
track.add_effect(Box::new(GainEffect::with_gain_db(gain_db)));
|
||||
}
|
||||
}
|
||||
Some(crate::audio::track::TrackNode::Group(group)) => {
|
||||
if let Some(effect) = group.effects.iter_mut().find(|e| e.name() == "Gain") {
|
||||
effect.set_parameter(0, gain_db);
|
||||
} else {
|
||||
group.add_effect(Box::new(GainEffect::with_gain_db(gain_db)));
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Command::AddPanEffect(track_id, pan) => {
|
||||
match self.project.get_track_mut(track_id) {
|
||||
Some(crate::audio::track::TrackNode::Audio(track)) => {
|
||||
if let Some(effect) = track.effects.iter_mut().find(|e| e.name() == "Pan") {
|
||||
effect.set_parameter(0, pan);
|
||||
} else {
|
||||
track.add_effect(Box::new(PanEffect::with_pan(pan)));
|
||||
}
|
||||
}
|
||||
Some(crate::audio::track::TrackNode::Midi(track)) => {
|
||||
if let Some(effect) = track.effects.iter_mut().find(|e| e.name() == "Pan") {
|
||||
effect.set_parameter(0, pan);
|
||||
} else {
|
||||
track.add_effect(Box::new(PanEffect::with_pan(pan)));
|
||||
}
|
||||
}
|
||||
Some(crate::audio::track::TrackNode::Group(group)) => {
|
||||
if let Some(effect) = group.effects.iter_mut().find(|e| e.name() == "Pan") {
|
||||
effect.set_parameter(0, pan);
|
||||
} else {
|
||||
group.add_effect(Box::new(PanEffect::with_pan(pan)));
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Command::AddEQEffect(track_id, low_db, mid_db, high_db) => {
|
||||
match self.project.get_track_mut(track_id) {
|
||||
Some(crate::audio::track::TrackNode::Audio(track)) => {
|
||||
if let Some(effect) = track.effects.iter_mut().find(|e| e.name() == "SimpleEQ") {
|
||||
effect.set_parameter(0, low_db);
|
||||
effect.set_parameter(1, mid_db);
|
||||
effect.set_parameter(2, high_db);
|
||||
} else {
|
||||
let mut eq = SimpleEQ::new();
|
||||
eq.set_parameter(0, low_db);
|
||||
eq.set_parameter(1, mid_db);
|
||||
eq.set_parameter(2, high_db);
|
||||
track.add_effect(Box::new(eq));
|
||||
}
|
||||
}
|
||||
Some(crate::audio::track::TrackNode::Midi(track)) => {
|
||||
if let Some(effect) = track.effects.iter_mut().find(|e| e.name() == "SimpleEQ") {
|
||||
effect.set_parameter(0, low_db);
|
||||
effect.set_parameter(1, mid_db);
|
||||
effect.set_parameter(2, high_db);
|
||||
} else {
|
||||
let mut eq = SimpleEQ::new();
|
||||
eq.set_parameter(0, low_db);
|
||||
eq.set_parameter(1, mid_db);
|
||||
eq.set_parameter(2, high_db);
|
||||
track.add_effect(Box::new(eq));
|
||||
}
|
||||
}
|
||||
Some(crate::audio::track::TrackNode::Group(group)) => {
|
||||
if let Some(effect) = group.effects.iter_mut().find(|e| e.name() == "SimpleEQ") {
|
||||
effect.set_parameter(0, low_db);
|
||||
effect.set_parameter(1, mid_db);
|
||||
effect.set_parameter(2, high_db);
|
||||
} else {
|
||||
let mut eq = SimpleEQ::new();
|
||||
eq.set_parameter(0, low_db);
|
||||
eq.set_parameter(1, mid_db);
|
||||
eq.set_parameter(2, high_db);
|
||||
group.add_effect(Box::new(eq));
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Command::ClearEffects(track_id) => {
|
||||
let _ = self.project.clear_effects(track_id);
|
||||
}
|
||||
Command::CreateGroup(name) => {
|
||||
let track_id = self.project.add_group_track(name.clone(), None);
|
||||
// Notify UI about the new group
|
||||
let _ = self.event_tx.push(AudioEvent::TrackCreated(track_id, true, name));
|
||||
}
|
||||
Command::AddToGroup(track_id, group_id) => {
|
||||
// Move the track to the new group (Project handles removing from old parent)
|
||||
self.project.move_to_group(track_id, group_id);
|
||||
}
|
||||
Command::RemoveFromGroup(track_id) => {
|
||||
// Move to root level (None as parent)
|
||||
self.project.move_to_root(track_id);
|
||||
}
|
||||
Command::CreateMidiTrack(name) => {
|
||||
let track_id = self.project.add_midi_track(name.clone(), None);
|
||||
// Notify UI about the new MIDI track
|
||||
let _ = self.event_tx.push(AudioEvent::TrackCreated(track_id, false, name));
|
||||
}
|
||||
Command::CreateMidiClip(track_id, start_time, duration) => {
|
||||
// Create a new MIDI clip with unique ID
|
||||
let clip_id = self.next_midi_clip_id;
|
||||
self.next_midi_clip_id += 1;
|
||||
let clip = MidiClip::new(clip_id, start_time, duration);
|
||||
let _ = self.project.add_midi_clip(track_id, clip);
|
||||
}
|
||||
Command::AddMidiNote(track_id, clip_id, time_offset, note, velocity, duration) => {
|
||||
// Add a MIDI note event to the specified clip
|
||||
if let Some(crate::audio::track::TrackNode::Midi(track)) = self.project.get_track_mut(track_id) {
|
||||
if let Some(clip) = track.clips.iter_mut().find(|c| c.id == clip_id) {
|
||||
// Convert time to sample timestamp
|
||||
let timestamp = (time_offset * self.sample_rate as f64) as u64;
|
||||
let note_on = MidiEvent::note_on(timestamp, 0, note, velocity);
|
||||
clip.events.push(note_on);
|
||||
|
||||
// Add note off event
|
||||
let note_off_timestamp = ((time_offset + duration) * self.sample_rate as f64) as u64;
|
||||
let note_off = MidiEvent::note_off(note_off_timestamp, 0, note, 64);
|
||||
clip.events.push(note_off);
|
||||
|
||||
// Sort events by timestamp
|
||||
clip.events.sort_by_key(|e| e.timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::AddLoadedMidiClip(track_id, clip) => {
|
||||
// Add a pre-loaded MIDI clip to the track
|
||||
let _ = self.project.add_midi_clip(track_id, clip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -201,7 +397,7 @@ impl Engine {
|
|||
|
||||
/// Get number of tracks
|
||||
pub fn track_count(&self) -> usize {
|
||||
self.tracks.len()
|
||||
self.project.track_count()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -256,6 +452,26 @@ impl EngineController {
|
|||
let _ = self.command_tx.push(Command::MoveClip(track_id, clip_id, new_start_time));
|
||||
}
|
||||
|
||||
/// Add or update gain effect on track
|
||||
pub fn add_gain_effect(&mut self, track_id: TrackId, gain_db: f32) {
|
||||
let _ = self.command_tx.push(Command::AddGainEffect(track_id, gain_db));
|
||||
}
|
||||
|
||||
/// Add or update pan effect on track
|
||||
pub fn add_pan_effect(&mut self, track_id: TrackId, pan: f32) {
|
||||
let _ = self.command_tx.push(Command::AddPanEffect(track_id, pan));
|
||||
}
|
||||
|
||||
/// Add or update EQ effect on track
|
||||
pub fn add_eq_effect(&mut self, track_id: TrackId, low_db: f32, mid_db: f32, high_db: f32) {
|
||||
let _ = self.command_tx.push(Command::AddEQEffect(track_id, low_db, mid_db, high_db));
|
||||
}
|
||||
|
||||
/// Clear all effects from a track
|
||||
pub fn clear_effects(&mut self, track_id: TrackId) {
|
||||
let _ = self.command_tx.push(Command::ClearEffects(track_id));
|
||||
}
|
||||
|
||||
/// Get current playhead position in samples
|
||||
pub fn get_playhead_samples(&self) -> u64 {
|
||||
self.playhead.load(Ordering::Relaxed)
|
||||
|
|
@ -266,4 +482,39 @@ impl EngineController {
|
|||
let samples = self.playhead.load(Ordering::Relaxed);
|
||||
samples as f64 / (self.sample_rate as f64 * self.channels as f64)
|
||||
}
|
||||
|
||||
/// Create a new group track
|
||||
pub fn create_group(&mut self, name: String) {
|
||||
let _ = self.command_tx.push(Command::CreateGroup(name));
|
||||
}
|
||||
|
||||
/// Add a track to a group
|
||||
pub fn add_to_group(&mut self, track_id: TrackId, group_id: TrackId) {
|
||||
let _ = self.command_tx.push(Command::AddToGroup(track_id, group_id));
|
||||
}
|
||||
|
||||
/// Remove a track from its parent group
|
||||
pub fn remove_from_group(&mut self, track_id: TrackId) {
|
||||
let _ = self.command_tx.push(Command::RemoveFromGroup(track_id));
|
||||
}
|
||||
|
||||
/// Create a new MIDI track
|
||||
pub fn create_midi_track(&mut self, name: String) {
|
||||
let _ = self.command_tx.push(Command::CreateMidiTrack(name));
|
||||
}
|
||||
|
||||
/// Create a new MIDI clip on a track
|
||||
pub fn create_midi_clip(&mut self, track_id: TrackId, start_time: f64, duration: f64) {
|
||||
let _ = self.command_tx.push(Command::CreateMidiClip(track_id, start_time, duration));
|
||||
}
|
||||
|
||||
/// Add a MIDI note to a clip
|
||||
pub fn add_midi_note(&mut self, track_id: TrackId, clip_id: MidiClipId, time_offset: f64, note: u8, velocity: u8, duration: f64) {
|
||||
let _ = self.command_tx.push(Command::AddMidiNote(track_id, clip_id, time_offset, note, velocity, duration));
|
||||
}
|
||||
|
||||
/// Add a pre-loaded MIDI clip to a track
|
||||
pub fn add_loaded_midi_clip(&mut self, track_id: TrackId, clip: MidiClip) {
|
||||
let _ = self.command_tx.push(Command::AddLoadedMidiClip(track_id, clip));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
/// MIDI event representing a single MIDI message
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct MidiEvent {
|
||||
/// Sample position within the clip
|
||||
pub timestamp: u64,
|
||||
/// MIDI status byte (includes channel)
|
||||
pub status: u8,
|
||||
/// First data byte (note number, CC number, etc.)
|
||||
pub data1: u8,
|
||||
/// Second data byte (velocity, CC value, etc.)
|
||||
pub data2: u8,
|
||||
}
|
||||
|
||||
impl MidiEvent {
|
||||
/// Create a new MIDI event
|
||||
pub fn new(timestamp: u64, status: u8, data1: u8, data2: u8) -> Self {
|
||||
Self {
|
||||
timestamp,
|
||||
status,
|
||||
data1,
|
||||
data2,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a note on event
|
||||
pub fn note_on(timestamp: u64, channel: u8, note: u8, velocity: u8) -> Self {
|
||||
Self {
|
||||
timestamp,
|
||||
status: 0x90 | (channel & 0x0F),
|
||||
data1: note,
|
||||
data2: velocity,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a note off event
|
||||
pub fn note_off(timestamp: u64, channel: u8, note: u8, velocity: u8) -> Self {
|
||||
Self {
|
||||
timestamp,
|
||||
status: 0x80 | (channel & 0x0F),
|
||||
data1: note,
|
||||
data2: velocity,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this is a note on event (with non-zero velocity)
|
||||
pub fn is_note_on(&self) -> bool {
|
||||
(self.status & 0xF0) == 0x90 && self.data2 > 0
|
||||
}
|
||||
|
||||
/// Check if this is a note off event (or note on with zero velocity)
|
||||
pub fn is_note_off(&self) -> bool {
|
||||
(self.status & 0xF0) == 0x80 || ((self.status & 0xF0) == 0x90 && self.data2 == 0)
|
||||
}
|
||||
|
||||
/// Get the MIDI channel (0-15)
|
||||
pub fn channel(&self) -> u8 {
|
||||
self.status & 0x0F
|
||||
}
|
||||
|
||||
/// Get the message type (upper 4 bits of status)
|
||||
pub fn message_type(&self) -> u8 {
|
||||
self.status & 0xF0
|
||||
}
|
||||
}
|
||||
|
||||
/// MIDI clip ID type
|
||||
pub type MidiClipId = u32;
|
||||
|
||||
/// MIDI clip containing a sequence of MIDI events
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MidiClip {
|
||||
pub id: MidiClipId,
|
||||
pub events: Vec<MidiEvent>,
|
||||
pub start_time: f64, // Position on timeline in seconds
|
||||
pub duration: f64, // Clip duration in seconds
|
||||
pub loop_enabled: bool,
|
||||
}
|
||||
|
||||
impl MidiClip {
|
||||
/// Create a new MIDI clip
|
||||
pub fn new(id: MidiClipId, start_time: f64, duration: f64) -> Self {
|
||||
Self {
|
||||
id,
|
||||
events: Vec::new(),
|
||||
start_time,
|
||||
duration,
|
||||
loop_enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a MIDI event to the clip
|
||||
pub fn add_event(&mut self, event: MidiEvent) {
|
||||
self.events.push(event);
|
||||
// Keep events sorted by timestamp
|
||||
self.events.sort_by_key(|e| e.timestamp);
|
||||
}
|
||||
|
||||
/// Get the end time of the clip
|
||||
pub fn end_time(&self) -> f64 {
|
||||
self.start_time + self.duration
|
||||
}
|
||||
|
||||
/// Get events that should be triggered in a given time range
|
||||
///
|
||||
/// Returns events along with their absolute timestamps in samples
|
||||
pub fn get_events_in_range(
|
||||
&self,
|
||||
range_start_seconds: f64,
|
||||
range_end_seconds: f64,
|
||||
sample_rate: u32,
|
||||
) -> Vec<(u64, MidiEvent)> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
// Check if clip overlaps with the range
|
||||
if range_start_seconds >= self.end_time() || range_end_seconds <= self.start_time {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Calculate the intersection
|
||||
let play_start = range_start_seconds.max(self.start_time);
|
||||
let play_end = range_end_seconds.min(self.end_time());
|
||||
|
||||
// Convert to samples
|
||||
let range_start_samples = (range_start_seconds * sample_rate as f64) as u64;
|
||||
|
||||
// Position within the clip
|
||||
let clip_position_seconds = play_start - self.start_time;
|
||||
let clip_position_samples = (clip_position_seconds * sample_rate as f64) as u64;
|
||||
let clip_end_samples = ((play_end - self.start_time) * sample_rate as f64) as u64;
|
||||
|
||||
// Find events in this range
|
||||
// Note: Using <= for the end boundary to include events exactly at the clip end
|
||||
for event in &self.events {
|
||||
if event.timestamp >= clip_position_samples && event.timestamp <= clip_end_samples {
|
||||
// Calculate absolute timestamp in the output buffer
|
||||
let absolute_timestamp = range_start_samples + (event.timestamp - clip_position_samples);
|
||||
result.push((absolute_timestamp, *event));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,15 @@
|
|||
pub mod buffer_pool;
|
||||
pub mod clip;
|
||||
pub mod engine;
|
||||
pub mod midi;
|
||||
pub mod pool;
|
||||
pub mod project;
|
||||
pub mod track;
|
||||
|
||||
pub use buffer_pool::BufferPool;
|
||||
pub use clip::{Clip, ClipId};
|
||||
pub use engine::{Engine, EngineController};
|
||||
pub use midi::{MidiClip, MidiClipId, MidiEvent};
|
||||
pub use pool::{AudioFile as PoolAudioFile, AudioPool};
|
||||
pub use track::{Track, TrackId};
|
||||
pub use project::Project;
|
||||
pub use track::{AudioTrack, GroupTrack, MidiTrack, Track, TrackId, TrackNode};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,412 @@
|
|||
use super::buffer_pool::BufferPool;
|
||||
use super::clip::Clip;
|
||||
use super::midi::MidiClip;
|
||||
use super::pool::AudioPool;
|
||||
use super::track::{AudioTrack, GroupTrack, MidiTrack, TrackId, TrackNode};
|
||||
use crate::effects::Effect;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Project manages the hierarchical track structure
|
||||
///
|
||||
/// Tracks are stored in a flat HashMap but can be organized into groups,
|
||||
/// forming a tree structure. Groups render their children recursively.
|
||||
pub struct Project {
|
||||
tracks: HashMap<TrackId, TrackNode>,
|
||||
next_track_id: TrackId,
|
||||
root_tracks: Vec<TrackId>, // Top-level tracks (not in any group)
|
||||
}
|
||||
|
||||
impl Project {
|
||||
/// Create a new empty project
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tracks: HashMap::new(),
|
||||
next_track_id: 0,
|
||||
root_tracks: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a new unique track ID
|
||||
fn next_id(&mut self) -> TrackId {
|
||||
let id = self.next_track_id;
|
||||
self.next_track_id += 1;
|
||||
id
|
||||
}
|
||||
|
||||
/// Add an audio track to the project
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `name` - Track name
|
||||
/// * `parent_id` - Optional parent group ID
|
||||
///
|
||||
/// # Returns
|
||||
/// The new track's ID
|
||||
pub fn add_audio_track(&mut self, name: String, parent_id: Option<TrackId>) -> TrackId {
|
||||
let id = self.next_id();
|
||||
let track = AudioTrack::new(id, name);
|
||||
self.tracks.insert(id, TrackNode::Audio(track));
|
||||
|
||||
if let Some(parent) = parent_id {
|
||||
// Add to parent group
|
||||
if let Some(TrackNode::Group(group)) = self.tracks.get_mut(&parent) {
|
||||
group.add_child(id);
|
||||
}
|
||||
} else {
|
||||
// Add to root level
|
||||
self.root_tracks.push(id);
|
||||
}
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Add a group track to the project
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `name` - Group name
|
||||
/// * `parent_id` - Optional parent group ID
|
||||
///
|
||||
/// # Returns
|
||||
/// The new group's ID
|
||||
pub fn add_group_track(&mut self, name: String, parent_id: Option<TrackId>) -> TrackId {
|
||||
let id = self.next_id();
|
||||
let group = GroupTrack::new(id, name);
|
||||
self.tracks.insert(id, TrackNode::Group(group));
|
||||
|
||||
if let Some(parent) = parent_id {
|
||||
// Add to parent group
|
||||
if let Some(TrackNode::Group(parent_group)) = self.tracks.get_mut(&parent) {
|
||||
parent_group.add_child(id);
|
||||
}
|
||||
} else {
|
||||
// Add to root level
|
||||
self.root_tracks.push(id);
|
||||
}
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Add a MIDI track to the project
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `name` - Track name
|
||||
/// * `parent_id` - Optional parent group ID
|
||||
///
|
||||
/// # Returns
|
||||
/// The new track's ID
|
||||
pub fn add_midi_track(&mut self, name: String, parent_id: Option<TrackId>) -> TrackId {
|
||||
let id = self.next_id();
|
||||
let track = MidiTrack::new(id, name);
|
||||
self.tracks.insert(id, TrackNode::Midi(track));
|
||||
|
||||
if let Some(parent) = parent_id {
|
||||
// Add to parent group
|
||||
if let Some(TrackNode::Group(group)) = self.tracks.get_mut(&parent) {
|
||||
group.add_child(id);
|
||||
}
|
||||
} else {
|
||||
// Add to root level
|
||||
self.root_tracks.push(id);
|
||||
}
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Remove a track from the project
|
||||
///
|
||||
/// If the track is a group, all children are moved to the parent (or root)
|
||||
pub fn remove_track(&mut self, track_id: TrackId) {
|
||||
if let Some(node) = self.tracks.remove(&track_id) {
|
||||
// If it's a group, handle its children
|
||||
if let TrackNode::Group(group) = node {
|
||||
// Find the parent of this group
|
||||
let parent_id = self.find_parent(track_id);
|
||||
|
||||
// Move children to parent or root
|
||||
for child_id in group.children {
|
||||
if let Some(parent) = parent_id {
|
||||
if let Some(TrackNode::Group(parent_group)) = self.tracks.get_mut(&parent) {
|
||||
parent_group.add_child(child_id);
|
||||
}
|
||||
} else {
|
||||
self.root_tracks.push(child_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from parent or root
|
||||
if let Some(parent_id) = self.find_parent(track_id) {
|
||||
if let Some(TrackNode::Group(parent)) = self.tracks.get_mut(&parent_id) {
|
||||
parent.remove_child(track_id);
|
||||
}
|
||||
} else {
|
||||
self.root_tracks.retain(|&id| id != track_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the parent group of a track
|
||||
fn find_parent(&self, track_id: TrackId) -> Option<TrackId> {
|
||||
for (id, node) in &self.tracks {
|
||||
if let TrackNode::Group(group) = node {
|
||||
if group.children.contains(&track_id) {
|
||||
return Some(*id);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Move a track to a different group
|
||||
pub fn move_to_group(&mut self, track_id: TrackId, new_parent_id: TrackId) {
|
||||
// First remove from current parent
|
||||
if let Some(old_parent_id) = self.find_parent(track_id) {
|
||||
if let Some(TrackNode::Group(parent)) = self.tracks.get_mut(&old_parent_id) {
|
||||
parent.remove_child(track_id);
|
||||
}
|
||||
} else {
|
||||
// Remove from root
|
||||
self.root_tracks.retain(|&id| id != track_id);
|
||||
}
|
||||
|
||||
// Add to new parent
|
||||
if let Some(TrackNode::Group(new_parent)) = self.tracks.get_mut(&new_parent_id) {
|
||||
new_parent.add_child(track_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Move a track to the root level (remove from any group)
|
||||
pub fn move_to_root(&mut self, track_id: TrackId) {
|
||||
// Remove from current parent if any
|
||||
if let Some(parent_id) = self.find_parent(track_id) {
|
||||
if let Some(TrackNode::Group(parent)) = self.tracks.get_mut(&parent_id) {
|
||||
parent.remove_child(track_id);
|
||||
}
|
||||
// Add to root if not already there
|
||||
if !self.root_tracks.contains(&track_id) {
|
||||
self.root_tracks.push(track_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to a track node
|
||||
pub fn get_track(&self, track_id: TrackId) -> Option<&TrackNode> {
|
||||
self.tracks.get(&track_id)
|
||||
}
|
||||
|
||||
/// Get a mutable reference to a track node
|
||||
pub fn get_track_mut(&mut self, track_id: TrackId) -> Option<&mut TrackNode> {
|
||||
self.tracks.get_mut(&track_id)
|
||||
}
|
||||
|
||||
/// Get all root-level track IDs
|
||||
pub fn root_tracks(&self) -> &[TrackId] {
|
||||
&self.root_tracks
|
||||
}
|
||||
|
||||
/// Get the number of tracks in the project
|
||||
pub fn track_count(&self) -> usize {
|
||||
self.tracks.len()
|
||||
}
|
||||
|
||||
/// Check if any track is soloed
|
||||
pub fn any_solo(&self) -> bool {
|
||||
self.tracks.values().any(|node| node.is_solo())
|
||||
}
|
||||
|
||||
/// Add a clip to an audio track
|
||||
pub fn add_clip(&mut self, track_id: TrackId, clip: Clip) -> Result<(), &'static str> {
|
||||
if let Some(TrackNode::Audio(track)) = self.tracks.get_mut(&track_id) {
|
||||
track.add_clip(clip);
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Track not found or is not an audio track")
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a MIDI clip to a MIDI track
|
||||
pub fn add_midi_clip(&mut self, track_id: TrackId, clip: MidiClip) -> Result<(), &'static str> {
|
||||
if let Some(TrackNode::Midi(track)) = self.tracks.get_mut(&track_id) {
|
||||
track.add_clip(clip);
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Track not found or is not a MIDI track")
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an effect to a track (audio, MIDI, or group)
|
||||
pub fn add_effect(&mut self, track_id: TrackId, effect: Box<dyn Effect>) -> Result<(), &'static str> {
|
||||
match self.tracks.get_mut(&track_id) {
|
||||
Some(TrackNode::Audio(track)) => {
|
||||
track.add_effect(effect);
|
||||
Ok(())
|
||||
}
|
||||
Some(TrackNode::Midi(track)) => {
|
||||
track.add_effect(effect);
|
||||
Ok(())
|
||||
}
|
||||
Some(TrackNode::Group(group)) => {
|
||||
group.add_effect(effect);
|
||||
Ok(())
|
||||
}
|
||||
None => Err("Track not found"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear effects from a track
|
||||
pub fn clear_effects(&mut self, track_id: TrackId) -> Result<(), &'static str> {
|
||||
match self.tracks.get_mut(&track_id) {
|
||||
Some(TrackNode::Audio(track)) => {
|
||||
track.clear_effects();
|
||||
Ok(())
|
||||
}
|
||||
Some(TrackNode::Midi(track)) => {
|
||||
track.clear_effects();
|
||||
Ok(())
|
||||
}
|
||||
Some(TrackNode::Group(group)) => {
|
||||
group.clear_effects();
|
||||
Ok(())
|
||||
}
|
||||
None => Err("Track not found"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Render all root tracks into the output buffer
|
||||
pub fn render(
|
||||
&mut self,
|
||||
output: &mut [f32],
|
||||
pool: &AudioPool,
|
||||
buffer_pool: &mut BufferPool,
|
||||
playhead_seconds: f64,
|
||||
sample_rate: u32,
|
||||
channels: u32,
|
||||
) {
|
||||
output.fill(0.0);
|
||||
|
||||
let any_solo = self.any_solo();
|
||||
|
||||
// Render each root track
|
||||
for &track_id in &self.root_tracks.clone() {
|
||||
self.render_track(
|
||||
track_id,
|
||||
output,
|
||||
pool,
|
||||
buffer_pool,
|
||||
playhead_seconds,
|
||||
sample_rate,
|
||||
channels,
|
||||
any_solo,
|
||||
false, // root tracks are not inside a soloed parent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively render a track (audio or group) into the output buffer
|
||||
fn render_track(
|
||||
&mut self,
|
||||
track_id: TrackId,
|
||||
output: &mut [f32],
|
||||
pool: &AudioPool,
|
||||
buffer_pool: &mut BufferPool,
|
||||
playhead_seconds: f64,
|
||||
sample_rate: u32,
|
||||
channels: u32,
|
||||
any_solo: bool,
|
||||
parent_is_soloed: bool,
|
||||
) {
|
||||
// Check if track should be rendered based on mute/solo
|
||||
let should_render = match self.tracks.get(&track_id) {
|
||||
Some(TrackNode::Audio(track)) => {
|
||||
// If parent is soloed, only check mute state
|
||||
// Otherwise, check normal solo logic
|
||||
if parent_is_soloed {
|
||||
!track.muted
|
||||
} else {
|
||||
track.is_active(any_solo)
|
||||
}
|
||||
}
|
||||
Some(TrackNode::Midi(track)) => {
|
||||
// Same logic for MIDI tracks
|
||||
if parent_is_soloed {
|
||||
!track.muted
|
||||
} else {
|
||||
track.is_active(any_solo)
|
||||
}
|
||||
}
|
||||
Some(TrackNode::Group(group)) => {
|
||||
// Same logic for groups
|
||||
if parent_is_soloed {
|
||||
!group.muted
|
||||
} else {
|
||||
group.is_active(any_solo)
|
||||
}
|
||||
}
|
||||
None => return,
|
||||
};
|
||||
|
||||
if !should_render {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle audio track vs MIDI track vs group track
|
||||
match self.tracks.get_mut(&track_id) {
|
||||
Some(TrackNode::Audio(track)) => {
|
||||
// Render audio track directly into output
|
||||
track.render(output, pool, playhead_seconds, sample_rate, channels);
|
||||
}
|
||||
Some(TrackNode::Midi(track)) => {
|
||||
// Render MIDI track directly into output
|
||||
track.render(output, playhead_seconds, sample_rate, channels);
|
||||
}
|
||||
Some(TrackNode::Group(group)) => {
|
||||
// Get children IDs and check if this group is soloed
|
||||
let children: Vec<TrackId> = group.children.clone();
|
||||
let this_group_is_soloed = group.solo;
|
||||
|
||||
// Acquire a temporary buffer for the group mix
|
||||
let mut group_buffer = buffer_pool.acquire();
|
||||
group_buffer.resize(output.len(), 0.0);
|
||||
group_buffer.fill(0.0);
|
||||
|
||||
// Recursively render all children into the group buffer
|
||||
// If this group is soloed (or parent was soloed), children inherit that state
|
||||
let children_parent_soloed = parent_is_soloed || this_group_is_soloed;
|
||||
for &child_id in &children {
|
||||
self.render_track(
|
||||
child_id,
|
||||
&mut group_buffer,
|
||||
pool,
|
||||
buffer_pool,
|
||||
playhead_seconds,
|
||||
sample_rate,
|
||||
channels,
|
||||
any_solo,
|
||||
children_parent_soloed,
|
||||
);
|
||||
}
|
||||
|
||||
// Apply group effects
|
||||
if let Some(TrackNode::Group(group)) = self.tracks.get_mut(&track_id) {
|
||||
for effect in &mut group.effects {
|
||||
effect.process(&mut group_buffer, channels as usize, sample_rate);
|
||||
}
|
||||
|
||||
// Apply group volume and mix into output
|
||||
for (out_sample, group_sample) in output.iter_mut().zip(group_buffer.iter()) {
|
||||
*out_sample += group_sample * group.volume;
|
||||
}
|
||||
}
|
||||
|
||||
// Release buffer back to pool
|
||||
buffer_pool.release(group_buffer);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Project {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +1,300 @@
|
|||
use super::clip::Clip;
|
||||
use super::midi::MidiClip;
|
||||
use super::pool::AudioPool;
|
||||
use crate::effects::{Effect, SimpleSynth};
|
||||
|
||||
/// Track ID type
|
||||
pub type TrackId = u32;
|
||||
|
||||
/// Audio track for Phase 4 with clips
|
||||
pub struct Track {
|
||||
/// Type alias for backwards compatibility
|
||||
pub type Track = AudioTrack;
|
||||
|
||||
/// Node in the track hierarchy - can be an audio track, MIDI track, or a group
|
||||
pub enum TrackNode {
|
||||
Audio(AudioTrack),
|
||||
Midi(MidiTrack),
|
||||
Group(GroupTrack),
|
||||
}
|
||||
|
||||
impl TrackNode {
|
||||
/// Get the track ID
|
||||
pub fn id(&self) -> TrackId {
|
||||
match self {
|
||||
TrackNode::Audio(track) => track.id,
|
||||
TrackNode::Midi(track) => track.id,
|
||||
TrackNode::Group(group) => group.id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the track name
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
TrackNode::Audio(track) => &track.name,
|
||||
TrackNode::Midi(track) => &track.name,
|
||||
TrackNode::Group(group) => &group.name,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get muted state
|
||||
pub fn is_muted(&self) -> bool {
|
||||
match self {
|
||||
TrackNode::Audio(track) => track.muted,
|
||||
TrackNode::Midi(track) => track.muted,
|
||||
TrackNode::Group(group) => group.muted,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get solo state
|
||||
pub fn is_solo(&self) -> bool {
|
||||
match self {
|
||||
TrackNode::Audio(track) => track.solo,
|
||||
TrackNode::Midi(track) => track.solo,
|
||||
TrackNode::Group(group) => group.solo,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set volume
|
||||
pub fn set_volume(&mut self, volume: f32) {
|
||||
match self {
|
||||
TrackNode::Audio(track) => track.set_volume(volume),
|
||||
TrackNode::Midi(track) => track.set_volume(volume),
|
||||
TrackNode::Group(group) => group.set_volume(volume),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set muted state
|
||||
pub fn set_muted(&mut self, muted: bool) {
|
||||
match self {
|
||||
TrackNode::Audio(track) => track.set_muted(muted),
|
||||
TrackNode::Midi(track) => track.set_muted(muted),
|
||||
TrackNode::Group(group) => group.set_muted(muted),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set solo state
|
||||
pub fn set_solo(&mut self, solo: bool) {
|
||||
match self {
|
||||
TrackNode::Audio(track) => track.set_solo(solo),
|
||||
TrackNode::Midi(track) => track.set_solo(solo),
|
||||
TrackNode::Group(group) => group.set_solo(solo),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Group track that contains other tracks (audio or groups)
|
||||
pub struct GroupTrack {
|
||||
pub id: TrackId,
|
||||
pub name: String,
|
||||
pub clips: Vec<Clip>,
|
||||
pub children: Vec<TrackId>,
|
||||
pub effects: Vec<Box<dyn Effect>>,
|
||||
pub volume: f32,
|
||||
pub muted: bool,
|
||||
pub solo: bool,
|
||||
}
|
||||
|
||||
impl Track {
|
||||
/// Create a new track with default settings
|
||||
impl GroupTrack {
|
||||
/// Create a new group track
|
||||
pub fn new(id: TrackId, name: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
children: Vec::new(),
|
||||
effects: Vec::new(),
|
||||
volume: 1.0,
|
||||
muted: false,
|
||||
solo: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a child track to this group
|
||||
pub fn add_child(&mut self, track_id: TrackId) {
|
||||
if !self.children.contains(&track_id) {
|
||||
self.children.push(track_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a child track from this group
|
||||
pub fn remove_child(&mut self, track_id: TrackId) {
|
||||
self.children.retain(|&id| id != track_id);
|
||||
}
|
||||
|
||||
/// Add an effect to the group's effect chain
|
||||
pub fn add_effect(&mut self, effect: Box<dyn Effect>) {
|
||||
self.effects.push(effect);
|
||||
}
|
||||
|
||||
/// Clear all effects from the group
|
||||
pub fn clear_effects(&mut self) {
|
||||
self.effects.clear();
|
||||
}
|
||||
|
||||
/// Set group volume
|
||||
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 group should be audible given the solo state
|
||||
pub fn is_active(&self, any_solo: bool) -> bool {
|
||||
!self.muted && (!any_solo || self.solo)
|
||||
}
|
||||
}
|
||||
|
||||
/// MIDI track with MIDI clips and a virtual instrument
|
||||
pub struct MidiTrack {
|
||||
pub id: TrackId,
|
||||
pub name: String,
|
||||
pub clips: Vec<MidiClip>,
|
||||
pub instrument: SimpleSynth,
|
||||
pub effects: Vec<Box<dyn Effect>>,
|
||||
pub volume: f32,
|
||||
pub muted: bool,
|
||||
pub solo: bool,
|
||||
}
|
||||
|
||||
impl MidiTrack {
|
||||
/// Create a new MIDI track with default settings
|
||||
pub fn new(id: TrackId, name: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
clips: Vec::new(),
|
||||
instrument: SimpleSynth::new(),
|
||||
effects: Vec::new(),
|
||||
volume: 1.0,
|
||||
muted: false,
|
||||
solo: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an effect to the track's effect chain
|
||||
pub fn add_effect(&mut self, effect: Box<dyn Effect>) {
|
||||
self.effects.push(effect);
|
||||
}
|
||||
|
||||
/// Clear all effects from the track
|
||||
pub fn clear_effects(&mut self) {
|
||||
self.effects.clear();
|
||||
}
|
||||
|
||||
/// Add a MIDI clip to this track
|
||||
pub fn add_clip(&mut self, clip: MidiClip) {
|
||||
self.clips.push(clip);
|
||||
}
|
||||
|
||||
/// Set track volume
|
||||
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
|
||||
pub fn is_active(&self, any_solo: bool) -> bool {
|
||||
!self.muted && (!any_solo || self.solo)
|
||||
}
|
||||
|
||||
/// Render this MIDI track into the output buffer
|
||||
pub fn render(
|
||||
&mut self,
|
||||
output: &mut [f32],
|
||||
playhead_seconds: f64,
|
||||
sample_rate: u32,
|
||||
channels: u32,
|
||||
) {
|
||||
let buffer_duration_seconds = output.len() as f64 / (sample_rate as f64 * channels as f64);
|
||||
let buffer_end_seconds = playhead_seconds + buffer_duration_seconds;
|
||||
|
||||
// Collect MIDI events from all clips that overlap with current time range
|
||||
for clip in &self.clips {
|
||||
let events = clip.get_events_in_range(
|
||||
playhead_seconds,
|
||||
buffer_end_seconds,
|
||||
sample_rate,
|
||||
);
|
||||
|
||||
// Queue events in the instrument
|
||||
for (_timestamp, event) in events {
|
||||
self.instrument.queue_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate audio from the instrument
|
||||
self.instrument.process(output, channels as usize, sample_rate);
|
||||
|
||||
// Apply effect chain
|
||||
for effect in &mut self.effects {
|
||||
effect.process(output, channels as usize, sample_rate);
|
||||
}
|
||||
|
||||
// Apply track volume
|
||||
for sample in output.iter_mut() {
|
||||
*sample *= self.volume;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Audio track with clips and effect chain
|
||||
pub struct AudioTrack {
|
||||
pub id: TrackId,
|
||||
pub name: String,
|
||||
pub clips: Vec<Clip>,
|
||||
pub effects: Vec<Box<dyn Effect>>,
|
||||
pub volume: f32,
|
||||
pub muted: bool,
|
||||
pub solo: bool,
|
||||
}
|
||||
|
||||
impl AudioTrack {
|
||||
/// Create a new audio track with default settings
|
||||
pub fn new(id: TrackId, name: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
clips: Vec::new(),
|
||||
effects: Vec::new(),
|
||||
volume: 1.0,
|
||||
muted: false,
|
||||
solo: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an effect to the track's effect chain
|
||||
pub fn add_effect(&mut self, effect: Box<dyn Effect>) {
|
||||
self.effects.push(effect);
|
||||
}
|
||||
|
||||
/// Remove an effect from the chain by index
|
||||
pub fn remove_effect(&mut self, index: usize) -> Option<Box<dyn Effect>> {
|
||||
if index < self.effects.len() {
|
||||
Some(self.effects.remove(index))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all effects from the track
|
||||
pub fn clear_effects(&mut self) {
|
||||
self.effects.clear();
|
||||
}
|
||||
|
||||
/// Add a clip to this track
|
||||
pub fn add_clip(&mut self, clip: Clip) {
|
||||
self.clips.push(clip);
|
||||
|
|
@ -55,7 +323,7 @@ impl Track {
|
|||
/// Render this track into the output buffer at a given timeline position
|
||||
/// Returns the number of samples actually rendered
|
||||
pub fn render(
|
||||
&self,
|
||||
&mut self,
|
||||
output: &mut [f32],
|
||||
pool: &AudioPool,
|
||||
playhead_seconds: f64,
|
||||
|
|
@ -82,6 +350,16 @@ impl Track {
|
|||
}
|
||||
}
|
||||
|
||||
// Apply effect chain
|
||||
for effect in &mut self.effects {
|
||||
effect.process(output, channels as usize, sample_rate);
|
||||
}
|
||||
|
||||
// Apply track volume
|
||||
for sample in output.iter_mut() {
|
||||
*sample *= self.volume;
|
||||
}
|
||||
|
||||
rendered
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::audio::{ClipId, TrackId};
|
||||
use crate::audio::{ClipId, MidiClip, MidiClipId, TrackId};
|
||||
|
||||
/// Commands sent from UI/control thread to audio thread
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -24,6 +24,34 @@ pub enum Command {
|
|||
// Clip management commands
|
||||
/// Move a clip to a new timeline position
|
||||
MoveClip(TrackId, ClipId, f64),
|
||||
|
||||
// Effect management commands
|
||||
/// Add or update gain effect on track (gain in dB)
|
||||
AddGainEffect(TrackId, f32),
|
||||
/// Add or update pan effect on track (-1.0 = left, 0.0 = center, 1.0 = right)
|
||||
AddPanEffect(TrackId, f32),
|
||||
/// Add or update EQ effect on track (low_db, mid_db, high_db)
|
||||
AddEQEffect(TrackId, f32, f32, f32),
|
||||
/// Clear all effects from a track
|
||||
ClearEffects(TrackId),
|
||||
|
||||
// Group management commands
|
||||
/// Create a new group track with a name
|
||||
CreateGroup(String),
|
||||
/// Add a track to a group (track_id, group_id)
|
||||
AddToGroup(TrackId, TrackId),
|
||||
/// Remove a track from its parent group
|
||||
RemoveFromGroup(TrackId),
|
||||
|
||||
// MIDI commands
|
||||
/// Create a new MIDI track with a name
|
||||
CreateMidiTrack(String),
|
||||
/// Create a new MIDI clip on a track (track_id, start_time, duration)
|
||||
CreateMidiClip(TrackId, f64, f64),
|
||||
/// Add a MIDI note to a clip (track_id, clip_id, time_offset, note, velocity, duration)
|
||||
AddMidiNote(TrackId, MidiClipId, f64, u8, u8, f64),
|
||||
/// Add a pre-loaded MIDI clip to a track
|
||||
AddLoadedMidiClip(TrackId, MidiClip),
|
||||
}
|
||||
|
||||
/// Events sent from audio thread back to UI/control thread
|
||||
|
|
@ -35,4 +63,6 @@ pub enum AudioEvent {
|
|||
PlaybackStopped,
|
||||
/// Audio buffer underrun detected
|
||||
BufferUnderrun,
|
||||
/// A new track was created (track_id, is_group, name)
|
||||
TrackCreated(TrackId, bool, String),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
use std::f32::consts::PI;
|
||||
|
||||
/// Biquad filter implementation (2-pole IIR filter)
|
||||
///
|
||||
/// Transfer function: H(z) = (b0 + b1*z^-1 + b2*z^-2) / (1 + a1*z^-1 + a2*z^-2)
|
||||
#[derive(Clone)]
|
||||
pub struct BiquadFilter {
|
||||
// Filter coefficients
|
||||
b0: f32,
|
||||
b1: f32,
|
||||
b2: f32,
|
||||
a1: f32,
|
||||
a2: f32,
|
||||
|
||||
// State variables (per channel, supporting up to 2 channels)
|
||||
x1: [f32; 2],
|
||||
x2: [f32; 2],
|
||||
y1: [f32; 2],
|
||||
y2: [f32; 2],
|
||||
}
|
||||
|
||||
impl BiquadFilter {
|
||||
/// Create a new biquad filter with unity gain (pass-through)
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
b0: 1.0,
|
||||
b1: 0.0,
|
||||
b2: 0.0,
|
||||
a1: 0.0,
|
||||
a2: 0.0,
|
||||
x1: [0.0; 2],
|
||||
x2: [0.0; 2],
|
||||
y1: [0.0; 2],
|
||||
y2: [0.0; 2],
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a lowpass filter
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `frequency` - Cutoff frequency in Hz
|
||||
/// * `q` - Quality factor (resonance), typically 0.707 for Butterworth
|
||||
/// * `sample_rate` - Sample rate in Hz
|
||||
pub fn lowpass(frequency: f32, q: f32, sample_rate: f32) -> Self {
|
||||
let mut filter = Self::new();
|
||||
filter.set_lowpass(frequency, q, sample_rate);
|
||||
filter
|
||||
}
|
||||
|
||||
/// Create a highpass filter
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `frequency` - Cutoff frequency in Hz
|
||||
/// * `q` - Quality factor (resonance), typically 0.707 for Butterworth
|
||||
/// * `sample_rate` - Sample rate in Hz
|
||||
pub fn highpass(frequency: f32, q: f32, sample_rate: f32) -> Self {
|
||||
let mut filter = Self::new();
|
||||
filter.set_highpass(frequency, q, sample_rate);
|
||||
filter
|
||||
}
|
||||
|
||||
/// Create a peaking EQ filter
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `frequency` - Center frequency in Hz
|
||||
/// * `q` - Quality factor (bandwidth)
|
||||
/// * `gain_db` - Gain in decibels
|
||||
/// * `sample_rate` - Sample rate in Hz
|
||||
pub fn peaking(frequency: f32, q: f32, gain_db: f32, sample_rate: f32) -> Self {
|
||||
let mut filter = Self::new();
|
||||
filter.set_peaking(frequency, q, gain_db, sample_rate);
|
||||
filter
|
||||
}
|
||||
|
||||
/// Set coefficients for a lowpass filter
|
||||
pub fn set_lowpass(&mut self, frequency: f32, q: f32, sample_rate: f32) {
|
||||
let omega = 2.0 * PI * frequency / sample_rate;
|
||||
let sin_omega = omega.sin();
|
||||
let cos_omega = omega.cos();
|
||||
let alpha = sin_omega / (2.0 * q);
|
||||
|
||||
let a0 = 1.0 + alpha;
|
||||
self.b0 = ((1.0 - cos_omega) / 2.0) / a0;
|
||||
self.b1 = (1.0 - cos_omega) / a0;
|
||||
self.b2 = ((1.0 - cos_omega) / 2.0) / a0;
|
||||
self.a1 = (-2.0 * cos_omega) / a0;
|
||||
self.a2 = (1.0 - alpha) / a0;
|
||||
}
|
||||
|
||||
/// Set coefficients for a highpass filter
|
||||
pub fn set_highpass(&mut self, frequency: f32, q: f32, sample_rate: f32) {
|
||||
let omega = 2.0 * PI * frequency / sample_rate;
|
||||
let sin_omega = omega.sin();
|
||||
let cos_omega = omega.cos();
|
||||
let alpha = sin_omega / (2.0 * q);
|
||||
|
||||
let a0 = 1.0 + alpha;
|
||||
self.b0 = ((1.0 + cos_omega) / 2.0) / a0;
|
||||
self.b1 = -(1.0 + cos_omega) / a0;
|
||||
self.b2 = ((1.0 + cos_omega) / 2.0) / a0;
|
||||
self.a1 = (-2.0 * cos_omega) / a0;
|
||||
self.a2 = (1.0 - alpha) / a0;
|
||||
}
|
||||
|
||||
/// Set coefficients for a peaking EQ filter
|
||||
pub fn set_peaking(&mut self, frequency: f32, q: f32, gain_db: f32, sample_rate: f32) {
|
||||
let omega = 2.0 * PI * frequency / sample_rate;
|
||||
let sin_omega = omega.sin();
|
||||
let cos_omega = omega.cos();
|
||||
let a_gain = 10.0_f32.powf(gain_db / 40.0);
|
||||
let alpha = sin_omega / (2.0 * q);
|
||||
|
||||
let a0 = 1.0 + alpha / a_gain;
|
||||
self.b0 = (1.0 + alpha * a_gain) / a0;
|
||||
self.b1 = (-2.0 * cos_omega) / a0;
|
||||
self.b2 = (1.0 - alpha * a_gain) / a0;
|
||||
self.a1 = (-2.0 * cos_omega) / a0;
|
||||
self.a2 = (1.0 - alpha / a_gain) / a0;
|
||||
}
|
||||
|
||||
/// Process a single sample
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `input` - Input sample
|
||||
/// * `channel` - Channel index (0 or 1)
|
||||
///
|
||||
/// # Returns
|
||||
/// Filtered output sample
|
||||
#[inline]
|
||||
pub fn process_sample(&mut self, input: f32, channel: usize) -> f32 {
|
||||
let channel = channel.min(1); // Clamp to 0 or 1
|
||||
|
||||
// Direct Form II Transposed implementation
|
||||
let output = self.b0 * input + self.x1[channel];
|
||||
|
||||
self.x1[channel] = self.b1 * input - self.a1 * output + self.x2[channel];
|
||||
self.x2[channel] = self.b2 * input - self.a2 * output;
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
/// Process a buffer of interleaved samples
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `buffer` - Interleaved audio samples
|
||||
/// * `channels` - Number of channels
|
||||
pub fn process_buffer(&mut self, buffer: &mut [f32], channels: usize) {
|
||||
if channels == 1 {
|
||||
// Mono
|
||||
for sample in buffer.iter_mut() {
|
||||
*sample = self.process_sample(*sample, 0);
|
||||
}
|
||||
} else if channels == 2 {
|
||||
// Stereo
|
||||
for frame in buffer.chunks_exact_mut(2) {
|
||||
frame[0] = self.process_sample(frame[0], 0);
|
||||
frame[1] = self.process_sample(frame[1], 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset filter state (clear delay lines)
|
||||
pub fn reset(&mut self) {
|
||||
self.x1 = [0.0; 2];
|
||||
self.x2 = [0.0; 2];
|
||||
self.y1 = [0.0; 2];
|
||||
self.y2 = [0.0; 2];
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BiquadFilter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
pub mod biquad;
|
||||
|
||||
pub use biquad::BiquadFilter;
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/// Audio effect processor trait
|
||||
///
|
||||
/// All effects must be Send to be usable in the audio thread.
|
||||
/// Effects should be real-time safe: no allocations, no blocking operations.
|
||||
pub trait Effect: Send {
|
||||
/// Process audio buffer in-place
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `buffer` - Interleaved audio samples to process
|
||||
/// * `channels` - Number of audio channels (2 for stereo)
|
||||
/// * `sample_rate` - Sample rate in Hz
|
||||
fn process(&mut self, buffer: &mut [f32], channels: usize, sample_rate: u32);
|
||||
|
||||
/// Set an effect parameter
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - Parameter identifier
|
||||
/// * `value` - Parameter value (normalized or specific units depending on parameter)
|
||||
fn set_parameter(&mut self, id: u32, value: f32);
|
||||
|
||||
/// Get an effect parameter value
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - Parameter identifier
|
||||
///
|
||||
/// # Returns
|
||||
/// Current parameter value
|
||||
fn get_parameter(&self, id: u32) -> f32;
|
||||
|
||||
/// Reset effect state (clear delays, resonances, etc.)
|
||||
fn reset(&mut self);
|
||||
|
||||
/// Get the effect name
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
use super::Effect;
|
||||
use crate::dsp::BiquadFilter;
|
||||
|
||||
/// Simple 3-band EQ (low shelf, mid peak, high shelf)
|
||||
///
|
||||
/// Parameters:
|
||||
/// - 0: Low gain in dB (-12.0 to +12.0)
|
||||
/// - 1: Mid gain in dB (-12.0 to +12.0)
|
||||
/// - 2: High gain in dB (-12.0 to +12.0)
|
||||
/// - 3: Low frequency in Hz (default: 250)
|
||||
/// - 4: Mid frequency in Hz (default: 1000)
|
||||
/// - 5: High frequency in Hz (default: 8000)
|
||||
pub struct SimpleEQ {
|
||||
low_gain: f32,
|
||||
mid_gain: f32,
|
||||
high_gain: f32,
|
||||
low_freq: f32,
|
||||
mid_freq: f32,
|
||||
high_freq: f32,
|
||||
|
||||
low_filter: BiquadFilter,
|
||||
mid_filter: BiquadFilter,
|
||||
high_filter: BiquadFilter,
|
||||
|
||||
sample_rate: f32,
|
||||
}
|
||||
|
||||
impl SimpleEQ {
|
||||
/// Create a new SimpleEQ with flat response
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
low_gain: 0.0,
|
||||
mid_gain: 0.0,
|
||||
high_gain: 0.0,
|
||||
low_freq: 250.0,
|
||||
mid_freq: 1000.0,
|
||||
high_freq: 8000.0,
|
||||
low_filter: BiquadFilter::new(),
|
||||
mid_filter: BiquadFilter::new(),
|
||||
high_filter: BiquadFilter::new(),
|
||||
sample_rate: 48000.0, // Default, will be updated on first process
|
||||
}
|
||||
}
|
||||
|
||||
/// Set low band gain in decibels
|
||||
pub fn set_low_gain(&mut self, gain_db: f32) {
|
||||
self.low_gain = gain_db.clamp(-12.0, 12.0);
|
||||
self.update_filters();
|
||||
}
|
||||
|
||||
/// Set mid band gain in decibels
|
||||
pub fn set_mid_gain(&mut self, gain_db: f32) {
|
||||
self.mid_gain = gain_db.clamp(-12.0, 12.0);
|
||||
self.update_filters();
|
||||
}
|
||||
|
||||
/// Set high band gain in decibels
|
||||
pub fn set_high_gain(&mut self, gain_db: f32) {
|
||||
self.high_gain = gain_db.clamp(-12.0, 12.0);
|
||||
self.update_filters();
|
||||
}
|
||||
|
||||
/// Set low band frequency
|
||||
pub fn set_low_freq(&mut self, freq: f32) {
|
||||
self.low_freq = freq.clamp(20.0, 500.0);
|
||||
self.update_filters();
|
||||
}
|
||||
|
||||
/// Set mid band frequency
|
||||
pub fn set_mid_freq(&mut self, freq: f32) {
|
||||
self.mid_freq = freq.clamp(200.0, 5000.0);
|
||||
self.update_filters();
|
||||
}
|
||||
|
||||
/// Set high band frequency
|
||||
pub fn set_high_freq(&mut self, freq: f32) {
|
||||
self.high_freq = freq.clamp(2000.0, 20000.0);
|
||||
self.update_filters();
|
||||
}
|
||||
|
||||
/// Update filter coefficients based on current parameters
|
||||
fn update_filters(&mut self) {
|
||||
// Only update if sample rate has been set
|
||||
if self.sample_rate > 0.0 {
|
||||
// Use peaking filters for all bands
|
||||
// Q factor of 1.0 gives a moderate bandwidth
|
||||
self.low_filter.set_peaking(self.low_freq, 1.0, self.low_gain, self.sample_rate);
|
||||
self.mid_filter.set_peaking(self.mid_freq, 1.0, self.mid_gain, self.sample_rate);
|
||||
self.high_filter.set_peaking(self.high_freq, 1.0, self.high_gain, self.sample_rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SimpleEQ {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Effect for SimpleEQ {
|
||||
fn process(&mut self, buffer: &mut [f32], channels: usize, sample_rate: u32) {
|
||||
// Update sample rate if it changed
|
||||
let sr = sample_rate as f32;
|
||||
if (self.sample_rate - sr).abs() > 0.1 {
|
||||
self.sample_rate = sr;
|
||||
self.update_filters();
|
||||
}
|
||||
|
||||
// Process through each filter in series
|
||||
self.low_filter.process_buffer(buffer, channels);
|
||||
self.mid_filter.process_buffer(buffer, channels);
|
||||
self.high_filter.process_buffer(buffer, channels);
|
||||
}
|
||||
|
||||
fn set_parameter(&mut self, id: u32, value: f32) {
|
||||
match id {
|
||||
0 => self.set_low_gain(value),
|
||||
1 => self.set_mid_gain(value),
|
||||
2 => self.set_high_gain(value),
|
||||
3 => self.set_low_freq(value),
|
||||
4 => self.set_mid_freq(value),
|
||||
5 => self.set_high_freq(value),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_parameter(&self, id: u32) -> f32 {
|
||||
match id {
|
||||
0 => self.low_gain,
|
||||
1 => self.mid_gain,
|
||||
2 => self.high_gain,
|
||||
3 => self.low_freq,
|
||||
4 => self.mid_freq,
|
||||
5 => self.high_freq,
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.low_filter.reset();
|
||||
self.mid_filter.reset();
|
||||
self.high_filter.reset();
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"SimpleEQ"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
use super::Effect;
|
||||
|
||||
/// Simple gain/volume effect
|
||||
///
|
||||
/// Parameters:
|
||||
/// - 0: Gain in dB (-60.0 to +12.0)
|
||||
pub struct GainEffect {
|
||||
gain_db: f32,
|
||||
gain_linear: f32,
|
||||
}
|
||||
|
||||
impl GainEffect {
|
||||
/// Create a new gain effect with 0 dB (unity) gain
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
gain_db: 0.0,
|
||||
gain_linear: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a gain effect with a specific dB value
|
||||
pub fn with_gain_db(gain_db: f32) -> Self {
|
||||
let gain_linear = db_to_linear(gain_db);
|
||||
Self {
|
||||
gain_db,
|
||||
gain_linear,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set gain in decibels
|
||||
pub fn set_gain_db(&mut self, gain_db: f32) {
|
||||
self.gain_db = gain_db.clamp(-60.0, 12.0);
|
||||
self.gain_linear = db_to_linear(self.gain_db);
|
||||
}
|
||||
|
||||
/// Get current gain in decibels
|
||||
pub fn gain_db(&self) -> f32 {
|
||||
self.gain_db
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GainEffect {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Effect for GainEffect {
|
||||
fn process(&mut self, buffer: &mut [f32], _channels: usize, _sample_rate: u32) {
|
||||
for sample in buffer.iter_mut() {
|
||||
*sample *= self.gain_linear;
|
||||
}
|
||||
}
|
||||
|
||||
fn set_parameter(&mut self, id: u32, value: f32) {
|
||||
if id == 0 {
|
||||
self.set_gain_db(value);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_parameter(&self, id: u32) -> f32 {
|
||||
if id == 0 {
|
||||
self.gain_db
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
// Gain has no state to reset
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"Gain"
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert decibels to linear gain
|
||||
#[inline]
|
||||
fn db_to_linear(db: f32) -> f32 {
|
||||
if db <= -60.0 {
|
||||
0.0
|
||||
} else {
|
||||
10.0_f32.powf(db / 20.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert linear gain to decibels
|
||||
#[inline]
|
||||
#[allow(dead_code)]
|
||||
fn linear_to_db(linear: f32) -> f32 {
|
||||
if linear <= 0.0 {
|
||||
-60.0
|
||||
} else {
|
||||
20.0 * linear.log10()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
pub mod effect_trait;
|
||||
pub mod eq;
|
||||
pub mod gain;
|
||||
pub mod pan;
|
||||
pub mod synth;
|
||||
|
||||
pub use effect_trait::Effect;
|
||||
pub use eq::SimpleEQ;
|
||||
pub use gain::GainEffect;
|
||||
pub use pan::PanEffect;
|
||||
pub use synth::SimpleSynth;
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
use super::Effect;
|
||||
|
||||
/// Stereo panning effect using constant-power panning law
|
||||
///
|
||||
/// Parameters:
|
||||
/// - 0: Pan position (-1.0 = full left, 0.0 = center, +1.0 = full right)
|
||||
pub struct PanEffect {
|
||||
pan: f32,
|
||||
left_gain: f32,
|
||||
right_gain: f32,
|
||||
}
|
||||
|
||||
impl PanEffect {
|
||||
/// Create a new pan effect with center panning
|
||||
pub fn new() -> Self {
|
||||
let mut effect = Self {
|
||||
pan: 0.0,
|
||||
left_gain: 1.0,
|
||||
right_gain: 1.0,
|
||||
};
|
||||
effect.update_gains();
|
||||
effect
|
||||
}
|
||||
|
||||
/// Create a pan effect with a specific pan position
|
||||
pub fn with_pan(pan: f32) -> Self {
|
||||
let mut effect = Self {
|
||||
pan: pan.clamp(-1.0, 1.0),
|
||||
left_gain: 1.0,
|
||||
right_gain: 1.0,
|
||||
};
|
||||
effect.update_gains();
|
||||
effect
|
||||
}
|
||||
|
||||
/// Set pan position (-1.0 = left, 0.0 = center, +1.0 = right)
|
||||
pub fn set_pan(&mut self, pan: f32) {
|
||||
self.pan = pan.clamp(-1.0, 1.0);
|
||||
self.update_gains();
|
||||
}
|
||||
|
||||
/// Get current pan position
|
||||
pub fn pan(&self) -> f32 {
|
||||
self.pan
|
||||
}
|
||||
|
||||
/// Update left/right gains using constant-power panning law
|
||||
fn update_gains(&mut self) {
|
||||
use std::f32::consts::PI;
|
||||
|
||||
// Constant-power panning: pan from -1 to +1 maps to angle 0 to PI/2
|
||||
let angle = (self.pan + 1.0) * 0.5 * PI / 2.0;
|
||||
|
||||
self.left_gain = angle.cos();
|
||||
self.right_gain = angle.sin();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PanEffect {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Effect for PanEffect {
|
||||
fn process(&mut self, buffer: &mut [f32], channels: usize, _sample_rate: u32) {
|
||||
if channels == 2 {
|
||||
// Stereo processing
|
||||
for frame in buffer.chunks_exact_mut(2) {
|
||||
frame[0] *= self.left_gain;
|
||||
frame[1] *= self.right_gain;
|
||||
}
|
||||
}
|
||||
// Mono and other channel counts: no panning applied
|
||||
}
|
||||
|
||||
fn set_parameter(&mut self, id: u32, value: f32) {
|
||||
if id == 0 {
|
||||
self.set_pan(value);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_parameter(&self, id: u32) -> f32 {
|
||||
if id == 0 {
|
||||
self.pan
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
// Pan has no state to reset
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"Pan"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
use super::Effect;
|
||||
use crate::audio::midi::MidiEvent;
|
||||
use std::f32::consts::PI;
|
||||
|
||||
/// Maximum number of simultaneous voices
|
||||
const MAX_VOICES: usize = 16;
|
||||
|
||||
/// A single synthesizer voice
|
||||
#[derive(Clone)]
|
||||
struct SynthVoice {
|
||||
active: bool,
|
||||
note: u8,
|
||||
channel: u8,
|
||||
velocity: u8,
|
||||
phase: f32,
|
||||
frequency: f32,
|
||||
age: u32, // For voice stealing
|
||||
}
|
||||
|
||||
impl SynthVoice {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
active: false,
|
||||
note: 0,
|
||||
channel: 0,
|
||||
velocity: 0,
|
||||
phase: 0.0,
|
||||
frequency: 0.0,
|
||||
age: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate frequency from MIDI note number
|
||||
fn note_to_frequency(note: u8) -> f32 {
|
||||
440.0 * 2.0_f32.powf((note as f32 - 69.0) / 12.0)
|
||||
}
|
||||
|
||||
/// Start playing a note
|
||||
fn note_on(&mut self, channel: u8, note: u8, velocity: u8) {
|
||||
self.active = true;
|
||||
self.channel = channel;
|
||||
self.note = note;
|
||||
self.velocity = velocity;
|
||||
self.frequency = Self::note_to_frequency(note);
|
||||
self.phase = 0.0;
|
||||
self.age = 0;
|
||||
}
|
||||
|
||||
/// Stop playing
|
||||
fn note_off(&mut self) {
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
/// Generate one sample
|
||||
fn process_sample(&mut self, sample_rate: f32) -> f32 {
|
||||
if !self.active {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Simple sine wave
|
||||
let sample = (self.phase * 2.0 * PI).sin() * (self.velocity as f32 / 127.0) * 0.3;
|
||||
|
||||
// Update phase
|
||||
self.phase += self.frequency / sample_rate;
|
||||
if self.phase >= 1.0 {
|
||||
self.phase -= 1.0;
|
||||
}
|
||||
|
||||
self.age += 1;
|
||||
sample
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple polyphonic synthesizer using sine waves
|
||||
pub struct SimpleSynth {
|
||||
voices: Vec<SynthVoice>,
|
||||
sample_rate: f32,
|
||||
pending_events: Vec<MidiEvent>,
|
||||
}
|
||||
|
||||
impl SimpleSynth {
|
||||
/// Create a new SimpleSynth
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
voices: vec![SynthVoice::new(); MAX_VOICES],
|
||||
sample_rate: 44100.0,
|
||||
pending_events: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Find a free voice, or steal the oldest one
|
||||
fn find_voice_for_note_on(&mut self) -> usize {
|
||||
// First, look for an inactive voice
|
||||
for (i, voice) in self.voices.iter().enumerate() {
|
||||
if !voice.active {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// No free voices, steal the oldest one
|
||||
self.voices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by_key(|(_, v)| v.age)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Find the voice playing a specific note on a specific channel
|
||||
fn find_voice_for_note_off(&mut self, channel: u8, note: u8) -> Option<usize> {
|
||||
self.voices
|
||||
.iter()
|
||||
.position(|v| v.active && v.channel == channel && v.note == note)
|
||||
}
|
||||
|
||||
/// Handle a MIDI event
|
||||
pub fn handle_event(&mut self, event: &MidiEvent) {
|
||||
if event.is_note_on() {
|
||||
let voice_idx = self.find_voice_for_note_on();
|
||||
self.voices[voice_idx].note_on(event.channel(), event.data1, event.data2);
|
||||
} else if event.is_note_off() {
|
||||
if let Some(voice_idx) = self.find_voice_for_note_off(event.channel(), event.data1) {
|
||||
self.voices[voice_idx].note_off();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Queue a MIDI event to be processed
|
||||
pub fn queue_event(&mut self, event: MidiEvent) {
|
||||
self.pending_events.push(event);
|
||||
}
|
||||
|
||||
/// Process all queued events
|
||||
fn process_events(&mut self) {
|
||||
// Collect events first to avoid borrowing issues
|
||||
let events: Vec<MidiEvent> = self.pending_events.drain(..).collect();
|
||||
for event in events {
|
||||
self.handle_event(&event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Effect for SimpleSynth {
|
||||
fn process(&mut self, buffer: &mut [f32], channels: usize, sample_rate: u32) {
|
||||
self.sample_rate = sample_rate as f32;
|
||||
|
||||
// Process any queued MIDI events
|
||||
self.process_events();
|
||||
|
||||
// Generate audio from all active voices
|
||||
if channels == 1 {
|
||||
// Mono
|
||||
for sample in buffer.iter_mut() {
|
||||
let mut sum = 0.0;
|
||||
for voice in &mut self.voices {
|
||||
sum += voice.process_sample(self.sample_rate);
|
||||
}
|
||||
*sample += sum;
|
||||
}
|
||||
} else if channels == 2 {
|
||||
// Stereo (duplicate mono signal)
|
||||
for frame in buffer.chunks_exact_mut(2) {
|
||||
let mut sum = 0.0;
|
||||
for voice in &mut self.voices {
|
||||
sum += voice.process_sample(self.sample_rate);
|
||||
}
|
||||
frame[0] += sum;
|
||||
frame[1] += sum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_parameter(&mut self, id: u32, value: f32) {
|
||||
// Parameter 0: Note on
|
||||
// Parameter 1: Note off
|
||||
// This is a simple interface for testing without proper MIDI routing
|
||||
match id {
|
||||
0 => {
|
||||
let note = value as u8;
|
||||
let voice_idx = self.find_voice_for_note_on();
|
||||
self.voices[voice_idx].note_on(0, note, 100);
|
||||
}
|
||||
1 => {
|
||||
let note = value as u8;
|
||||
if let Some(voice_idx) = self.find_voice_for_note_off(0, note) {
|
||||
self.voices[voice_idx].note_off();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_parameter(&self, _id: u32) -> f32 {
|
||||
0.0
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
for voice in &mut self.voices {
|
||||
voice.note_off();
|
||||
}
|
||||
self.pending_events.clear();
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"SimpleSynth"
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SimpleSynth {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
use crate::audio::midi::{MidiClip, MidiClipId, MidiEvent};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
/// Load a MIDI file and convert it to a MidiClip
|
||||
pub fn load_midi_file<P: AsRef<Path>>(
|
||||
path: P,
|
||||
clip_id: MidiClipId,
|
||||
sample_rate: u32,
|
||||
) -> Result<MidiClip, String> {
|
||||
// Read the MIDI file
|
||||
let data = fs::read(path.as_ref()).map_err(|e| format!("Failed to read MIDI file: {}", e))?;
|
||||
|
||||
// Parse with midly
|
||||
let smf = midly::Smf::parse(&data).map_err(|e| format!("Failed to parse MIDI file: {}", e))?;
|
||||
|
||||
// Convert timing to ticks per second
|
||||
let ticks_per_beat = match smf.header.timing {
|
||||
midly::Timing::Metrical(tpb) => tpb.as_int() as f64,
|
||||
midly::Timing::Timecode(fps, subframe) => {
|
||||
// For timecode, calculate equivalent ticks per second
|
||||
(fps.as_f32() * subframe as f32) as f64
|
||||
}
|
||||
};
|
||||
|
||||
// First pass: collect all events with their tick positions and tempo changes
|
||||
#[derive(Debug)]
|
||||
enum RawEvent {
|
||||
Midi {
|
||||
tick: u64,
|
||||
channel: u8,
|
||||
message: midly::MidiMessage,
|
||||
},
|
||||
Tempo {
|
||||
tick: u64,
|
||||
microseconds_per_beat: f64,
|
||||
},
|
||||
}
|
||||
|
||||
let mut raw_events = Vec::new();
|
||||
let mut max_time_ticks = 0u64;
|
||||
|
||||
// Collect all events from all tracks with their absolute tick positions
|
||||
for track in &smf.tracks {
|
||||
let mut current_tick = 0u64;
|
||||
|
||||
for event in track {
|
||||
current_tick += event.delta.as_int() as u64;
|
||||
max_time_ticks = max_time_ticks.max(current_tick);
|
||||
|
||||
match event.kind {
|
||||
midly::TrackEventKind::Midi { channel, message } => {
|
||||
raw_events.push(RawEvent::Midi {
|
||||
tick: current_tick,
|
||||
channel: channel.as_int(),
|
||||
message,
|
||||
});
|
||||
}
|
||||
midly::TrackEventKind::Meta(midly::MetaMessage::Tempo(tempo)) => {
|
||||
raw_events.push(RawEvent::Tempo {
|
||||
tick: current_tick,
|
||||
microseconds_per_beat: tempo.as_int() as f64,
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
// Ignore other meta events
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort all events by tick position
|
||||
raw_events.sort_by_key(|e| match e {
|
||||
RawEvent::Midi { tick, .. } => *tick,
|
||||
RawEvent::Tempo { tick, .. } => *tick,
|
||||
});
|
||||
|
||||
// Second pass: convert ticks to timestamps with proper tempo tracking
|
||||
let mut events = Vec::new();
|
||||
let mut microseconds_per_beat = 500000.0; // Default: 120 BPM
|
||||
let mut last_tick = 0u64;
|
||||
let mut accumulated_time = 0.0; // Time in seconds
|
||||
|
||||
for raw_event in raw_events {
|
||||
match raw_event {
|
||||
RawEvent::Tempo {
|
||||
tick,
|
||||
microseconds_per_beat: new_tempo,
|
||||
} => {
|
||||
// Update accumulated time up to this tempo change
|
||||
let delta_ticks = tick - last_tick;
|
||||
let delta_time = (delta_ticks as f64 / ticks_per_beat)
|
||||
* (microseconds_per_beat / 1_000_000.0);
|
||||
accumulated_time += delta_time;
|
||||
last_tick = tick;
|
||||
|
||||
// Update tempo for future events
|
||||
microseconds_per_beat = new_tempo;
|
||||
}
|
||||
RawEvent::Midi {
|
||||
tick,
|
||||
channel,
|
||||
message,
|
||||
} => {
|
||||
// Calculate time for this event
|
||||
let delta_ticks = tick - last_tick;
|
||||
let delta_time = (delta_ticks as f64 / ticks_per_beat)
|
||||
* (microseconds_per_beat / 1_000_000.0);
|
||||
accumulated_time += delta_time;
|
||||
last_tick = tick;
|
||||
|
||||
let timestamp = (accumulated_time * sample_rate as f64) as u64;
|
||||
|
||||
match message {
|
||||
midly::MidiMessage::NoteOn { key, vel } => {
|
||||
let velocity = vel.as_int();
|
||||
if velocity > 0 {
|
||||
events.push(MidiEvent::note_on(
|
||||
timestamp,
|
||||
channel,
|
||||
key.as_int(),
|
||||
velocity,
|
||||
));
|
||||
} else {
|
||||
events.push(MidiEvent::note_off(timestamp, channel, key.as_int(), 64));
|
||||
}
|
||||
}
|
||||
midly::MidiMessage::NoteOff { key, vel } => {
|
||||
events.push(MidiEvent::note_off(
|
||||
timestamp,
|
||||
channel,
|
||||
key.as_int(),
|
||||
vel.as_int(),
|
||||
));
|
||||
}
|
||||
midly::MidiMessage::Controller { controller, value } => {
|
||||
let status = 0xB0 | channel;
|
||||
events.push(MidiEvent::new(
|
||||
timestamp,
|
||||
status,
|
||||
controller.as_int(),
|
||||
value.as_int(),
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
// Ignore other MIDI messages
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate final clip duration
|
||||
let final_delta_ticks = max_time_ticks - last_tick;
|
||||
let final_delta_time =
|
||||
(final_delta_ticks as f64 / ticks_per_beat) * (microseconds_per_beat / 1_000_000.0);
|
||||
let duration_seconds = accumulated_time + final_delta_time;
|
||||
|
||||
// Create the MIDI clip
|
||||
let mut clip = MidiClip::new(clip_id, 0.0, duration_seconds);
|
||||
clip.events = events;
|
||||
|
||||
Ok(clip)
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
pub mod audio_file;
|
||||
pub mod midi_file;
|
||||
|
||||
pub use audio_file::AudioFile;
|
||||
pub use midi_file::load_midi_file;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
// DAW Backend - Phase 4: Clips & Timeline
|
||||
// DAW Backend - Phase 6: Hierarchical Tracks
|
||||
//
|
||||
// A DAW backend with timeline-based playback, clips, and audio pool.
|
||||
// Supports multiple tracks, mixing, per-track volume/mute/solo, and shared audio data.
|
||||
// A DAW backend with timeline-based playback, clips, audio pool, effects, and hierarchical track groups.
|
||||
// Supports multiple tracks, mixing, per-track volume/mute/solo, shared audio data, effect chains, and nested groups.
|
||||
// Uses lock-free command queues, cpal for audio I/O, and symphonia for audio file decoding.
|
||||
|
||||
pub mod audio;
|
||||
pub mod command;
|
||||
pub mod dsp;
|
||||
pub mod effects;
|
||||
pub mod io;
|
||||
|
||||
// Re-export commonly used types
|
||||
pub use audio::{AudioPool, Clip, ClipId, Engine, EngineController, PoolAudioFile, Track, TrackId};
|
||||
pub use audio::{
|
||||
AudioPool, AudioTrack, BufferPool, Clip, ClipId, Engine, EngineController,
|
||||
GroupTrack, MidiClip, MidiClipId, MidiEvent, MidiTrack, PoolAudioFile, Project, Track, TrackId, TrackNode,
|
||||
};
|
||||
pub use command::{AudioEvent, Command};
|
||||
pub use io::AudioFile;
|
||||
pub use effects::{Effect, GainEffect, PanEffect, SimpleEQ, SimpleSynth};
|
||||
pub use io::{load_midi_file, AudioFile};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use daw_backend::{AudioEvent, AudioFile, Clip, Engine, PoolAudioFile, Track};
|
||||
use daw_backend::{load_midi_file, AudioEvent, AudioFile, Clip, Engine, PoolAudioFile, Track, TrackNode};
|
||||
use std::env;
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
@ -17,7 +17,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
println!("DAW Backend - Phase 4: Clips & Timeline\n");
|
||||
println!("DAW Backend - Phase 6: Hierarchical Tracks\n");
|
||||
|
||||
// Load all audio files
|
||||
let mut audio_files = Vec::new();
|
||||
|
|
@ -92,7 +92,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
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 track_ids = Arc::new(Mutex::new(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;
|
||||
|
|
@ -128,7 +128,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
|
||||
track.add_clip(clip);
|
||||
engine.add_track(track);
|
||||
track_ids.push(track_id);
|
||||
track_ids.lock().unwrap().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);
|
||||
|
|
@ -140,6 +140,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
|
||||
// Wrap engine in Arc<Mutex> for thread-safe access
|
||||
let engine = Arc::new(Mutex::new(engine));
|
||||
let engine_for_commands = Arc::clone(&engine);
|
||||
|
||||
// Build the output stream
|
||||
let stream = match sample_format {
|
||||
|
|
@ -153,11 +154,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
stream.play()?;
|
||||
println!("\nAudio stream started!");
|
||||
print_help();
|
||||
print_status(0.0, max_duration, &track_ids);
|
||||
{
|
||||
let ids = track_ids.lock().unwrap();
|
||||
print_status(0.0, max_duration, &ids);
|
||||
}
|
||||
|
||||
// Spawn event listener thread
|
||||
let event_rx = Arc::new(Mutex::new(event_rx));
|
||||
let event_rx_clone = Arc::clone(&event_rx);
|
||||
let track_ids_clone = Arc::clone(&track_ids);
|
||||
let _event_thread = thread::spawn(move || {
|
||||
loop {
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
|
|
@ -192,6 +197,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
AudioEvent::BufferUnderrun => {
|
||||
eprintln!("\nWarning: Buffer underrun detected");
|
||||
}
|
||||
AudioEvent::TrackCreated(track_id, is_group, name) => {
|
||||
print!("\r\x1b[K");
|
||||
if is_group {
|
||||
println!("Group {} created: '{}' (ID: {})", track_id, name, track_id);
|
||||
} else {
|
||||
println!("Track {} created: '{}' (ID: {})", track_id, name, track_id);
|
||||
}
|
||||
track_ids_clone.lock().unwrap().push(track_id);
|
||||
print!("> ");
|
||||
io::stdout().flush().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -238,11 +254,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
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) {
|
||||
let ids = track_ids.lock().unwrap();
|
||||
if ids.contains(&track_id) {
|
||||
drop(ids);
|
||||
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);
|
||||
println!("Invalid track ID. Available tracks: {:?}", *ids);
|
||||
}
|
||||
} else {
|
||||
println!("Invalid format. Usage: volume <track_id> <volume>");
|
||||
|
|
@ -253,11 +271,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
} 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) {
|
||||
let ids = track_ids.lock().unwrap();
|
||||
if ids.contains(&track_id) {
|
||||
drop(ids);
|
||||
controller.set_track_mute(track_id, true);
|
||||
println!("Muted track {}", track_id);
|
||||
} else {
|
||||
println!("Invalid track ID. Available tracks: {:?}", track_ids);
|
||||
println!("Invalid track ID. Available tracks: {:?}", *ids);
|
||||
}
|
||||
} else {
|
||||
println!("Usage: mute <track_id>");
|
||||
|
|
@ -265,11 +285,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
} 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) {
|
||||
let ids = track_ids.lock().unwrap();
|
||||
if ids.contains(&track_id) {
|
||||
drop(ids);
|
||||
controller.set_track_mute(track_id, false);
|
||||
println!("Unmuted track {}", track_id);
|
||||
} else {
|
||||
println!("Invalid track ID. Available tracks: {:?}", track_ids);
|
||||
println!("Invalid track ID. Available tracks: {:?}", *ids);
|
||||
}
|
||||
} else {
|
||||
println!("Usage: unmute <track_id>");
|
||||
|
|
@ -277,11 +299,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
} 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) {
|
||||
let ids = track_ids.lock().unwrap();
|
||||
if ids.contains(&track_id) {
|
||||
drop(ids);
|
||||
controller.set_track_solo(track_id, true);
|
||||
println!("Soloed track {}", track_id);
|
||||
} else {
|
||||
println!("Invalid track ID. Available tracks: {:?}", track_ids);
|
||||
println!("Invalid track ID. Available tracks: {:?}", *ids);
|
||||
}
|
||||
} else {
|
||||
println!("Usage: solo <track_id>");
|
||||
|
|
@ -289,11 +313,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
} 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) {
|
||||
let ids = track_ids.lock().unwrap();
|
||||
if ids.contains(&track_id) {
|
||||
drop(ids);
|
||||
controller.set_track_solo(track_id, false);
|
||||
println!("Unsoloed track {}", track_id);
|
||||
} else {
|
||||
println!("Invalid track ID. Available tracks: {:?}", track_ids);
|
||||
println!("Invalid track ID. Available tracks: {:?}", *ids);
|
||||
}
|
||||
} else {
|
||||
println!("Usage: unsolo <track_id>");
|
||||
|
|
@ -322,11 +348,249 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
println!("Usage: move <track_id> <clip_id> <time>");
|
||||
}
|
||||
} else if input == "tracks" {
|
||||
println!("Available tracks: {:?}", track_ids);
|
||||
let ids = track_ids.lock().unwrap();
|
||||
println!("Available tracks: {:?}", *ids);
|
||||
} else if input == "clips" {
|
||||
// Query the actual project state for all clips
|
||||
let engine = engine_for_commands.lock().unwrap();
|
||||
let project = engine.project();
|
||||
let track_ids_list = track_ids.lock().unwrap().clone();
|
||||
|
||||
println!("Available clips:");
|
||||
for (tid, cid, name, dur) in &clip_info {
|
||||
println!(" Track {}, Clip {} ('{}', duration {:.2}s)", tid, cid, name, dur);
|
||||
let mut clip_count = 0;
|
||||
|
||||
for &track_id in &track_ids_list {
|
||||
if let Some(track_node) = project.get_track(track_id) {
|
||||
match track_node {
|
||||
TrackNode::Audio(track) => {
|
||||
for clip in &track.clips {
|
||||
println!(" Track {} ({}), Audio Clip {}: start {:.2}s, duration {:.2}s",
|
||||
track_id, track.name, clip.id, clip.start_time,
|
||||
clip.end_time() - clip.start_time);
|
||||
clip_count += 1;
|
||||
}
|
||||
}
|
||||
TrackNode::Midi(track) => {
|
||||
for clip in &track.clips {
|
||||
let event_count = clip.events.len();
|
||||
println!(" Track {} ({}), MIDI Clip {}: start {:.2}s, duration {:.2}s, {} events",
|
||||
track_id, track.name, clip.id, clip.start_time,
|
||||
clip.duration, event_count);
|
||||
clip_count += 1;
|
||||
}
|
||||
}
|
||||
TrackNode::Group(_) => {
|
||||
// Groups don't have clips
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if clip_count == 0 {
|
||||
println!(" (no clips)");
|
||||
}
|
||||
} else if input.starts_with("gain ") {
|
||||
// Parse: gain <track_id> <gain_db>
|
||||
let parts: Vec<&str> = input.split_whitespace().collect();
|
||||
if parts.len() == 3 {
|
||||
if let (Ok(track_id), Ok(gain_db)) = (parts[1].parse::<u32>(), parts[2].parse::<f32>()) {
|
||||
let ids = track_ids.lock().unwrap();
|
||||
if ids.contains(&track_id) {
|
||||
drop(ids);
|
||||
controller.add_gain_effect(track_id, gain_db);
|
||||
println!("Set gain on track {} to {:.1} dB", track_id, gain_db);
|
||||
} else {
|
||||
println!("Invalid track ID. Available tracks: {:?}", *ids);
|
||||
}
|
||||
} else {
|
||||
println!("Invalid format. Usage: gain <track_id> <gain_db>");
|
||||
}
|
||||
} else {
|
||||
println!("Usage: gain <track_id> <gain_db>");
|
||||
}
|
||||
} else if input.starts_with("pan ") {
|
||||
// Parse: pan <track_id> <pan>
|
||||
let parts: Vec<&str> = input.split_whitespace().collect();
|
||||
if parts.len() == 3 {
|
||||
if let (Ok(track_id), Ok(pan)) = (parts[1].parse::<u32>(), parts[2].parse::<f32>()) {
|
||||
let ids = track_ids.lock().unwrap();
|
||||
if ids.contains(&track_id) {
|
||||
drop(ids);
|
||||
let clamped_pan = pan.clamp(-1.0, 1.0);
|
||||
controller.add_pan_effect(track_id, clamped_pan);
|
||||
let pos = if clamped_pan < -0.01 {
|
||||
format!("{:.0}% left", -clamped_pan * 100.0)
|
||||
} else if clamped_pan > 0.01 {
|
||||
format!("{:.0}% right", clamped_pan * 100.0)
|
||||
} else {
|
||||
"center".to_string()
|
||||
};
|
||||
println!("Set pan on track {} to {} ({:.2})", track_id, pos, clamped_pan);
|
||||
} else {
|
||||
println!("Invalid track ID. Available tracks: {:?}", *ids);
|
||||
}
|
||||
} else {
|
||||
println!("Invalid format. Usage: pan <track_id> <pan>");
|
||||
}
|
||||
} else {
|
||||
println!("Usage: pan <track_id> <pan> (where pan is -1.0=left, 0.0=center, 1.0=right)");
|
||||
}
|
||||
} else if input.starts_with("eq ") {
|
||||
// Parse: eq <track_id> <low_db> <mid_db> <high_db>
|
||||
let parts: Vec<&str> = input.split_whitespace().collect();
|
||||
if parts.len() == 5 {
|
||||
if let (Ok(track_id), Ok(low), Ok(mid), Ok(high)) =
|
||||
(parts[1].parse::<u32>(), parts[2].parse::<f32>(), parts[3].parse::<f32>(), parts[4].parse::<f32>()) {
|
||||
let ids = track_ids.lock().unwrap();
|
||||
if ids.contains(&track_id) {
|
||||
drop(ids);
|
||||
controller.add_eq_effect(track_id, low, mid, high);
|
||||
println!("Set EQ on track {}: Low {:.1} dB, Mid {:.1} dB, High {:.1} dB",
|
||||
track_id, low, mid, high);
|
||||
} else {
|
||||
println!("Invalid track ID. Available tracks: {:?}", *ids);
|
||||
}
|
||||
} else {
|
||||
println!("Invalid format. Usage: eq <track_id> <low_db> <mid_db> <high_db>");
|
||||
}
|
||||
} else {
|
||||
println!("Usage: eq <track_id> <low_db> <mid_db> <high_db>");
|
||||
}
|
||||
} else if input.starts_with("clearfx ") {
|
||||
// Parse: clearfx <track_id>
|
||||
if let Ok(track_id) = input[8..].trim().parse::<u32>() {
|
||||
let ids = track_ids.lock().unwrap();
|
||||
if ids.contains(&track_id) {
|
||||
drop(ids);
|
||||
controller.clear_effects(track_id);
|
||||
println!("Cleared all effects from track {}", track_id);
|
||||
} else {
|
||||
println!("Invalid track ID. Available tracks: {:?}", *ids);
|
||||
}
|
||||
} else {
|
||||
println!("Usage: clearfx <track_id>");
|
||||
}
|
||||
} else if input.starts_with("group ") {
|
||||
// Parse: group <name>
|
||||
let name = input[6..].trim().to_string();
|
||||
if !name.is_empty() {
|
||||
controller.create_group(name.clone());
|
||||
println!("Created group '{}'", name);
|
||||
} else {
|
||||
println!("Usage: group <name>");
|
||||
}
|
||||
} else if input.starts_with("addtogroup ") {
|
||||
// Parse: addtogroup <track_id> <group_id>
|
||||
let parts: Vec<&str> = input.split_whitespace().collect();
|
||||
if parts.len() == 3 {
|
||||
if let (Ok(track_id), Ok(group_id)) = (parts[1].parse::<u32>(), parts[2].parse::<u32>()) {
|
||||
controller.add_to_group(track_id, group_id);
|
||||
println!("Added track {} to group {}", track_id, group_id);
|
||||
} else {
|
||||
println!("Invalid format. Usage: addtogroup <track_id> <group_id>");
|
||||
}
|
||||
} else {
|
||||
println!("Usage: addtogroup <track_id> <group_id>");
|
||||
}
|
||||
} else if input.starts_with("removefromgroup ") {
|
||||
// Parse: removefromgroup <track_id>
|
||||
if let Ok(track_id) = input[16..].trim().parse::<u32>() {
|
||||
controller.remove_from_group(track_id);
|
||||
println!("Removed track {} from its group", track_id);
|
||||
} else {
|
||||
println!("Usage: removefromgroup <track_id>");
|
||||
}
|
||||
} else if input.starts_with("midi ") {
|
||||
// Parse: midi <name>
|
||||
let name = input[5..].trim().to_string();
|
||||
if !name.is_empty() {
|
||||
controller.create_midi_track(name.clone());
|
||||
println!("Created MIDI track '{}'", name);
|
||||
} else {
|
||||
println!("Usage: midi <name>");
|
||||
}
|
||||
} else if input.starts_with("midiclip ") {
|
||||
// Parse: midiclip <track_id> <start_time> <duration>
|
||||
let parts: Vec<&str> = input.split_whitespace().collect();
|
||||
if parts.len() == 4 {
|
||||
if let (Ok(track_id), Ok(start_time), Ok(duration)) =
|
||||
(parts[1].parse::<u32>(), parts[2].parse::<f64>(), parts[3].parse::<f64>()) {
|
||||
let ids = track_ids.lock().unwrap();
|
||||
if ids.contains(&track_id) {
|
||||
drop(ids);
|
||||
controller.create_midi_clip(track_id, start_time, duration);
|
||||
println!("Created MIDI clip on track {} at {:.2}s (duration {:.2}s)",
|
||||
track_id, start_time, duration);
|
||||
} else {
|
||||
println!("Invalid track ID. Available tracks: {:?}", *ids);
|
||||
}
|
||||
} else {
|
||||
println!("Invalid format. Usage: midiclip <track_id> <start_time> <duration>");
|
||||
}
|
||||
} else {
|
||||
println!("Usage: midiclip <track_id> <start_time> <duration>");
|
||||
}
|
||||
} else if input.starts_with("note ") {
|
||||
// Parse: note <track_id> <clip_id> <time_offset> <note> <velocity> <duration>
|
||||
let parts: Vec<&str> = input.split_whitespace().collect();
|
||||
if parts.len() == 7 {
|
||||
if let (Ok(track_id), Ok(clip_id), Ok(time_offset), Ok(note), Ok(velocity), Ok(duration)) =
|
||||
(parts[1].parse::<u32>(), parts[2].parse::<u32>(), parts[3].parse::<f64>(),
|
||||
parts[4].parse::<u8>(), parts[5].parse::<u8>(), parts[6].parse::<f64>()) {
|
||||
if note > 127 || velocity > 127 {
|
||||
println!("Note and velocity must be 0-127");
|
||||
} else {
|
||||
controller.add_midi_note(track_id, clip_id, time_offset, note, velocity, duration);
|
||||
println!("Added note {} (velocity {}) to clip {} on track {} at offset {:.2}s (duration {:.2}s)",
|
||||
note, velocity, clip_id, track_id, time_offset, duration);
|
||||
}
|
||||
} else {
|
||||
println!("Invalid format. Usage: note <track_id> <clip_id> <time_offset> <note> <velocity> <duration>");
|
||||
}
|
||||
} else {
|
||||
println!("Usage: note <track_id> <clip_id> <time_offset> <note> <velocity> <duration>");
|
||||
}
|
||||
} else if input.starts_with("loadmidi ") {
|
||||
// Parse: loadmidi <track_id> <file_path> [start_time]
|
||||
let parts: Vec<&str> = input.splitn(4, ' ').collect();
|
||||
if parts.len() >= 3 {
|
||||
if let Ok(track_id) = parts[1].parse::<u32>() {
|
||||
let file_path = parts[2];
|
||||
let start_time = if parts.len() == 4 {
|
||||
parts[3].parse::<f64>().unwrap_or(0.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let ids = track_ids.lock().unwrap();
|
||||
if ids.contains(&track_id) {
|
||||
drop(ids);
|
||||
|
||||
// Load the MIDI file (this happens on the UI thread, not audio thread)
|
||||
match load_midi_file(file_path, clip_id_counter, max_sample_rate) {
|
||||
Ok(mut clip) => {
|
||||
clip.start_time = start_time;
|
||||
let event_count = clip.events.len();
|
||||
let duration = clip.duration;
|
||||
let clip_id = clip.id;
|
||||
clip_id_counter += 1;
|
||||
|
||||
controller.add_loaded_midi_clip(track_id, clip);
|
||||
println!("Loaded MIDI file '{}' to track {} as clip {} at {:.2}s ({} events, duration {:.2}s)",
|
||||
file_path, track_id, clip_id, start_time, event_count, duration);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error loading MIDI file: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Invalid track ID. Available tracks: {:?}", *ids);
|
||||
}
|
||||
} else {
|
||||
println!("Invalid format. Usage: loadmidi <track_id> <file_path> [start_time]");
|
||||
}
|
||||
} else {
|
||||
println!("Usage: loadmidi <track_id> <file_path> [start_time]");
|
||||
}
|
||||
} else if input == "help" || input == "h" {
|
||||
print_help();
|
||||
|
|
@ -360,6 +624,25 @@ fn print_help() {
|
|||
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!("\nEffect Commands:");
|
||||
println!(" gain <id> <db> - Add/update gain effect (e.g. 'gain 0 6.0' for +6dB)");
|
||||
println!(" pan <id> <pan> - Add/update pan effect (-1.0=left, 0.0=center, 1.0=right)");
|
||||
println!(" eq <id> <l> <m> <h> - Add/update 3-band EQ (low, mid, high in dB)");
|
||||
println!(" (e.g. 'eq 0 3.0 0.0 -2.0')");
|
||||
println!(" clearfx <id> - Clear all effects from a track");
|
||||
println!("\nGroup Commands:");
|
||||
println!(" group <name> - Create a new group track");
|
||||
println!(" addtogroup <t> <g> - Add track to group (e.g. 'addtogroup 0 2')");
|
||||
println!(" removefromgroup <t> - Remove track from its parent group");
|
||||
println!("\nMIDI Commands:");
|
||||
println!(" midi <name> - Create a new MIDI track");
|
||||
println!(" midiclip <t> <s> <d> - Create MIDI clip on track (start, duration)");
|
||||
println!(" (e.g. 'midiclip 0 0.0 4.0')");
|
||||
println!(" note <t> <c> <o> <n> <v> <d> - Add note to MIDI clip");
|
||||
println!(" (track, clip, time_offset, note, velocity, duration)");
|
||||
println!(" (e.g. 'note 0 0 0.0 60 100 0.5' adds middle C)");
|
||||
println!(" loadmidi <t> <file> [start] - Load .mid file into track");
|
||||
println!(" (e.g. 'loadmidi 0 song.mid 0.0')");
|
||||
println!("\nOther:");
|
||||
println!(" h, help - Show this help");
|
||||
println!(" q, quit - Quit");
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
{"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":{}}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
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.
|
|
@ -1 +0,0 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -1 +0,0 @@
|
|||
efc2abc372d112a8
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"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}
|
||||
|
|
@ -1 +0,0 @@
|
|||
01b118c574f54313
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"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}
|
||||
|
|
@ -1 +0,0 @@
|
|||
f25297bf94dbb645
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"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.
|
|
@ -1 +0,0 @@
|
|||
This file has an mtime of when this was started.
|
||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -1 +0,0 @@
|
|||
a1ac8cc77d64c2ad
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"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.
|
|
@ -1 +0,0 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -1 +0,0 @@
|
|||
afc4bfc0995f2524
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"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.
|
|
@ -1 +0,0 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -1 +0,0 @@
|
|||
edfa6133c992fd0c
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"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.
|
|
@ -1 +0,0 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -1 +0,0 @@
|
|||
b92268bca71f240d
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"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.
|
|
@ -1 +0,0 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -1 +0,0 @@
|
|||
c964e5b071fc9ec1
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"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.
|
|
@ -1 +0,0 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -1 +0,0 @@
|
|||
cd32eda4bd5008be
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"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.
|
|
@ -1 +0,0 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -1 +0,0 @@
|
|||
90068b4b799b4460
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"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}
|
||||
|
|
@ -1 +0,0 @@
|
|||
cf7e864a41b58c80
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"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.
|
|
@ -1 +0,0 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -1 +0,0 @@
|
|||
6a97691492ff657b
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"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.
|
|
@ -1 +0,0 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -1 +0,0 @@
|
|||
bb490f6f4243bec5
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"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}
|
||||
|
|
@ -1 +0,0 @@
|
|||
24747b6cdebc3336
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"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.
|
|
@ -1 +0,0 @@
|
|||
This file has an mtime of when this was started.
|
||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -1 +0,0 @@
|
|||
87b66ae7311a7954
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"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}
|
||||
|
|
@ -1 +0,0 @@
|
|||
e6c890495605d37d
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"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.
|
|
@ -1 +0,0 @@
|
|||
This file has an mtime of when this was started.
|
||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -1 +0,0 @@
|
|||
1a47bca53b19e1c0
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"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.
|
|
@ -1 +0,0 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -1 +0,0 @@
|
|||
f5a59024aee69529
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"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.
|
|
@ -1 +0,0 @@
|
|||
This file has an mtime of when this was started.
|
||||
|
|
@ -1 +0,0 @@
|
|||
28219a49b8789695
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"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.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue