Improve idle performance
This commit is contained in:
parent
26f06da5bf
commit
ac2b4ff8ab
|
|
@ -41,6 +41,11 @@ pub struct AudioSystem {
|
||||||
pub event_rx: Option<rtrb::Consumer<AudioEvent>>,
|
pub event_rx: Option<rtrb::Consumer<AudioEvent>>,
|
||||||
/// Consumer for recording audio mirror (streams recorded samples to UI for live waveform)
|
/// Consumer for recording audio mirror (streams recorded samples to UI for live waveform)
|
||||||
recording_mirror_rx: Option<rtrb::Consumer<f32>>,
|
recording_mirror_rx: Option<rtrb::Consumer<f32>>,
|
||||||
|
/// Producer end of the input ring-buffer. Taken into the closure when the
|
||||||
|
/// input stream is opened; `None` after `open_input_stream()` has been called.
|
||||||
|
input_tx: Option<rtrb::Producer<f32>>,
|
||||||
|
/// The live microphone/line-in stream. `None` until `open_input_stream()` is called.
|
||||||
|
input_stream: Option<cpal::Stream>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioSystem {
|
impl AudioSystem {
|
||||||
|
|
@ -138,137 +143,8 @@ impl AudioSystem {
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to build output stream: {e:?}"))?;
|
.map_err(|e| format!("Failed to build output stream: {e:?}"))?;
|
||||||
|
|
||||||
// Get input device
|
// Start output stream
|
||||||
let input_device = match host.default_input_device() {
|
|
||||||
Some(device) => device,
|
|
||||||
None => {
|
|
||||||
eprintln!("Warning: No input device available, recording will be disabled");
|
|
||||||
// Start output stream and return without input
|
|
||||||
output_stream.play().map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
// Spawn emitter thread if provided
|
|
||||||
if let Some(emitter) = event_emitter {
|
|
||||||
Self::spawn_emitter_thread(event_rx, emitter);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(Self {
|
|
||||||
controller,
|
|
||||||
stream: output_stream,
|
|
||||||
sample_rate,
|
|
||||||
channels,
|
|
||||||
event_rx: None, // No event receiver when audio device unavailable
|
|
||||||
recording_mirror_rx: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get input config using the device's default (most compatible)
|
|
||||||
let input_config = match input_device.default_input_config() {
|
|
||||||
Ok(config) => {
|
|
||||||
let cfg: cpal::StreamConfig = config.into();
|
|
||||||
cfg
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Warning: Could not get input config: {}, recording will be disabled", e);
|
|
||||||
output_stream.play().map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
if let Some(emitter) = event_emitter {
|
|
||||||
Self::spawn_emitter_thread(event_rx, emitter);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(Self {
|
|
||||||
controller,
|
|
||||||
stream: output_stream,
|
|
||||||
sample_rate,
|
|
||||||
channels,
|
|
||||||
event_rx: None,
|
|
||||||
recording_mirror_rx: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let input_sample_rate = input_config.sample_rate;
|
|
||||||
let input_channels = input_config.channels as u32;
|
|
||||||
let output_sample_rate = sample_rate;
|
|
||||||
let output_channels = channels;
|
|
||||||
let needs_resample = input_sample_rate != output_sample_rate || input_channels != output_channels;
|
|
||||||
|
|
||||||
if needs_resample {
|
|
||||||
eprintln!("[AUDIO] Input device: {}Hz {}ch -> resampling to {}Hz {}ch",
|
|
||||||
input_sample_rate, input_channels, output_sample_rate, output_channels);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build input stream with resampling if needed
|
|
||||||
let input_stream = match input_device
|
|
||||||
.build_input_stream(
|
|
||||||
&input_config,
|
|
||||||
move |data: &[f32], _: &cpal::InputCallbackInfo| {
|
|
||||||
if !needs_resample {
|
|
||||||
for &sample in data {
|
|
||||||
let _ = input_tx.push(sample);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Resample: linear interpolation from input rate to output rate
|
|
||||||
let in_ch = input_channels as usize;
|
|
||||||
let out_ch = output_channels as usize;
|
|
||||||
let ratio = output_sample_rate as f64 / input_sample_rate as f64;
|
|
||||||
let in_frames = data.len() / in_ch;
|
|
||||||
let out_frames = (in_frames as f64 * ratio) as usize;
|
|
||||||
|
|
||||||
for i in 0..out_frames {
|
|
||||||
let src_pos = i as f64 / ratio;
|
|
||||||
let src_idx = src_pos as usize;
|
|
||||||
let frac = (src_pos - src_idx as f64) as f32;
|
|
||||||
|
|
||||||
for ch in 0..out_ch {
|
|
||||||
// Map output channel to input channel
|
|
||||||
let in_ch_idx = ch.min(in_ch - 1);
|
|
||||||
|
|
||||||
let s0 = if src_idx < in_frames {
|
|
||||||
data[src_idx * in_ch + in_ch_idx]
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
let s1 = if src_idx + 1 < in_frames {
|
|
||||||
data[(src_idx + 1) * in_ch + in_ch_idx]
|
|
||||||
} else {
|
|
||||||
s0
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = input_tx.push(s0 + frac * (s1 - s0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|err| eprintln!("Input stream error: {}", err),
|
|
||||||
None,
|
|
||||||
) {
|
|
||||||
Ok(stream) => stream,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Warning: Could not build input stream: {}, recording will be disabled", e);
|
|
||||||
output_stream.play().map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
if let Some(emitter) = event_emitter {
|
|
||||||
Self::spawn_emitter_thread(event_rx, emitter);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(Self {
|
|
||||||
controller,
|
|
||||||
stream: output_stream,
|
|
||||||
sample_rate,
|
|
||||||
channels,
|
|
||||||
event_rx: None,
|
|
||||||
recording_mirror_rx: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start both streams
|
|
||||||
output_stream.play().map_err(|e| e.to_string())?;
|
output_stream.play().map_err(|e| e.to_string())?;
|
||||||
input_stream.play().map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
// Leak the input stream to keep it alive
|
|
||||||
Box::leak(Box::new(input_stream));
|
|
||||||
|
|
||||||
// Spawn emitter thread if provided, or store event_rx for manual polling
|
// Spawn emitter thread if provided, or store event_rx for manual polling
|
||||||
let event_rx_option = if let Some(emitter) = event_emitter {
|
let event_rx_option = if let Some(emitter) = event_emitter {
|
||||||
|
|
@ -278,6 +154,8 @@ impl AudioSystem {
|
||||||
Some(event_rx)
|
Some(event_rx)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Input stream is NOT opened here — call open_input_stream() when an
|
||||||
|
// audio input track is actually selected, to avoid constant ALSA wakeups.
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
controller,
|
controller,
|
||||||
stream: output_stream,
|
stream: output_stream,
|
||||||
|
|
@ -285,6 +163,8 @@ impl AudioSystem {
|
||||||
channels,
|
channels,
|
||||||
event_rx: event_rx_option,
|
event_rx: event_rx_option,
|
||||||
recording_mirror_rx: Some(mirror_rx),
|
recording_mirror_rx: Some(mirror_rx),
|
||||||
|
input_tx: Some(input_tx),
|
||||||
|
input_stream: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -293,6 +173,99 @@ impl AudioSystem {
|
||||||
self.recording_mirror_rx.take()
|
self.recording_mirror_rx.take()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open the microphone/line-in input stream.
|
||||||
|
///
|
||||||
|
/// Call this as soon as an audio input track is selected so the stream is
|
||||||
|
/// ready before recording starts. The stream is opened with the same fixed
|
||||||
|
/// buffer size as the output stream to avoid ALSA spinning at high callback
|
||||||
|
/// rates with its tiny default buffer.
|
||||||
|
///
|
||||||
|
/// No-ops if the stream is already open.
|
||||||
|
pub fn open_input_stream(&mut self, buffer_size: u32) -> Result<(), String> {
|
||||||
|
if self.input_stream.is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let mut input_tx = match self.input_tx.take() {
|
||||||
|
Some(tx) => tx,
|
||||||
|
None => return Err("Input ring-buffer already consumed".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let host = cpal::default_host();
|
||||||
|
let input_device = host.default_input_device()
|
||||||
|
.ok_or("No input device available")?;
|
||||||
|
|
||||||
|
let default_cfg = input_device.default_input_config()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let mut input_config: cpal::StreamConfig = default_cfg.into();
|
||||||
|
// Match the output buffer size so ALSA wakes up at the same rate as
|
||||||
|
// the output thread — prevents the ~750 wakeups/sec that the default
|
||||||
|
// 64-frame buffer causes.
|
||||||
|
if !cfg!(target_os = "windows") {
|
||||||
|
input_config.buffer_size = cpal::BufferSize::Fixed(buffer_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
let input_sample_rate = input_config.sample_rate;
|
||||||
|
let input_channels = input_config.channels as u32;
|
||||||
|
let output_sample_rate = self.sample_rate;
|
||||||
|
let output_channels = self.channels;
|
||||||
|
let needs_resample = input_sample_rate != output_sample_rate
|
||||||
|
|| input_channels != output_channels;
|
||||||
|
|
||||||
|
if needs_resample {
|
||||||
|
eprintln!("[AUDIO] Input: {}Hz {}ch → resampling to {}Hz {}ch",
|
||||||
|
input_sample_rate, input_channels, output_sample_rate, output_channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stream = input_device.build_input_stream(
|
||||||
|
&input_config,
|
||||||
|
move |data: &[f32], _: &cpal::InputCallbackInfo| {
|
||||||
|
if !needs_resample {
|
||||||
|
for &s in data { let _ = input_tx.push(s); }
|
||||||
|
} else {
|
||||||
|
let in_ch = input_channels as usize;
|
||||||
|
let out_ch = output_channels as usize;
|
||||||
|
let ratio = output_sample_rate as f64 / input_sample_rate as f64;
|
||||||
|
let in_frames = data.len() / in_ch;
|
||||||
|
let out_frames = (in_frames as f64 * ratio) as usize;
|
||||||
|
for i in 0..out_frames {
|
||||||
|
let src_pos = i as f64 / ratio;
|
||||||
|
let src_idx = src_pos as usize;
|
||||||
|
let frac = (src_pos - src_idx as f64) as f32;
|
||||||
|
for ch in 0..out_ch {
|
||||||
|
let ic = ch.min(in_ch - 1);
|
||||||
|
let s0 = data.get(src_idx * in_ch + ic).copied().unwrap_or(0.0);
|
||||||
|
let s1 = data.get((src_idx + 1) * in_ch + ic).copied().unwrap_or(s0);
|
||||||
|
let _ = input_tx.push(s0 + frac * (s1 - s0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|err| eprintln!("Input stream error: {err}"),
|
||||||
|
None,
|
||||||
|
).map_err(|e| format!("Failed to build input stream: {e}"))?;
|
||||||
|
|
||||||
|
stream.play().map_err(|e| e.to_string())?;
|
||||||
|
self.input_stream = Some(stream);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close the input stream (e.g. when the last audio input track is removed).
|
||||||
|
pub fn close_input_stream(&mut self) {
|
||||||
|
self.input_stream = None; // Drop stops the stream
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract an [`InputStreamOpener`] that can be stored independently and
|
||||||
|
/// used to open the microphone/line-in stream on demand.
|
||||||
|
/// Returns `None` if called a second time.
|
||||||
|
pub fn take_input_opener(&mut self) -> Option<InputStreamOpener> {
|
||||||
|
self.input_tx.take().map(|tx| InputStreamOpener {
|
||||||
|
input_tx: tx,
|
||||||
|
sample_rate: self.sample_rate,
|
||||||
|
channels: self.channels,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Spawn a background thread to emit events from the ringbuffer
|
/// Spawn a background thread to emit events from the ringbuffer
|
||||||
fn spawn_emitter_thread(mut event_rx: rtrb::Consumer<AudioEvent>, emitter: std::sync::Arc<dyn EventEmitter>) {
|
fn spawn_emitter_thread(mut event_rx: rtrb::Consumer<AudioEvent>, emitter: std::sync::Arc<dyn EventEmitter>) {
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
|
|
@ -308,3 +281,77 @@ impl AudioSystem {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Self-contained handle for opening the microphone/line-in stream on demand.
|
||||||
|
///
|
||||||
|
/// Obtained via [`AudioSystem::take_input_opener`]. Call [`open`](Self::open)
|
||||||
|
/// when the user selects an audio input track; store the returned
|
||||||
|
/// `cpal::Stream` to keep it alive (dropping it stops the stream).
|
||||||
|
pub struct InputStreamOpener {
|
||||||
|
input_tx: rtrb::Producer<f32>,
|
||||||
|
sample_rate: u32,
|
||||||
|
channels: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputStreamOpener {
|
||||||
|
/// Open and start the input stream with the given buffer size.
|
||||||
|
///
|
||||||
|
/// Uses the same `buffer_size` as the output stream so ALSA wakes up at
|
||||||
|
/// the same rate (~187/s at 256 frames) rather than the ~750/s it defaults
|
||||||
|
/// to with 64-frame buffers.
|
||||||
|
pub fn open(mut self, buffer_size: u32) -> Result<cpal::Stream, String> {
|
||||||
|
let host = cpal::default_host();
|
||||||
|
let device = host.default_input_device()
|
||||||
|
.ok_or("No input device available")?;
|
||||||
|
|
||||||
|
let default_cfg = device.default_input_config()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let mut cfg: cpal::StreamConfig = default_cfg.into();
|
||||||
|
if !cfg!(target_os = "windows") {
|
||||||
|
cfg.buffer_size = cpal::BufferSize::Fixed(buffer_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
let in_rate = cfg.sample_rate;
|
||||||
|
let in_ch = cfg.channels as u32;
|
||||||
|
let out_rate = self.sample_rate;
|
||||||
|
let out_ch = self.channels;
|
||||||
|
let needs_resample = in_rate != out_rate || in_ch != out_ch;
|
||||||
|
|
||||||
|
if needs_resample {
|
||||||
|
eprintln!("[AUDIO] Input: {}Hz {}ch → resampling to {}Hz {}ch",
|
||||||
|
in_rate, in_ch, out_rate, out_ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stream = device.build_input_stream(
|
||||||
|
&cfg,
|
||||||
|
move |data: &[f32], _: &cpal::InputCallbackInfo| {
|
||||||
|
if !needs_resample {
|
||||||
|
for &s in data { let _ = self.input_tx.push(s); }
|
||||||
|
} else {
|
||||||
|
let ic = in_ch as usize;
|
||||||
|
let oc = out_ch as usize;
|
||||||
|
let ratio = out_rate as f64 / in_rate as f64;
|
||||||
|
let in_frames = data.len() / ic;
|
||||||
|
let out_frames = (in_frames as f64 * ratio) as usize;
|
||||||
|
for i in 0..out_frames {
|
||||||
|
let src = i as f64 / ratio;
|
||||||
|
let si = src as usize;
|
||||||
|
let f = (src - si as f64) as f32;
|
||||||
|
for ch in 0..oc {
|
||||||
|
let ich = ch.min(ic - 1);
|
||||||
|
let s0 = data.get(si * ic + ich).copied().unwrap_or(0.0);
|
||||||
|
let s1 = data.get((si + 1) * ic + ich).copied().unwrap_or(s0);
|
||||||
|
let _ = self.input_tx.push(s0 + f * (s1 - s0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|err| eprintln!("Input stream error: {err}"),
|
||||||
|
None,
|
||||||
|
).map_err(|e| format!("Failed to build input stream: {e}"))?;
|
||||||
|
|
||||||
|
stream.play().map_err(|e| e.to_string())?;
|
||||||
|
Ok(stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -796,6 +796,13 @@ struct EditorApp {
|
||||||
#[allow(dead_code)] // Must be kept alive to maintain audio output
|
#[allow(dead_code)] // Must be kept alive to maintain audio output
|
||||||
audio_stream: Option<cpal::Stream>,
|
audio_stream: Option<cpal::Stream>,
|
||||||
audio_controller: Option<std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>>,
|
audio_controller: Option<std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>>,
|
||||||
|
/// Holds `input_tx` and device info needed to open the microphone stream on
|
||||||
|
/// demand (when the user selects an audio input track).
|
||||||
|
audio_input: Option<daw_backend::InputStreamOpener>,
|
||||||
|
/// Active microphone/line-in stream; kept alive while an audio input track is selected.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
audio_input_stream: Option<cpal::Stream>,
|
||||||
|
audio_buffer_size: u32,
|
||||||
audio_event_rx: Option<rtrb::Consumer<daw_backend::AudioEvent>>,
|
audio_event_rx: Option<rtrb::Consumer<daw_backend::AudioEvent>>,
|
||||||
audio_events_pending: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
audio_events_pending: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||||
/// Count of in-flight graph preset loads — keeps the repaint loop alive
|
/// Count of in-flight graph preset loads — keeps the repaint loop alive
|
||||||
|
|
@ -1004,13 +1011,16 @@ impl EditorApp {
|
||||||
let action_executor = lightningbeam_core::action::ActionExecutor::new(document);
|
let action_executor = lightningbeam_core::action::ActionExecutor::new(document);
|
||||||
|
|
||||||
// Initialize audio system and destructure it for sharing
|
// Initialize audio system and destructure it for sharing
|
||||||
let (audio_stream, audio_controller, audio_event_rx, audio_sample_rate, audio_channels, file_command_tx, recording_mirror_rx) =
|
let (audio_stream, audio_controller, audio_event_rx, audio_sample_rate, audio_channels, file_command_tx, recording_mirror_rx, audio_input) =
|
||||||
match daw_backend::AudioSystem::new(None, config.audio_buffer_size) {
|
match daw_backend::AudioSystem::new(None, config.audio_buffer_size) {
|
||||||
Ok(mut audio_system) => {
|
Ok(mut audio_system) => {
|
||||||
println!("✅ Audio engine initialized successfully");
|
println!("✅ Audio engine initialized successfully");
|
||||||
|
|
||||||
// Extract components
|
// Extract components
|
||||||
let mirror_rx = audio_system.take_recording_mirror_rx();
|
let mirror_rx = audio_system.take_recording_mirror_rx();
|
||||||
|
// take_input_opener pulls out input_tx + sample_rate/channels into
|
||||||
|
// a self-contained struct that can open the stream on demand.
|
||||||
|
let input_opener = audio_system.take_input_opener();
|
||||||
let stream = audio_system.stream;
|
let stream = audio_system.stream;
|
||||||
let sample_rate = audio_system.sample_rate;
|
let sample_rate = audio_system.sample_rate;
|
||||||
let channels = audio_system.channels;
|
let channels = audio_system.channels;
|
||||||
|
|
@ -1022,7 +1032,7 @@ impl EditorApp {
|
||||||
// Spawn file operations worker
|
// Spawn file operations worker
|
||||||
let file_command_tx = FileOperationsWorker::spawn(controller.clone());
|
let file_command_tx = FileOperationsWorker::spawn(controller.clone());
|
||||||
|
|
||||||
(Some(stream), Some(controller), event_rx, sample_rate, channels, file_command_tx, mirror_rx)
|
(Some(stream), Some(controller), event_rx, sample_rate, channels, file_command_tx, mirror_rx, input_opener)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("❌ Failed to initialize audio engine: {}", e);
|
eprintln!("❌ Failed to initialize audio engine: {}", e);
|
||||||
|
|
@ -1030,7 +1040,7 @@ impl EditorApp {
|
||||||
|
|
||||||
// Create a dummy channel for file operations (won't be used)
|
// Create a dummy channel for file operations (won't be used)
|
||||||
let (tx, _rx) = std::sync::mpsc::channel();
|
let (tx, _rx) = std::sync::mpsc::channel();
|
||||||
(None, None, None, 48000, 2, tx, None)
|
(None, None, None, 48000, 2, tx, None, None)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1078,6 +1088,9 @@ impl EditorApp {
|
||||||
audio_stream,
|
audio_stream,
|
||||||
audio_controller,
|
audio_controller,
|
||||||
audio_event_rx,
|
audio_event_rx,
|
||||||
|
audio_input,
|
||||||
|
audio_input_stream: None,
|
||||||
|
audio_buffer_size: config.audio_buffer_size,
|
||||||
audio_events_pending: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
audio_events_pending: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||||
pending_graph_loads: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)),
|
pending_graph_loads: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)),
|
||||||
commit_raster_floating_if_any: false,
|
commit_raster_floating_if_any: false,
|
||||||
|
|
@ -5679,6 +5692,9 @@ impl eframe::App for EditorApp {
|
||||||
schneider_max_error: &mut self.schneider_max_error,
|
schneider_max_error: &mut self.schneider_max_error,
|
||||||
raster_settings: &mut self.raster_settings,
|
raster_settings: &mut self.raster_settings,
|
||||||
audio_controller: self.audio_controller.as_ref(),
|
audio_controller: self.audio_controller.as_ref(),
|
||||||
|
audio_input_opener: &mut self.audio_input,
|
||||||
|
audio_input_stream: &mut self.audio_input_stream,
|
||||||
|
audio_buffer_size: self.audio_buffer_size,
|
||||||
video_manager: &self.video_manager,
|
video_manager: &self.video_manager,
|
||||||
playback_time: &mut self.playback_time,
|
playback_time: &mut self.playback_time,
|
||||||
is_playing: &mut self.is_playing,
|
is_playing: &mut self.is_playing,
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,12 @@ pub struct SharedPaneState<'a> {
|
||||||
pub raster_settings: &'a mut crate::tools::RasterToolSettings,
|
pub raster_settings: &'a mut crate::tools::RasterToolSettings,
|
||||||
/// Audio engine controller for playback control (wrapped in Arc<Mutex<>> for thread safety)
|
/// Audio engine controller for playback control (wrapped in Arc<Mutex<>> for thread safety)
|
||||||
pub audio_controller: Option<&'a std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>>,
|
pub audio_controller: Option<&'a std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>>,
|
||||||
|
/// Opener for the microphone/line-in stream — consumed on first use.
|
||||||
|
pub audio_input_opener: &'a mut Option<daw_backend::InputStreamOpener>,
|
||||||
|
/// Live input stream handle; kept alive while recording is active.
|
||||||
|
pub audio_input_stream: &'a mut Option<cpal::Stream>,
|
||||||
|
/// Buffer size (frames) used for the output stream, passed to the input stream opener.
|
||||||
|
pub audio_buffer_size: u32,
|
||||||
/// Video manager for video decoding and frame caching
|
/// Video manager for video decoding and frame caching
|
||||||
pub video_manager: &'a std::sync::Arc<std::sync::Mutex<lightningbeam_core::video::VideoManager>>,
|
pub video_manager: &'a std::sync::Arc<std::sync::Mutex<lightningbeam_core::video::VideoManager>>,
|
||||||
/// Mapping from Document layer UUIDs to daw-backend TrackIds
|
/// Mapping from Document layer UUIDs to daw-backend TrackIds
|
||||||
|
|
|
||||||
|
|
@ -657,6 +657,15 @@ impl TimelinePane {
|
||||||
}
|
}
|
||||||
RecordCandidate::AudioSampled => {
|
RecordCandidate::AudioSampled => {
|
||||||
if let Some(&track_id) = shared.layer_to_track_map.get(&layer_id) {
|
if let Some(&track_id) = shared.layer_to_track_map.get(&layer_id) {
|
||||||
|
// Open the input stream now if it hasn't been opened yet.
|
||||||
|
if shared.audio_input_stream.is_none() {
|
||||||
|
if let Some(opener) = shared.audio_input_opener.take() {
|
||||||
|
match opener.open(shared.audio_buffer_size) {
|
||||||
|
Ok(stream) => *shared.audio_input_stream = Some(stream),
|
||||||
|
Err(e) => eprintln!("⚠️ Could not open input stream: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(controller_arc) = shared.audio_controller {
|
if let Some(controller_arc) = shared.audio_controller {
|
||||||
let mut controller = controller_arc.lock().unwrap();
|
let mut controller = controller_arc.lock().unwrap();
|
||||||
controller.start_recording(track_id, start_time);
|
controller.start_recording(track_id, start_time);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue