maolan 0.2.2

Rust DAW application for recording, editing, routing, automation, export, and plugin hosting
use super::*;
use crate::state::StateData;

const MIXER_STRIP_SPACING: f32 = 2.0;
const MIXER_ROW_PADDING_X: f32 = 8.0;
const MIXER_OVERSCAN_PX: f32 = 160.0;

fn mixer_strip_width_for_channels(channels: usize) -> f32 {
    use crate::consts::workspace_mixer::{
        BAY_INSET, FADER_WIDTH, METER_BAR_GAP, METER_BAR_WIDTH, METER_PAD_X, SCALE_WIDTH,
        STRIP_WIDTH,
    };

    let channels = channels.max(1);
    let meter_inner_width =
        channels as f32 * METER_BAR_WIDTH + (channels.saturating_sub(1) as f32 * METER_BAR_GAP);
    let meter_total_width = meter_inner_width + (METER_PAD_X * 2.0);
    (FADER_WIDTH + SCALE_WIDTH + 3.0 + 8.0 + meter_total_width + 16.0 + (BAY_INSET * 2.0))
        .max(STRIP_WIDTH)
}

fn visible_mixer_track_names(
    app: &Maolan,
    state: &StateData,
) -> Option<std::collections::HashSet<String>> {
    if !app.mixer_visible {
        return None;
    }

    let metronome_width = if state.metronome_enabled {
        state
            .tracks
            .iter()
            .find(|track| track.name == crate::consts::state_ids::METRONOME_TRACK_ID)
            .map(|track| mixer_strip_width_for_channels(track.audio.outs))
            .unwrap_or(0.0)
    } else {
        0.0
    };
    let hw_out_channels = state.hw_out.as_ref().map(|hw| hw.channels).unwrap_or(0);
    let master_width = mixer_strip_width_for_channels(hw_out_channels.max(1));
    let output_width = master_width
        + metronome_width
        + if metronome_width > 0.0 {
            MIXER_STRIP_SPACING
        } else {
            0.0
        };
    let viewport_width = (app.size.width - output_width).max(0.0);

    let tracks: Vec<_> = state
        .tracks
        .iter()
        .filter(|track| track.name != crate::consts::state_ids::METRONOME_TRACK_ID)
        .collect();
    if tracks.is_empty() {
        return Some(std::collections::HashSet::new());
    }

    let content_width = tracks
        .iter()
        .map(|track| mixer_strip_width_for_channels(track.audio.outs))
        .sum::<f32>()
        + (MIXER_STRIP_SPACING * tracks.len().saturating_sub(1) as f32)
        + (MIXER_ROW_PADDING_X * 2.0);
    if viewport_width <= 0.0 || content_width <= viewport_width {
        return Some(tracks.into_iter().map(|track| track.name.clone()).collect());
    }

    let max_scroll = (content_width - viewport_width).max(0.0);
    let left_edge = (app.mixer_scroll_x.clamp(0.0, 1.0) * max_scroll - MIXER_OVERSCAN_PX).max(0.0);
    let right_edge = (left_edge + viewport_width + (MIXER_OVERSCAN_PX * 2.0)).min(content_width);
    let mut current_x = MIXER_ROW_PADDING_X;
    let mut visible = std::collections::HashSet::new();
    for track in tracks {
        let width = mixer_strip_width_for_channels(track.audio.outs);
        let strip_start = current_x;
        let strip_end = strip_start + width;
        if strip_end >= left_edge && strip_start <= right_edge {
            visible.insert(track.name.clone());
        }
        current_x = strip_end + MIXER_STRIP_SPACING;
    }
    Some(visible)
}

impl Maolan {
    pub(super) fn handle_response_freeze_meter_action(
        &mut self,
        action: &Action,
    ) -> Option<Task<Message>> {
        match action {
            Action::TrackOfflineBounce {
                track_name,
                output_path,
                ..
            } => {
                if self.export_in_progress
                    && self.export_pending_bounces.remove(track_name)
                    && self.export_pending_bounces.is_empty()
                {
                    if let Some(notify) = self.export_bounce_notify.take() {
                        notify.notify_one();
                    }
                    return Some(Task::none());
                }
                self.freeze_in_progress = false;
                self.freeze_track_name = None;
                if let Some(pending) = self.pending_track_freeze_bounce.remove(track_name) {
                    if self.freeze_cancel_requested {
                        self.freeze_cancel_requested = false;
                        if let Err(_err) = std::fs::remove_file(output_path) {}
                        self.state.blocking_write().message =
                            format!("Freeze canceled for '{}'", track_name);
                        return Some(Task::none());
                    }
                    {
                        let mut state = self.state.blocking_write();
                        if let Some(track_mut) =
                            state.tracks.iter_mut().find(|t| t.name == *track_name)
                        {
                            track_mut.frozen_audio_backup = pending.backup_audio.clone();
                            track_mut.frozen_midi_backup = pending.backup_midi.clone();
                            track_mut.frozen_render_clip = Some(pending.rendered_clip_rel.clone());
                            state.message = format!("Frozen track '{}'", track_name);
                        }
                    }
                    let mut tasks = vec![self.send(Action::BeginHistoryGroup)];
                    if !pending.backup_audio.is_empty() {
                        tasks.push(self.send(Action::RemoveClip {
                            track_name: track_name.clone(),
                            kind: Kind::Audio,
                            clip_indices: (0..pending.backup_audio.len()).collect(),
                        }));
                    }
                    if !pending.backup_midi.is_empty() {
                        tasks.push(self.send(Action::RemoveClip {
                            track_name: track_name.clone(),
                            kind: Kind::MIDI,
                            clip_indices: (0..pending.backup_midi.len()).collect(),
                        }));
                    }
                    tasks.push(self.send(Action::AddClip {
                        clip_id: crate::state::generate_clip_id(),
                        name: pending.rendered_clip_rel,
                        track_name: track_name.clone(),
                        start: 0,
                        length: pending.rendered_length.max(1),
                        offset: 0,
                        input_channel: 0,
                        muted: false,
                        peaks_file: None,
                        kind: Kind::Audio,
                        fade_enabled: true,
                        fade_in_samples: 240,
                        fade_out_samples: 240,
                        source_name: None,
                        source_offset: None,
                        source_length: None,
                        preview_name: None,
                        pitch_correction_points: vec![],
                        pitch_correction_frame_likeness: None,
                        pitch_correction_inertia_ms: None,
                        pitch_correction_formant_compensation: None,
                        plugin_graph_json: None,
                    }));
                    tasks.push(self.send(Action::TrackSetFrozen {
                        track_name: track_name.clone(),
                        frozen: true,
                    }));
                    tasks.push(self.send(Action::EndHistoryGroup));
                    return Some(Task::batch(tasks));
                }
                Some(Task::none())
            }
            Action::TrackOfflineBounceProgress {
                track_name,
                progress,
                operation,
            } => {
                if self.export_in_progress && self.export_pending_bounces.contains(track_name) {
                    self.export_progress = 0.05 + progress * 0.15;
                    self.export_operation = operation
                        .clone()
                        .or_else(|| Some("Bouncing tracks...".to_string()));
                    return Some(Task::none());
                }
                self.freeze_in_progress = true;
                self.freeze_track_name = Some(track_name.clone());
                self.freeze_progress = *progress;
                let percent = (progress * 100.0).round().clamp(0.0, 100.0) as u32;
                self.state.blocking_write().message = if self.freeze_cancel_requested {
                    format!("Canceling freeze ({percent}%)...")
                } else if let Some(op) = operation {
                    format!("{} ({percent}%)", op)
                } else {
                    format!("Rendering freeze ({percent}%)")
                };
                Some(Task::none())
            }
            Action::TrackOfflineBounceCanceled { track_name } => {
                self.freeze_in_progress = false;
                self.freeze_track_name = None;
                self.freeze_progress = 0.0;
                self.freeze_cancel_requested = false;
                self.pending_track_freeze_bounce.remove(track_name);
                self.state.blocking_write().message =
                    format!("Freeze canceled for '{}'", track_name);
                Some(Task::none())
            }
            Action::TrackMeters {
                track_name,
                output_db,
            } => {
                if track_name == "hw:out" {
                    let mut state = self.state.blocking_write();
                    Self::smooth_meter_db_levels(&mut state.hw_out_meter_db, output_db);
                    return Some(Task::none());
                }
                let mut state = self.state.blocking_write();
                if let Some(track) = state.tracks.iter_mut().find(|t| t.name == *track_name) {
                    Self::smooth_meter_db_levels(&mut track.meter_out_db, output_db);
                }
                Some(Task::none())
            }
            Action::MeterSnapshot {
                hw_out_db,
                track_meters,
            } => {
                let mut state = self.state.blocking_write();
                let visible_tracks = visible_mixer_track_names(self, &state);
                let has_input_monitor = state.tracks.iter().any(|track| {
                    track.input_monitor.iter().any(|&m| m)
                        || track.midi_input_monitor.iter().any(|&m| m)
                });
                let allow_live_hw_out = (self.playing && !self.paused)
                    || self.live_session_playing
                    || has_input_monitor;
                if (!allow_live_hw_out || hw_out_db.is_empty()) && !state.hw_out_meter_db.is_empty()
                {
                    let silence = vec![-90.0; state.hw_out_meter_db.len()];
                    Self::smooth_meter_db_levels(&mut state.hw_out_meter_db, &silence);
                } else {
                    Self::smooth_meter_db_levels(&mut state.hw_out_meter_db, hw_out_db);
                }
                for track in &mut state.tracks {
                    if let Some(visible_tracks) = visible_tracks.as_ref()
                        && track.name != crate::consts::state_ids::METRONOME_TRACK_ID
                        && !visible_tracks.contains(track.name.as_str())
                    {
                        continue;
                    }
                    let allow_live_track_out = (self.playing && !self.paused)
                        || self.live_session_playing
                        || track.input_monitor.iter().any(|&m| m)
                        || track.midi_input_monitor.iter().any(|&m| m);
                    if !allow_live_track_out || track_meters.is_empty() {
                        let silence = vec![-90.0; track.meter_out_db.len()];
                        Self::smooth_meter_db_levels(&mut track.meter_out_db, &silence);
                    } else if let Some((_, output_db)) = track_meters
                        .iter()
                        .find(|(track_name, _)| track_name.as_str() == track.name.as_str())
                    {
                        Self::smooth_meter_db_levels(&mut track.meter_out_db, output_db);
                    } else if !track.meter_out_db.is_empty() {
                        let silence = vec![-90.0; track.meter_out_db.len()];
                        Self::smooth_meter_db_levels(&mut track.meter_out_db, &silence);
                    }
                }

                Some(Task::none())
            }
            _ => None,
        }
    }
}