Compare commits
2 Commits
8e9d90ed92
...
49b822da8c
| Author | SHA1 | Date |
|---|---|---|
|
|
49b822da8c | |
|
|
a6e04ae89b |
|
|
@ -71,6 +71,16 @@ pub struct Engine {
|
||||||
// Disk reader for streaming playback of compressed files
|
// Disk reader for streaming playback of compressed files
|
||||||
disk_reader: Option<crate::audio::disk_reader::DiskReader>,
|
disk_reader: Option<crate::audio::disk_reader::DiskReader>,
|
||||||
|
|
||||||
|
// Input monitoring and metering
|
||||||
|
input_monitoring: bool,
|
||||||
|
input_gain: f32,
|
||||||
|
input_level_peak: f32,
|
||||||
|
input_level_counter: usize,
|
||||||
|
output_level_peak_l: f32,
|
||||||
|
output_level_peak_r: f32,
|
||||||
|
output_level_counter: usize,
|
||||||
|
track_level_counter: usize,
|
||||||
|
|
||||||
// Callback timing diagnostics (enabled by DAW_AUDIO_DEBUG=1)
|
// Callback timing diagnostics (enabled by DAW_AUDIO_DEBUG=1)
|
||||||
debug_audio: bool,
|
debug_audio: bool,
|
||||||
callback_count: u64,
|
callback_count: u64,
|
||||||
|
|
@ -138,6 +148,14 @@ impl Engine {
|
||||||
metronome: Metronome::new(sample_rate),
|
metronome: Metronome::new(sample_rate),
|
||||||
recording_sample_buffer: Vec::with_capacity(4096),
|
recording_sample_buffer: Vec::with_capacity(4096),
|
||||||
disk_reader: Some(disk_reader),
|
disk_reader: Some(disk_reader),
|
||||||
|
input_monitoring: false,
|
||||||
|
input_gain: 1.0,
|
||||||
|
input_level_peak: 0.0,
|
||||||
|
input_level_counter: 0,
|
||||||
|
output_level_peak_l: 0.0,
|
||||||
|
output_level_peak_r: 0.0,
|
||||||
|
output_level_counter: 0,
|
||||||
|
track_level_counter: 0,
|
||||||
debug_audio: std::env::var("DAW_AUDIO_DEBUG").map_or(false, |v| v == "1"),
|
debug_audio: std::env::var("DAW_AUDIO_DEBUG").map_or(false, |v| v == "1"),
|
||||||
callback_count: 0,
|
callback_count: 0,
|
||||||
timing_worst_total_us: 0,
|
timing_worst_total_us: 0,
|
||||||
|
|
@ -380,73 +398,116 @@ impl Engine {
|
||||||
self.process_live_midi(output);
|
self.process_live_midi(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process recording if active (independent of playback state)
|
// Compute stereo output peaks for master VU meter (independent of playback state)
|
||||||
if let Some(recording) = &mut self.recording_state {
|
{
|
||||||
|
let channels = self.channels as usize;
|
||||||
|
for frame in output.chunks(channels) {
|
||||||
|
if channels >= 2 {
|
||||||
|
self.output_level_peak_l = self.output_level_peak_l.max(frame[0].abs());
|
||||||
|
self.output_level_peak_r = self.output_level_peak_r.max(frame[1].abs());
|
||||||
|
} else {
|
||||||
|
let v = frame[0].abs();
|
||||||
|
self.output_level_peak_l = self.output_level_peak_l.max(v);
|
||||||
|
self.output_level_peak_r = self.output_level_peak_r.max(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.output_level_counter += output.len();
|
||||||
|
let meter_interval = self.sample_rate as usize / 20; // ~50ms
|
||||||
|
if self.output_level_counter >= meter_interval {
|
||||||
|
let _ = self.event_tx.push(AudioEvent::OutputLevel(self.output_level_peak_l, self.output_level_peak_r));
|
||||||
|
self.output_level_peak_l = 0.0;
|
||||||
|
self.output_level_peak_r = 0.0;
|
||||||
|
self.output_level_counter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send per-track peak levels periodically
|
||||||
|
self.track_level_counter += output.len();
|
||||||
|
if self.track_level_counter >= meter_interval {
|
||||||
|
let levels = self.project.collect_track_peaks();
|
||||||
|
let _ = self.event_tx.push(AudioEvent::TrackLevels(levels));
|
||||||
|
self.track_level_counter = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process input monitoring and/or recording (independent of playback state)
|
||||||
|
let is_recording = self.recording_state.is_some();
|
||||||
|
if is_recording || self.input_monitoring {
|
||||||
if let Some(input_rx) = &mut self.input_rx {
|
if let Some(input_rx) = &mut self.input_rx {
|
||||||
// Phase 1: Discard stale samples by popping without storing
|
// Phase 1: Discard stale samples during recording skip phase
|
||||||
// (fast — no Vec push, no add_samples overhead)
|
if let Some(recording) = &mut self.recording_state {
|
||||||
while recording.samples_to_skip > 0 {
|
while recording.samples_to_skip > 0 {
|
||||||
match input_rx.pop() {
|
match input_rx.pop() {
|
||||||
Ok(_) => recording.samples_to_skip -= 1,
|
Ok(_) => recording.samples_to_skip -= 1,
|
||||||
Err(_) => break,
|
Err(_) => break,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 2: Pull fresh samples for actual recording
|
// Phase 2: Pull fresh samples
|
||||||
self.recording_sample_buffer.clear();
|
self.recording_sample_buffer.clear();
|
||||||
while let Ok(sample) = input_rx.pop() {
|
while let Ok(sample) = input_rx.pop() {
|
||||||
self.recording_sample_buffer.push(sample);
|
// Apply input gain
|
||||||
|
self.recording_sample_buffer.push(sample * self.input_gain);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add samples to recording
|
|
||||||
if !self.recording_sample_buffer.is_empty() {
|
if !self.recording_sample_buffer.is_empty() {
|
||||||
// Calculate how many samples will be skipped (stale buffer data)
|
// Compute input peak for VU metering
|
||||||
let skip = if recording.paused {
|
let input_peak = self.recording_sample_buffer.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
|
||||||
self.recording_sample_buffer.len()
|
self.input_level_peak = self.input_level_peak.max(input_peak);
|
||||||
} else {
|
self.input_level_counter += self.recording_sample_buffer.len();
|
||||||
recording.samples_to_skip.min(self.recording_sample_buffer.len())
|
let meter_interval = self.sample_rate as usize / 20; // ~50ms
|
||||||
};
|
if self.input_level_counter >= meter_interval {
|
||||||
|
let _ = self.event_tx.push(AudioEvent::InputLevel(self.input_level_peak));
|
||||||
|
self.input_level_peak = 0.0;
|
||||||
|
self.input_level_counter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
match recording.add_samples(&self.recording_sample_buffer) {
|
// Feed samples to recording if active
|
||||||
Ok(_flushed) => {
|
if let Some(recording) = &mut self.recording_state {
|
||||||
// Mirror non-skipped samples to UI for live waveform display
|
let skip = if recording.paused {
|
||||||
if skip < self.recording_sample_buffer.len() {
|
self.recording_sample_buffer.len()
|
||||||
if let Some(ref mut mirror_tx) = self.recording_mirror_tx {
|
} else {
|
||||||
for &sample in &self.recording_sample_buffer[skip..] {
|
recording.samples_to_skip.min(self.recording_sample_buffer.len())
|
||||||
let _ = mirror_tx.push(sample);
|
};
|
||||||
|
|
||||||
|
match recording.add_samples(&self.recording_sample_buffer) {
|
||||||
|
Ok(_flushed) => {
|
||||||
|
// Mirror non-skipped samples to UI for live waveform display
|
||||||
|
if skip < self.recording_sample_buffer.len() {
|
||||||
|
if let Some(ref mut mirror_tx) = self.recording_mirror_tx {
|
||||||
|
for &sample in &self.recording_sample_buffer[skip..] {
|
||||||
|
let _ = mirror_tx.push(sample);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Update clip duration every callback for sample-accurate timing
|
// Update clip duration every callback for sample-accurate timing
|
||||||
let duration = recording.duration();
|
let duration = recording.duration();
|
||||||
let clip_id = recording.clip_id;
|
let clip_id = recording.clip_id;
|
||||||
let track_id = recording.track_id;
|
let track_id = recording.track_id;
|
||||||
|
|
||||||
// Update clip duration in project as recording progresses
|
// Update clip duration in project as recording progresses
|
||||||
if let Some(crate::audio::track::TrackNode::Audio(track)) = self.project.get_track_mut(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) {
|
if let Some(clip) = track.clips.iter_mut().find(|c| c.id == clip_id) {
|
||||||
// Update both internal_end and external_duration as recording progresses
|
clip.internal_end = clip.internal_start + duration;
|
||||||
clip.internal_end = clip.internal_start + duration;
|
clip.external_duration = duration;
|
||||||
clip.external_duration = duration;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send progress event periodically (every ~0.1 seconds)
|
||||||
|
self.recording_progress_counter += self.recording_sample_buffer.len();
|
||||||
|
if self.recording_progress_counter >= (self.sample_rate as usize / 10) {
|
||||||
|
let _ = self.event_tx.push(AudioEvent::RecordingProgress(clip_id, duration));
|
||||||
|
self.recording_progress_counter = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
// Send progress event periodically (every ~0.1 seconds)
|
let _ = self.event_tx.push(AudioEvent::RecordingError(
|
||||||
self.recording_progress_counter += self.recording_sample_buffer.len();
|
format!("Recording write error: {}", e)
|
||||||
if self.recording_progress_counter >= (self.sample_rate as usize / 10) {
|
));
|
||||||
let _ = self.event_tx.push(AudioEvent::RecordingProgress(clip_id, duration));
|
self.recording_state = None;
|
||||||
self.recording_progress_counter = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
// Recording error occurred
|
|
||||||
let _ = self.event_tx.push(AudioEvent::RecordingError(
|
|
||||||
format!("Recording write error: {}", e)
|
|
||||||
));
|
|
||||||
// Stop recording on error
|
|
||||||
self.recording_state = None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1136,6 +1197,14 @@ impl Engine {
|
||||||
self.metronome.set_enabled(enabled);
|
self.metronome.set_enabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Command::SetInputMonitoring(enabled) => {
|
||||||
|
self.input_monitoring = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
Command::SetInputGain(gain) => {
|
||||||
|
self.input_gain = gain;
|
||||||
|
}
|
||||||
|
|
||||||
Command::SetTempo(bpm, time_sig) => {
|
Command::SetTempo(bpm, time_sig) => {
|
||||||
self.metronome.update_timing(bpm, time_sig);
|
self.metronome.update_timing(bpm, time_sig);
|
||||||
self.project.set_tempo(bpm, time_sig.0);
|
self.project.set_tempo(bpm, time_sig.0);
|
||||||
|
|
@ -2851,6 +2920,16 @@ impl EngineController {
|
||||||
let _ = self.command_tx.push(Command::SetTrackSolo(track_id, solo));
|
let _ = self.command_tx.push(Command::SetTrackSolo(track_id, solo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enable or disable input monitoring (mic level metering)
|
||||||
|
pub fn set_input_monitoring(&mut self, enabled: bool) {
|
||||||
|
let _ = self.command_tx.push(Command::SetInputMonitoring(enabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the input gain multiplier (applied before recording)
|
||||||
|
pub fn set_input_gain(&mut self, gain: f32) {
|
||||||
|
let _ = self.command_tx.push(Command::SetInputGain(gain));
|
||||||
|
}
|
||||||
|
|
||||||
/// Move a clip to a new timeline position (changes external_start)
|
/// Move a clip to a new timeline position (changes external_start)
|
||||||
pub fn move_clip(&mut self, track_id: TrackId, clip_id: ClipId, new_start_time: f64) {
|
pub fn move_clip(&mut self, track_id: TrackId, clip_id: ClipId, new_start_time: f64) {
|
||||||
let _ = self.command_tx.push(Command::MoveClip(track_id, clip_id, new_start_time));
|
let _ = self.command_tx.push(Command::MoveClip(track_id, clip_id, new_start_time));
|
||||||
|
|
|
||||||
|
|
@ -441,13 +441,34 @@ impl Project {
|
||||||
// Handle audio track vs MIDI track vs group track
|
// Handle audio track vs MIDI track vs group track
|
||||||
match self.tracks.get_mut(&track_id) {
|
match self.tracks.get_mut(&track_id) {
|
||||||
Some(TrackNode::Audio(track)) => {
|
Some(TrackNode::Audio(track)) => {
|
||||||
// Render audio track directly into output
|
// Render audio track into a temp buffer for peak measurement
|
||||||
track.render(output, audio_pool, ctx.playhead_seconds, ctx.sample_rate, ctx.channels);
|
let mut track_buffer = buffer_pool.acquire();
|
||||||
|
track_buffer.resize(output.len(), 0.0);
|
||||||
|
track_buffer.fill(0.0);
|
||||||
|
track.render(&mut track_buffer, audio_pool, ctx.playhead_seconds, ctx.sample_rate, ctx.channels);
|
||||||
|
// Accumulate peak level for VU metering (max over meter interval)
|
||||||
|
let buffer_peak = track_buffer.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
|
||||||
|
track.peak_level = track.peak_level.max(buffer_peak);
|
||||||
|
// Mix into output
|
||||||
|
for (out, src) in output.iter_mut().zip(track_buffer.iter()) {
|
||||||
|
*out += src;
|
||||||
|
}
|
||||||
|
buffer_pool.release(track_buffer);
|
||||||
}
|
}
|
||||||
Some(TrackNode::Midi(track)) => {
|
Some(TrackNode::Midi(track)) => {
|
||||||
// Render MIDI track directly into output
|
// Render MIDI track into a temp buffer for peak measurement
|
||||||
// Access midi_clip_pool from self - safe because we only need immutable access
|
let mut track_buffer = buffer_pool.acquire();
|
||||||
track.render(output, &self.midi_clip_pool, ctx.playhead_seconds, ctx.sample_rate, ctx.channels);
|
track_buffer.resize(output.len(), 0.0);
|
||||||
|
track_buffer.fill(0.0);
|
||||||
|
track.render(&mut track_buffer, &self.midi_clip_pool, ctx.playhead_seconds, ctx.sample_rate, ctx.channels);
|
||||||
|
// Accumulate peak level for VU metering (max over meter interval)
|
||||||
|
let buffer_peak = track_buffer.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
|
||||||
|
track.peak_level = track.peak_level.max(buffer_peak);
|
||||||
|
// Mix into output
|
||||||
|
for (out, src) in output.iter_mut().zip(track_buffer.iter()) {
|
||||||
|
*out += src;
|
||||||
|
}
|
||||||
|
buffer_pool.release(track_buffer);
|
||||||
}
|
}
|
||||||
Some(TrackNode::Group(group)) => {
|
Some(TrackNode::Group(group)) => {
|
||||||
// Skip rendering if playhead is outside the metatrack's trim window
|
// Skip rendering if playhead is outside the metatrack's trim window
|
||||||
|
|
@ -534,6 +555,25 @@ impl Project {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collect per-track peak levels for VU metering and reset accumulators
|
||||||
|
pub fn collect_track_peaks(&mut self) -> Vec<(TrackId, f32)> {
|
||||||
|
let mut levels = Vec::new();
|
||||||
|
for (id, track) in &mut self.tracks {
|
||||||
|
match track {
|
||||||
|
TrackNode::Audio(t) => {
|
||||||
|
levels.push((*id, t.peak_level));
|
||||||
|
t.peak_level = 0.0;
|
||||||
|
}
|
||||||
|
TrackNode::Midi(t) => {
|
||||||
|
levels.push((*id, t.peak_level));
|
||||||
|
t.peak_level = 0.0;
|
||||||
|
}
|
||||||
|
TrackNode::Group(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
levels
|
||||||
|
}
|
||||||
|
|
||||||
/// Stop all notes on all MIDI tracks
|
/// Stop all notes on all MIDI tracks
|
||||||
pub fn stop_all_notes(&mut self) {
|
pub fn stop_all_notes(&mut self) {
|
||||||
for track in self.tracks.values_mut() {
|
for track in self.tracks.values_mut() {
|
||||||
|
|
|
||||||
|
|
@ -435,6 +435,10 @@ pub struct MidiTrack {
|
||||||
/// Used to detect when the playhead exits a clip, so we can send all-notes-off.
|
/// Used to detect when the playhead exits a clip, so we can send all-notes-off.
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
prev_active_instances: HashSet<MidiClipInstanceId>,
|
prev_active_instances: HashSet<MidiClipInstanceId>,
|
||||||
|
|
||||||
|
/// Peak level of last render() call (for VU metering)
|
||||||
|
#[serde(skip, default)]
|
||||||
|
pub peak_level: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for MidiTrack {
|
impl Clone for MidiTrack {
|
||||||
|
|
@ -452,6 +456,7 @@ impl Clone for MidiTrack {
|
||||||
next_automation_id: self.next_automation_id,
|
next_automation_id: self.next_automation_id,
|
||||||
live_midi_queue: Vec::new(), // Don't clone live MIDI queue
|
live_midi_queue: Vec::new(), // Don't clone live MIDI queue
|
||||||
prev_active_instances: HashSet::new(),
|
prev_active_instances: HashSet::new(),
|
||||||
|
peak_level: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -479,6 +484,7 @@ impl MidiTrack {
|
||||||
next_automation_id: 0,
|
next_automation_id: 0,
|
||||||
live_midi_queue: Vec::new(),
|
live_midi_queue: Vec::new(),
|
||||||
prev_active_instances: HashSet::new(),
|
prev_active_instances: HashSet::new(),
|
||||||
|
peak_level: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -705,6 +711,10 @@ pub struct AudioTrack {
|
||||||
/// Pre-allocated buffer for clip rendering (avoids heap allocation per callback)
|
/// Pre-allocated buffer for clip rendering (avoids heap allocation per callback)
|
||||||
#[serde(skip, default)]
|
#[serde(skip, default)]
|
||||||
clip_render_buffer: Vec<f32>,
|
clip_render_buffer: Vec<f32>,
|
||||||
|
|
||||||
|
/// Peak level of last render() call (for VU metering)
|
||||||
|
#[serde(skip, default)]
|
||||||
|
pub peak_level: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for AudioTrack {
|
impl Clone for AudioTrack {
|
||||||
|
|
@ -721,6 +731,7 @@ impl Clone for AudioTrack {
|
||||||
effects_graph_preset: self.effects_graph_preset.clone(),
|
effects_graph_preset: self.effects_graph_preset.clone(),
|
||||||
effects_graph: default_audio_graph(), // Create fresh graph, not cloned
|
effects_graph: default_audio_graph(), // Create fresh graph, not cloned
|
||||||
clip_render_buffer: Vec::new(),
|
clip_render_buffer: Vec::new(),
|
||||||
|
peak_level: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -764,6 +775,7 @@ impl AudioTrack {
|
||||||
effects_graph_preset: None,
|
effects_graph_preset: None,
|
||||||
effects_graph,
|
effects_graph,
|
||||||
clip_render_buffer: Vec::new(),
|
clip_render_buffer: Vec::new(),
|
||||||
|
peak_level: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -987,7 +999,7 @@ impl AudioTrack {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate combined gain
|
// Calculate combined gain
|
||||||
let combined_gain = clip.gain * self.volume;
|
let combined_gain = clip.gain;
|
||||||
|
|
||||||
let mut total_rendered = 0;
|
let mut total_rendered = 0;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,12 @@ pub enum Command {
|
||||||
priority: u8, // 0=Low, 1=Medium, 2=High
|
priority: u8, // 0=Low, 1=Medium, 2=High
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Input monitoring/gain commands
|
||||||
|
/// Enable or disable input monitoring (mic level metering)
|
||||||
|
SetInputMonitoring(bool),
|
||||||
|
/// Set the input gain multiplier (applied before recording)
|
||||||
|
SetInputGain(f32),
|
||||||
|
|
||||||
// Async audio import
|
// Async audio import
|
||||||
/// Import an audio file asynchronously. The engine probes the file format
|
/// Import an audio file asynchronously. The engine probes the file format
|
||||||
/// and either memory-maps it (WAV/AIFF) or sets up stream decode
|
/// and either memory-maps it (WAV/AIFF) or sets up stream decode
|
||||||
|
|
@ -333,6 +339,13 @@ pub enum AudioEvent {
|
||||||
channels: u32,
|
channels: u32,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Peak amplitude of mic input (for input monitoring meter)
|
||||||
|
InputLevel(f32),
|
||||||
|
/// Peak amplitude of mix output (for master meter), stereo (left, right)
|
||||||
|
OutputLevel(f32, f32),
|
||||||
|
/// Per-track playback peak levels
|
||||||
|
TrackLevels(Vec<(TrackId, f32)>),
|
||||||
|
|
||||||
/// Background waveform decode progress/completion for a compressed audio file.
|
/// Background waveform decode progress/completion for a compressed audio file.
|
||||||
/// Internal event — consumed by the engine to update the pool, not forwarded to UI.
|
/// Internal event — consumed by the engine to update the pool, not forwarded to UI.
|
||||||
/// `decoded_frames` < `total_frames` means partial; equal means complete.
|
/// `decoded_frames` < `total_frames` means partial; equal means complete.
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use uuid::Uuid;
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum LayerProperty {
|
pub enum LayerProperty {
|
||||||
Volume(f64),
|
Volume(f64),
|
||||||
|
InputGain(f64),
|
||||||
Muted(bool),
|
Muted(bool),
|
||||||
Soloed(bool),
|
Soloed(bool),
|
||||||
Locked(bool),
|
Locked(bool),
|
||||||
|
|
@ -25,6 +26,7 @@ pub enum LayerProperty {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum OldValue {
|
enum OldValue {
|
||||||
Volume(f64),
|
Volume(f64),
|
||||||
|
InputGain(f64),
|
||||||
Muted(bool),
|
Muted(bool),
|
||||||
Soloed(bool),
|
Soloed(bool),
|
||||||
Locked(bool),
|
Locked(bool),
|
||||||
|
|
@ -85,6 +87,7 @@ impl Action for SetLayerPropertiesAction {
|
||||||
if self.old_values[i].is_none() {
|
if self.old_values[i].is_none() {
|
||||||
self.old_values[i] = Some(match &self.property {
|
self.old_values[i] = Some(match &self.property {
|
||||||
LayerProperty::Volume(_) => OldValue::Volume(layer.volume()),
|
LayerProperty::Volume(_) => OldValue::Volume(layer.volume()),
|
||||||
|
LayerProperty::InputGain(_) => OldValue::InputGain(layer.layer().input_gain),
|
||||||
LayerProperty::Muted(_) => OldValue::Muted(layer.muted()),
|
LayerProperty::Muted(_) => OldValue::Muted(layer.muted()),
|
||||||
LayerProperty::Soloed(_) => OldValue::Soloed(layer.soloed()),
|
LayerProperty::Soloed(_) => OldValue::Soloed(layer.soloed()),
|
||||||
LayerProperty::Locked(_) => OldValue::Locked(layer.locked()),
|
LayerProperty::Locked(_) => OldValue::Locked(layer.locked()),
|
||||||
|
|
@ -104,6 +107,7 @@ impl Action for SetLayerPropertiesAction {
|
||||||
// Set new value
|
// Set new value
|
||||||
match &self.property {
|
match &self.property {
|
||||||
LayerProperty::Volume(v) => layer.set_volume(*v),
|
LayerProperty::Volume(v) => layer.set_volume(*v),
|
||||||
|
LayerProperty::InputGain(g) => layer.layer_mut().input_gain = *g,
|
||||||
LayerProperty::Muted(m) => layer.set_muted(*m),
|
LayerProperty::Muted(m) => layer.set_muted(*m),
|
||||||
LayerProperty::Soloed(s) => layer.set_soloed(*s),
|
LayerProperty::Soloed(s) => layer.set_soloed(*s),
|
||||||
LayerProperty::Locked(l) => layer.set_locked(*l),
|
LayerProperty::Locked(l) => layer.set_locked(*l),
|
||||||
|
|
@ -128,6 +132,7 @@ impl Action for SetLayerPropertiesAction {
|
||||||
if let Some(old_value) = &self.old_values[i] {
|
if let Some(old_value) = &self.old_values[i] {
|
||||||
match old_value {
|
match old_value {
|
||||||
OldValue::Volume(v) => layer.set_volume(*v),
|
OldValue::Volume(v) => layer.set_volume(*v),
|
||||||
|
OldValue::InputGain(g) => layer.layer_mut().input_gain = *g,
|
||||||
OldValue::Muted(m) => layer.set_muted(*m),
|
OldValue::Muted(m) => layer.set_muted(*m),
|
||||||
OldValue::Soloed(s) => layer.set_soloed(*s),
|
OldValue::Soloed(s) => layer.set_soloed(*s),
|
||||||
OldValue::Locked(l) => layer.set_locked(*l),
|
OldValue::Locked(l) => layer.set_locked(*l),
|
||||||
|
|
@ -159,6 +164,7 @@ impl Action for SetLayerPropertiesAction {
|
||||||
if let Some(&track_id) = backend.layer_to_track_map.get(&layer_id) {
|
if let Some(&track_id) = backend.layer_to_track_map.get(&layer_id) {
|
||||||
match &self.property {
|
match &self.property {
|
||||||
LayerProperty::Volume(v) => controller.set_track_volume(track_id, *v as f32),
|
LayerProperty::Volume(v) => controller.set_track_volume(track_id, *v as f32),
|
||||||
|
LayerProperty::InputGain(g) => controller.set_input_gain(*g as f32),
|
||||||
LayerProperty::Muted(m) => controller.set_track_mute(track_id, *m),
|
LayerProperty::Muted(m) => controller.set_track_mute(track_id, *m),
|
||||||
LayerProperty::Soloed(s) => controller.set_track_solo(track_id, *s),
|
LayerProperty::Soloed(s) => controller.set_track_solo(track_id, *s),
|
||||||
_ => {} // Locked/Opacity/Visible/CameraEnabled are UI-only
|
_ => {} // Locked/Opacity/Visible/CameraEnabled are UI-only
|
||||||
|
|
@ -183,6 +189,7 @@ impl Action for SetLayerPropertiesAction {
|
||||||
if let Some(old_value) = &self.old_values[i] {
|
if let Some(old_value) = &self.old_values[i] {
|
||||||
match old_value {
|
match old_value {
|
||||||
OldValue::Volume(v) => controller.set_track_volume(track_id, *v as f32),
|
OldValue::Volume(v) => controller.set_track_volume(track_id, *v as f32),
|
||||||
|
OldValue::InputGain(g) => controller.set_input_gain(*g as f32),
|
||||||
OldValue::Muted(m) => controller.set_track_mute(track_id, *m),
|
OldValue::Muted(m) => controller.set_track_mute(track_id, *m),
|
||||||
OldValue::Soloed(s) => controller.set_track_solo(track_id, *s),
|
OldValue::Soloed(s) => controller.set_track_solo(track_id, *s),
|
||||||
_ => {} // Locked/Opacity/Visible are UI-only
|
_ => {} // Locked/Opacity/Visible are UI-only
|
||||||
|
|
@ -196,6 +203,7 @@ impl Action for SetLayerPropertiesAction {
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
let property_name = match &self.property {
|
let property_name = match &self.property {
|
||||||
LayerProperty::Volume(_) => "volume",
|
LayerProperty::Volume(_) => "volume",
|
||||||
|
LayerProperty::InputGain(_) => "input gain",
|
||||||
LayerProperty::Muted(_) => "mute",
|
LayerProperty::Muted(_) => "mute",
|
||||||
LayerProperty::Soloed(_) => "solo",
|
LayerProperty::Soloed(_) => "solo",
|
||||||
LayerProperty::Locked(_) => "lock",
|
LayerProperty::Locked(_) => "lock",
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,8 @@ pub trait LayerTrait {
|
||||||
fn set_locked(&mut self, locked: bool);
|
fn set_locked(&mut self, locked: bool);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_input_gain() -> f64 { 1.0 }
|
||||||
|
|
||||||
/// Base layer structure
|
/// Base layer structure
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct Layer {
|
pub struct Layer {
|
||||||
|
|
@ -84,6 +86,10 @@ pub struct Layer {
|
||||||
/// Audio volume (1.0 = 100%, affects nested audio layers/clips)
|
/// Audio volume (1.0 = 100%, affects nested audio layers/clips)
|
||||||
pub volume: f64,
|
pub volume: f64,
|
||||||
|
|
||||||
|
/// Input gain for recording (1.0 = unity, range 0.0–4.0)
|
||||||
|
#[serde(default = "default_input_gain")]
|
||||||
|
pub input_gain: f64,
|
||||||
|
|
||||||
/// Audio mute state
|
/// Audio mute state
|
||||||
pub muted: bool,
|
pub muted: bool,
|
||||||
|
|
||||||
|
|
@ -108,6 +114,7 @@ impl Layer {
|
||||||
visible: true,
|
visible: true,
|
||||||
opacity: 1.0,
|
opacity: 1.0,
|
||||||
volume: 1.0, // 100% volume
|
volume: 1.0, // 100% volume
|
||||||
|
input_gain: 1.0,
|
||||||
muted: false,
|
muted: false,
|
||||||
soloed: false,
|
soloed: false,
|
||||||
locked: false,
|
locked: false,
|
||||||
|
|
@ -125,6 +132,7 @@ impl Layer {
|
||||||
visible: true,
|
visible: true,
|
||||||
opacity: 1.0,
|
opacity: 1.0,
|
||||||
volume: 1.0,
|
volume: 1.0,
|
||||||
|
input_gain: 1.0,
|
||||||
muted: false,
|
muted: false,
|
||||||
soloed: false,
|
soloed: false,
|
||||||
locked: false,
|
locked: false,
|
||||||
|
|
|
||||||
|
|
@ -814,6 +814,11 @@ struct EditorApp {
|
||||||
region_selection: Option<lightningbeam_core::selection::RegionSelection>,
|
region_selection: Option<lightningbeam_core::selection::RegionSelection>,
|
||||||
region_select_mode: lightningbeam_core::tool::RegionSelectMode,
|
region_select_mode: lightningbeam_core::tool::RegionSelectMode,
|
||||||
|
|
||||||
|
// VU meter levels
|
||||||
|
input_level: f32,
|
||||||
|
output_level: (f32, f32),
|
||||||
|
track_levels: HashMap<daw_backend::TrackId, f32>,
|
||||||
|
|
||||||
/// Cache for MIDI event data (keyed by backend midi_clip_id)
|
/// Cache for MIDI event data (keyed by backend midi_clip_id)
|
||||||
/// Prevents repeated backend queries for the same MIDI clip
|
/// Prevents repeated backend queries for the same MIDI clip
|
||||||
/// Format: (timestamp, note_number, velocity, is_note_on)
|
/// Format: (timestamp, note_number, velocity, is_note_on)
|
||||||
|
|
@ -1057,6 +1062,9 @@ impl EditorApp {
|
||||||
polygon_sides: 5, // Default to pentagon
|
polygon_sides: 5, // Default to pentagon
|
||||||
region_selection: None,
|
region_selection: None,
|
||||||
region_select_mode: lightningbeam_core::tool::RegionSelectMode::default(),
|
region_select_mode: lightningbeam_core::tool::RegionSelectMode::default(),
|
||||||
|
input_level: 0.0,
|
||||||
|
output_level: (0.0, 0.0),
|
||||||
|
track_levels: HashMap::new(),
|
||||||
midi_event_cache: HashMap::new(), // Initialize empty MIDI event cache
|
midi_event_cache: HashMap::new(), // Initialize empty MIDI event cache
|
||||||
audio_duration_cache: HashMap::new(), // Initialize empty audio duration cache
|
audio_duration_cache: HashMap::new(), // Initialize empty audio duration cache
|
||||||
audio_pools_with_new_waveforms: HashSet::new(), // Track pool indices with new raw audio
|
audio_pools_with_new_waveforms: HashSet::new(), // Track pool indices with new raw audio
|
||||||
|
|
@ -4672,6 +4680,19 @@ impl eframe::App for EditorApp {
|
||||||
);
|
);
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
}
|
}
|
||||||
|
AudioEvent::InputLevel(peak) => {
|
||||||
|
self.input_level = self.input_level.max(peak);
|
||||||
|
}
|
||||||
|
AudioEvent::OutputLevel(peak_l, peak_r) => {
|
||||||
|
self.output_level.0 = self.output_level.0.max(peak_l);
|
||||||
|
self.output_level.1 = self.output_level.1.max(peak_r);
|
||||||
|
}
|
||||||
|
AudioEvent::TrackLevels(levels) => {
|
||||||
|
for (track_id, peak) in levels {
|
||||||
|
let entry = self.track_levels.entry(track_id).or_insert(0.0);
|
||||||
|
*entry = entry.max(peak);
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {} // Ignore other events for now
|
_ => {} // Ignore other events for now
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4686,6 +4707,39 @@ impl eframe::App for EditorApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update input monitoring based on active layer
|
||||||
|
if let Some(controller) = &self.audio_controller {
|
||||||
|
let should_monitor = self.active_layer_id.map_or(false, |layer_id| {
|
||||||
|
let doc = self.action_executor.document();
|
||||||
|
if let Some(layer) = doc.get_layer(&layer_id) {
|
||||||
|
matches!(layer, lightningbeam_core::layer::AnyLayer::Audio(a) if a.audio_layer_type == lightningbeam_core::layer::AudioLayerType::Sampled)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Ok(mut ctrl) = controller.try_lock() {
|
||||||
|
ctrl.set_input_monitoring(should_monitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decay VU meter levels (~1.5s full fall at 60fps)
|
||||||
|
{
|
||||||
|
let decay = 0.97f32;
|
||||||
|
self.input_level *= decay;
|
||||||
|
self.output_level.0 *= decay;
|
||||||
|
self.output_level.1 *= decay;
|
||||||
|
for level in self.track_levels.values_mut() {
|
||||||
|
*level *= decay;
|
||||||
|
}
|
||||||
|
// Request repaint while any level is visible
|
||||||
|
let any_active = self.input_level > 0.001
|
||||||
|
|| self.output_level.0 > 0.001 || self.output_level.1 > 0.001
|
||||||
|
|| self.track_levels.values().any(|&v| v > 0.001);
|
||||||
|
if any_active {
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let _post_events_ms = _frame_start.elapsed().as_secs_f64() * 1000.0;
|
let _post_events_ms = _frame_start.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
|
||||||
// Request continuous repaints when playing to update time display
|
// Request continuous repaints when playing to update time display
|
||||||
|
|
@ -5075,6 +5129,10 @@ impl eframe::App for EditorApp {
|
||||||
target_format: self.target_format,
|
target_format: self.target_format,
|
||||||
pending_menu_actions: &mut pending_menu_actions,
|
pending_menu_actions: &mut pending_menu_actions,
|
||||||
clipboard_manager: &mut self.clipboard_manager,
|
clipboard_manager: &mut self.clipboard_manager,
|
||||||
|
input_level: self.input_level,
|
||||||
|
output_level: self.output_level,
|
||||||
|
track_levels: &self.track_levels,
|
||||||
|
track_to_layer_map: &self.track_to_layer_map,
|
||||||
waveform_stereo: self.config.waveform_stereo,
|
waveform_stereo: self.config.waveform_stereo,
|
||||||
project_generation: &mut self.project_generation,
|
project_generation: &mut self.project_generation,
|
||||||
script_to_edit: &mut self.script_to_edit,
|
script_to_edit: &mut self.script_to_edit,
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,12 @@ pub struct SharedPaneState<'a> {
|
||||||
pub pending_menu_actions: &'a mut Vec<crate::menu::MenuAction>,
|
pub pending_menu_actions: &'a mut Vec<crate::menu::MenuAction>,
|
||||||
/// Clipboard manager for cut/copy/paste operations
|
/// Clipboard manager for cut/copy/paste operations
|
||||||
pub clipboard_manager: &'a mut lightningbeam_core::clipboard::ClipboardManager,
|
pub clipboard_manager: &'a mut lightningbeam_core::clipboard::ClipboardManager,
|
||||||
|
// VU meter levels
|
||||||
|
pub input_level: f32,
|
||||||
|
pub output_level: (f32, f32),
|
||||||
|
pub track_levels: &'a std::collections::HashMap<daw_backend::TrackId, f32>,
|
||||||
|
#[allow(dead_code)] // Available for panes that need reverse track->layer lookup
|
||||||
|
pub track_to_layer_map: &'a std::collections::HashMap<daw_backend::TrackId, Uuid>,
|
||||||
/// Whether to show waveforms as stacked stereo (true) or combined mono (false)
|
/// Whether to show waveforms as stacked stereo (true) or combined mono (false)
|
||||||
pub waveform_stereo: bool,
|
pub waveform_stereo: bool,
|
||||||
/// Generation counter - incremented on project load to force reloads
|
/// Generation counter - incremented on project load to force reloads
|
||||||
|
|
|
||||||
|
|
@ -1260,6 +1260,9 @@ impl TimelinePane {
|
||||||
pending_actions: &mut Vec<Box<dyn lightningbeam_core::action::Action>>,
|
pending_actions: &mut Vec<Box<dyn lightningbeam_core::action::Action>>,
|
||||||
_document: &lightningbeam_core::document::Document,
|
_document: &lightningbeam_core::document::Document,
|
||||||
context_layers: &[&lightningbeam_core::layer::AnyLayer],
|
context_layers: &[&lightningbeam_core::layer::AnyLayer],
|
||||||
|
layer_to_track_map: &std::collections::HashMap<uuid::Uuid, daw_backend::TrackId>,
|
||||||
|
track_levels: &std::collections::HashMap<daw_backend::TrackId, f32>,
|
||||||
|
input_level: f32,
|
||||||
) {
|
) {
|
||||||
// Background for header column
|
// Background for header column
|
||||||
let header_style = theme.style(".timeline-header", ui.ctx());
|
let header_style = theme.style(".timeline-header", ui.ctx());
|
||||||
|
|
@ -1659,6 +1662,10 @@ impl TimelinePane {
|
||||||
(response, temp_slider_value)
|
(response, temp_slider_value)
|
||||||
}).inner;
|
}).inner;
|
||||||
|
|
||||||
|
// Block layer drag while interacting with the slider
|
||||||
|
if volume_response.0.dragged() || volume_response.0.has_focus() {
|
||||||
|
self.layer_control_clicked = true;
|
||||||
|
}
|
||||||
if volume_response.0.changed() {
|
if volume_response.0.changed() {
|
||||||
self.layer_control_clicked = true;
|
self.layer_control_clicked = true;
|
||||||
// Map slider position (0.0-1.0) back to volume (0.0-2.0)
|
// Map slider position (0.0-1.0) back to volume (0.0-2.0)
|
||||||
|
|
@ -1678,6 +1685,93 @@ impl TimelinePane {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Input gain slider for sampled audio layers (below volume slider)
|
||||||
|
if let lightningbeam_core::layer::AnyLayer::Audio(audio_layer) = layer_for_controls {
|
||||||
|
if audio_layer.audio_layer_type == lightningbeam_core::layer::AudioLayerType::Sampled {
|
||||||
|
let gain_slider_rect = egui::Rect::from_min_size(
|
||||||
|
egui::pos2(controls_right - slider_width, controls_top + 22.0),
|
||||||
|
egui::vec2(slider_width, 16.0),
|
||||||
|
);
|
||||||
|
let current_gain = audio_layer.layer.input_gain;
|
||||||
|
|
||||||
|
// Map gain (0.0-4.0) to slider (0.0-1.0): linear
|
||||||
|
let mut slider_val = (current_gain / 4.0) as f32;
|
||||||
|
let gain_response = ui.scope_builder(egui::UiBuilder::new().max_rect(gain_slider_rect), |ui| {
|
||||||
|
let slider = egui::Slider::new(&mut slider_val, 0.0..=1.0f32)
|
||||||
|
.show_value(false);
|
||||||
|
ui.add(slider)
|
||||||
|
}).inner;
|
||||||
|
|
||||||
|
// Block layer drag while interacting with the slider
|
||||||
|
if gain_response.dragged() || gain_response.has_focus() {
|
||||||
|
self.layer_control_clicked = true;
|
||||||
|
}
|
||||||
|
if gain_response.changed() {
|
||||||
|
self.layer_control_clicked = true;
|
||||||
|
let new_gain = (slider_val * 4.0) as f64;
|
||||||
|
pending_actions.push(Box::new(
|
||||||
|
lightningbeam_core::actions::SetLayerPropertiesAction::new(
|
||||||
|
layer_id,
|
||||||
|
lightningbeam_core::actions::LayerProperty::InputGain(new_gain),
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label
|
||||||
|
let label_rect = egui::Rect::from_min_size(
|
||||||
|
egui::pos2(gain_slider_rect.min.x - 26.0, controls_top + 22.0),
|
||||||
|
egui::vec2(24.0, 16.0),
|
||||||
|
);
|
||||||
|
ui.painter().text(
|
||||||
|
label_rect.center(),
|
||||||
|
egui::Align2::CENTER_CENTER,
|
||||||
|
"Gain",
|
||||||
|
egui::FontId::proportional(9.0),
|
||||||
|
egui::Color32::from_gray(140),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-layer VU meter bar (4px tall at bottom of header)
|
||||||
|
{
|
||||||
|
// Look up the track level for this layer
|
||||||
|
let mut level = 0.0f32;
|
||||||
|
if let Some(&track_id) = layer_to_track_map.get(&layer_id) {
|
||||||
|
if let Some(&track_level) = track_levels.get(&track_id) {
|
||||||
|
level = track_level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For active sampled audio layer, show max of track level and input level
|
||||||
|
let is_active_sampled_audio = active_layer_id.map_or(false, |id| id == layer_id)
|
||||||
|
&& matches!(layer_for_controls, lightningbeam_core::layer::AnyLayer::Audio(a) if a.audio_layer_type == lightningbeam_core::layer::AudioLayerType::Sampled);
|
||||||
|
if is_active_sampled_audio {
|
||||||
|
level = level.max(input_level);
|
||||||
|
}
|
||||||
|
|
||||||
|
if level > 0.001 {
|
||||||
|
let meter_height = 4.0;
|
||||||
|
let meter_rect = egui::Rect::from_min_size(
|
||||||
|
egui::pos2(header_rect.min.x, header_rect.max.y - meter_height - 1.0),
|
||||||
|
egui::vec2(header_rect.width(), meter_height),
|
||||||
|
);
|
||||||
|
let clamped = level.min(1.0);
|
||||||
|
let filled_width = meter_rect.width() * clamped;
|
||||||
|
let color = if clamped > 0.9 {
|
||||||
|
egui::Color32::from_rgb(220, 50, 50)
|
||||||
|
} else if clamped > 0.7 {
|
||||||
|
egui::Color32::from_rgb(220, 200, 50)
|
||||||
|
} else {
|
||||||
|
egui::Color32::from_rgb(50, 200, 80)
|
||||||
|
};
|
||||||
|
let filled = egui::Rect::from_min_size(
|
||||||
|
meter_rect.left_top(),
|
||||||
|
egui::vec2(filled_width, meter_rect.height()),
|
||||||
|
);
|
||||||
|
ui.painter().rect_filled(filled, 0.0, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Separator line at bottom
|
// Separator line at bottom
|
||||||
ui.painter().line_segment(
|
ui.painter().line_segment(
|
||||||
[
|
[
|
||||||
|
|
@ -4140,6 +4234,42 @@ impl PaneRenderer for TimelinePane {
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
|
// Stereo mix output VU meter (two stacked bars: L on top, R on bottom)
|
||||||
|
{
|
||||||
|
let meter_width = 80.0;
|
||||||
|
let meter_height = 14.0; // total height for both bars + gap
|
||||||
|
let bar_height = 6.0;
|
||||||
|
let gap = 2.0;
|
||||||
|
let (meter_rect, _) = ui.allocate_exact_size(
|
||||||
|
egui::vec2(meter_width, meter_height),
|
||||||
|
egui::Sense::hover(),
|
||||||
|
);
|
||||||
|
// Background
|
||||||
|
ui.painter().rect_filled(meter_rect, 2.0, egui::Color32::from_gray(30));
|
||||||
|
|
||||||
|
let levels = [shared.output_level.0.min(1.0), shared.output_level.1.min(1.0)];
|
||||||
|
for (i, &level) in levels.iter().enumerate() {
|
||||||
|
let bar_y = meter_rect.min.y + i as f32 * (bar_height + gap);
|
||||||
|
if level > 0.001 {
|
||||||
|
let filled_width = meter_rect.width() * level;
|
||||||
|
let color = if level > 0.9 {
|
||||||
|
egui::Color32::from_rgb(220, 50, 50)
|
||||||
|
} else if level > 0.7 {
|
||||||
|
egui::Color32::from_rgb(220, 200, 50)
|
||||||
|
} else {
|
||||||
|
egui::Color32::from_rgb(50, 200, 80)
|
||||||
|
};
|
||||||
|
let filled_rect = egui::Rect::from_min_size(
|
||||||
|
egui::pos2(meter_rect.min.x, bar_y),
|
||||||
|
egui::vec2(filled_width, bar_height),
|
||||||
|
);
|
||||||
|
ui.painter().rect_filled(filled_rect, 1.0, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
// BPM control
|
// BPM control
|
||||||
let mut bpm_val = bpm;
|
let mut bpm_val = bpm;
|
||||||
ui.label("BPM:");
|
ui.label("BPM:");
|
||||||
|
|
@ -4278,7 +4408,7 @@ impl PaneRenderer for TimelinePane {
|
||||||
|
|
||||||
// Render layer header column with clipping
|
// Render layer header column with clipping
|
||||||
ui.set_clip_rect(layer_headers_rect.intersect(original_clip_rect));
|
ui.set_clip_rect(layer_headers_rect.intersect(original_clip_rect));
|
||||||
self.render_layer_headers(ui, layer_headers_rect, shared.theme, shared.active_layer_id, shared.focus, &mut shared.pending_actions, document, &context_layers);
|
self.render_layer_headers(ui, layer_headers_rect, shared.theme, shared.active_layer_id, shared.focus, &mut shared.pending_actions, document, &context_layers, shared.layer_to_track_map, shared.track_levels, shared.input_level);
|
||||||
|
|
||||||
// Render time ruler (clip to ruler rect)
|
// Render time ruler (clip to ruler rect)
|
||||||
ui.set_clip_rect(ruler_rect.intersect(original_clip_rect));
|
ui.set_clip_rect(ruler_rect.intersect(original_clip_rect));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue