282 lines
9.2 KiB
Rust
282 lines
9.2 KiB
Rust
/// Audio recording system for capturing microphone input
|
|
use crate::audio::{ClipId, MidiClipId, TrackId};
|
|
use crate::io::{WavWriter, WaveformPeak};
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
|
|
/// State of an active recording session
|
|
pub struct RecordingState {
|
|
/// Track being recorded to
|
|
pub track_id: TrackId,
|
|
/// Clip ID for the intermediate clip
|
|
pub clip_id: ClipId,
|
|
/// Path to temporary WAV file
|
|
pub temp_file_path: PathBuf,
|
|
/// WAV file writer (only used at finalization, not during recording)
|
|
pub writer: WavWriter,
|
|
/// Sample rate of recording
|
|
pub sample_rate: u32,
|
|
/// Number of channels
|
|
pub channels: u32,
|
|
/// Timeline start position in seconds
|
|
pub start_time: f64,
|
|
/// Total frames recorded
|
|
pub frames_written: usize,
|
|
/// Whether recording is currently paused
|
|
pub paused: bool,
|
|
/// Number of samples remaining to skip (to discard stale buffer data)
|
|
pub samples_to_skip: usize,
|
|
/// Waveform peaks generated incrementally during recording
|
|
pub waveform: Vec<WaveformPeak>,
|
|
/// Temporary buffer for collecting samples for next waveform peak
|
|
pub waveform_buffer: Vec<f32>,
|
|
/// Number of frames per waveform peak
|
|
pub frames_per_peak: usize,
|
|
/// All recorded audio data accumulated in memory (written to disk at finalization)
|
|
pub audio_data: Vec<f32>,
|
|
}
|
|
|
|
impl RecordingState {
|
|
/// Create a new recording state
|
|
pub fn new(
|
|
track_id: TrackId,
|
|
clip_id: ClipId,
|
|
temp_file_path: PathBuf,
|
|
writer: WavWriter,
|
|
sample_rate: u32,
|
|
channels: u32,
|
|
start_time: f64,
|
|
_flush_interval_seconds: f64, // No longer used - kept for API compatibility
|
|
) -> Self {
|
|
// Calculate frames per waveform peak
|
|
// Target ~300 peaks per second with minimum 1000 samples per peak
|
|
let target_peaks_per_second = 300;
|
|
let frames_per_peak = (sample_rate / target_peaks_per_second).max(1000) as usize;
|
|
|
|
Self {
|
|
track_id,
|
|
clip_id,
|
|
temp_file_path,
|
|
writer,
|
|
sample_rate,
|
|
channels,
|
|
start_time,
|
|
frames_written: 0,
|
|
paused: false,
|
|
samples_to_skip: 0, // Will be set by engine when it knows buffer size
|
|
waveform: Vec::new(),
|
|
waveform_buffer: Vec::new(),
|
|
frames_per_peak,
|
|
audio_data: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Add samples to the accumulation buffer
|
|
/// Returns true if a flush occurred
|
|
pub fn add_samples(&mut self, samples: &[f32]) -> Result<bool, std::io::Error> {
|
|
if self.paused {
|
|
return Ok(false);
|
|
}
|
|
|
|
// Determine which samples to process
|
|
let samples_to_process = if self.samples_to_skip > 0 {
|
|
let to_skip = self.samples_to_skip.min(samples.len());
|
|
self.samples_to_skip -= to_skip;
|
|
|
|
if to_skip == samples.len() {
|
|
// Skip entire batch
|
|
return Ok(false);
|
|
}
|
|
|
|
// Skip partial batch and process the rest
|
|
&samples[to_skip..]
|
|
} else {
|
|
samples
|
|
};
|
|
|
|
// Add to audio data (accumulate in memory - disk write happens at finalization only)
|
|
self.audio_data.extend_from_slice(samples_to_process);
|
|
|
|
// Add to waveform buffer and generate peaks incrementally
|
|
self.waveform_buffer.extend_from_slice(samples_to_process);
|
|
self.generate_waveform_peaks();
|
|
|
|
// Track frames for duration calculation (no disk I/O in audio callback!)
|
|
let frames_added = samples_to_process.len() / self.channels as usize;
|
|
self.frames_written += frames_added;
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
/// Generate waveform peaks from accumulated samples
|
|
/// This is called incrementally as samples arrive
|
|
fn generate_waveform_peaks(&mut self) {
|
|
let samples_per_peak = self.frames_per_peak * self.channels as usize;
|
|
|
|
while self.waveform_buffer.len() >= samples_per_peak {
|
|
let mut min = 0.0f32;
|
|
let mut max = 0.0f32;
|
|
|
|
// Scan all samples for this peak
|
|
for sample in &self.waveform_buffer[..samples_per_peak] {
|
|
min = min.min(*sample);
|
|
max = max.max(*sample);
|
|
}
|
|
|
|
self.waveform.push(WaveformPeak { min, max });
|
|
|
|
// Remove processed samples from waveform buffer
|
|
self.waveform_buffer.drain(..samples_per_peak);
|
|
}
|
|
}
|
|
|
|
/// Get current recording duration in seconds
|
|
pub fn duration(&self) -> f64 {
|
|
self.frames_written as f64 / self.sample_rate as f64
|
|
}
|
|
|
|
/// Finalize the recording and return the temp file path, waveform, and audio data
|
|
pub fn finalize(mut self) -> Result<(PathBuf, Vec<WaveformPeak>, Vec<f32>), std::io::Error> {
|
|
// Write all audio data to disk at once (outside audio callback - safe to do I/O)
|
|
if !self.audio_data.is_empty() {
|
|
self.writer.write_samples(&self.audio_data)?;
|
|
}
|
|
|
|
// Generate final waveform peak from any remaining samples
|
|
if !self.waveform_buffer.is_empty() {
|
|
let mut min = 0.0f32;
|
|
let mut max = 0.0f32;
|
|
|
|
for sample in &self.waveform_buffer {
|
|
min = min.min(*sample);
|
|
max = max.max(*sample);
|
|
}
|
|
|
|
self.waveform.push(WaveformPeak { min, max });
|
|
}
|
|
|
|
// Finalize the WAV file
|
|
self.writer.finalize()?;
|
|
|
|
Ok((self.temp_file_path, self.waveform, self.audio_data))
|
|
}
|
|
|
|
/// Pause recording
|
|
pub fn pause(&mut self) {
|
|
self.paused = true;
|
|
}
|
|
|
|
/// Resume recording
|
|
pub fn resume(&mut self) {
|
|
self.paused = false;
|
|
}
|
|
}
|
|
|
|
/// Active MIDI note waiting for its noteOff event
|
|
#[derive(Debug, Clone)]
|
|
struct ActiveMidiNote {
|
|
/// MIDI note number (0-127)
|
|
note: u8,
|
|
/// Velocity (0-127)
|
|
velocity: u8,
|
|
/// Absolute time when note started (seconds)
|
|
start_time: f64,
|
|
}
|
|
|
|
/// State of an active MIDI recording session
|
|
pub struct MidiRecordingState {
|
|
/// Track being recorded to
|
|
pub track_id: TrackId,
|
|
/// MIDI clip ID
|
|
pub clip_id: MidiClipId,
|
|
/// Timeline start position in seconds
|
|
pub start_time: f64,
|
|
/// Currently active notes (noteOn without matching noteOff)
|
|
/// Maps note number to ActiveMidiNote
|
|
active_notes: HashMap<u8, ActiveMidiNote>,
|
|
/// Completed notes ready to be added to clip
|
|
/// Format: (time_offset, note, velocity, duration)
|
|
pub completed_notes: Vec<(f64, u8, u8, f64)>,
|
|
}
|
|
|
|
impl MidiRecordingState {
|
|
/// Create a new MIDI recording state
|
|
pub fn new(track_id: TrackId, clip_id: MidiClipId, start_time: f64) -> Self {
|
|
Self {
|
|
track_id,
|
|
clip_id,
|
|
start_time,
|
|
active_notes: HashMap::new(),
|
|
completed_notes: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Handle a MIDI note on event
|
|
pub fn note_on(&mut self, note: u8, velocity: u8, absolute_time: f64) {
|
|
// Store this note as active
|
|
self.active_notes.insert(note, ActiveMidiNote {
|
|
note,
|
|
velocity,
|
|
start_time: absolute_time,
|
|
});
|
|
}
|
|
|
|
/// Handle a MIDI note off event
|
|
pub fn note_off(&mut self, note: u8, absolute_time: f64) {
|
|
// Find the matching noteOn
|
|
if let Some(active_note) = self.active_notes.remove(¬e) {
|
|
// Calculate relative time offset and duration
|
|
let time_offset = active_note.start_time - self.start_time;
|
|
let duration = absolute_time - active_note.start_time;
|
|
|
|
eprintln!("[MIDI_RECORDING_STATE] Completing note {}: note_start={:.3}s, note_end={:.3}s, recording_start={:.3}s, time_offset={:.3}s, duration={:.3}s",
|
|
note, active_note.start_time, absolute_time, self.start_time, time_offset, duration);
|
|
|
|
// Add to completed notes
|
|
self.completed_notes.push((
|
|
time_offset,
|
|
active_note.note,
|
|
active_note.velocity,
|
|
duration,
|
|
));
|
|
}
|
|
// If no matching noteOn found, ignore the noteOff
|
|
}
|
|
|
|
/// Get all completed notes
|
|
pub fn get_notes(&self) -> &[(f64, u8, u8, f64)] {
|
|
&self.completed_notes
|
|
}
|
|
|
|
/// Get the number of completed notes
|
|
pub fn note_count(&self) -> usize {
|
|
self.completed_notes.len()
|
|
}
|
|
|
|
/// Get the note numbers of all currently held (active) notes
|
|
pub fn active_note_numbers(&self) -> Vec<u8> {
|
|
self.active_notes.keys().copied().collect()
|
|
}
|
|
|
|
/// Close out all active notes at the given time
|
|
/// This should be called when stopping recording to end any held notes
|
|
pub fn close_active_notes(&mut self, end_time: f64) {
|
|
// Collect all active notes and close them
|
|
let active_notes: Vec<_> = self.active_notes.drain().collect();
|
|
|
|
for (_note_num, active_note) in active_notes {
|
|
// Calculate relative time offset and duration
|
|
let time_offset = active_note.start_time - self.start_time;
|
|
let duration = end_time - active_note.start_time;
|
|
|
|
// Add to completed notes
|
|
self.completed_notes.push((
|
|
time_offset,
|
|
active_note.note,
|
|
active_note.velocity,
|
|
duration,
|
|
));
|
|
}
|
|
}
|
|
}
|