maolan 0.2.0

Rust DAW application for recording, editing, routing, automation, export, and plugin hosting
use super::*;
use tracing::error;

impl Maolan {
    pub(super) fn handle_transport_message(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::TransportPlay => {
                self.toolbar.update(&message);
                let was_playing = self.playing;
                self.playing = true;
                self.paused = false;
                self.last_playback_tick = Some(Instant::now());
                if self.record_armed {
                    self.start_recording_preview();
                }
                let mut tasks = vec![self.send(Action::SetClipPlaybackEnabled(true))];
                if !was_playing {
                    tasks.push(self.send(Action::Play));
                }
                Task::batch(tasks)
            }
            Message::TransportPause => {
                self.toolbar.update(&message);
                let was_playing = self.playing;
                self.playing = true;
                self.paused = true;
                self.last_playback_tick = None;
                self.stop_recording_preview();
                let mut tasks = vec![self.send(Action::SetClipPlaybackEnabled(false))];
                if !was_playing {
                    tasks.push(self.send(Action::Play));
                }
                Task::batch(tasks)
            }
            Message::TransportStop => {
                self.toolbar.update(&message);
                self.playing = false;
                self.paused = false;
                self.last_playback_tick = None;
                self.track_automation_runtime.clear();
                self.touch_automation_overrides.clear();
                self.touch_active_keys.clear();
                self.latch_automation_overrides.clear();
                self.stop_recording_preview();
                Task::batch(vec![
                    self.send(Action::SetClipPlaybackEnabled(true)),
                    self.send(Action::Stop),
                ])
            }
            Message::TransportPanic => {
                self.toolbar.update(&message);
                self.send(Action::Panic)
            }
            Message::JumpToStart => {
                self.transport_samples = 0.0;
                self.track_automation_runtime.clear();
                self.touch_automation_overrides.clear();
                self.touch_active_keys.clear();
                self.latch_automation_overrides.clear();
                self.send(Action::TransportPosition(0))
            }
            Message::JumpToEnd => {
                let end_sample = {
                    let state = self.state.blocking_read();
                    state
                        .tracks
                        .iter()
                        .flat_map(|track| {
                            let audio = track
                                .audio
                                .clips
                                .iter()
                                .map(|clip| clip.start.saturating_add(clip.length));
                            let midi = track
                                .midi
                                .clips
                                .iter()
                                .map(|clip| clip.start.saturating_add(clip.length));
                            audio.chain(midi)
                        })
                        .max()
                        .unwrap_or(0)
                };
                self.transport_samples = end_sample as f64;
                self.send(Action::TransportPosition(end_sample))
            }
            Message::PlaybackTick => {
                let mut now_sample = self.transport_samples.max(0.0) as usize;
                if self.playing
                    && !self.paused
                    && let Some(last) = self.last_playback_tick
                {
                    let now = Instant::now();
                    let delta_s = now.duration_since(last).as_secs_f64();
                    self.last_playback_tick = Some(now);
                    self.transport_samples += delta_s * self.playback_rate_hz;
                    now_sample = self.transport_samples.max(0.0) as usize;
                }
                let mut tasks = Vec::new();
                {
                    let state = self.state.blocking_read();
                    let (bpm, num, den) = Self::timing_at_sample(&state, now_sample);
                    let tempo_changed = self
                        .last_sent_tempo_bpm
                        .is_none_or(|prev| (prev - bpm as f64).abs() > 0.0001);
                    let ts_changed = self
                        .last_sent_time_signature
                        .is_none_or(|prev| prev != (num as u16, den as u16));
                    if tempo_changed {
                        self.tempo_input = format!("{:.2}", bpm);
                    }
                    if ts_changed {
                        self.time_signature_num_input = num.to_string();
                        self.time_signature_denom_input = den.to_string();
                    }
                    if self
                        .last_sent_tempo_bpm
                        .is_none_or(|prev| (prev - bpm as f64).abs() > 0.0001)
                    {
                        self.last_sent_tempo_bpm = Some(bpm as f64);
                        tasks.push(self.send(Action::SetTempo(bpm as f64)));
                    }
                    if self
                        .last_sent_time_signature
                        .is_none_or(|prev| prev != (num as u16, den as u16))
                    {
                        self.last_sent_time_signature = Some((num as u16, den as u16));
                        tasks.push(self.send(Action::SetTimeSignature {
                            numerator: num as u16,
                            denominator: den as u16,
                        }));
                    }
                }
                if self.playing && !self.paused {
                    let tracks = {
                        let state = self.state.blocking_read();
                        state
                            .tracks
                            .iter()
                            .map(|track| AutomationTrackView {
                                name: track.name.clone(),
                                automation_mode: track.automation_mode,
                                automation_lanes: track.automation_lanes.clone(),
                                frozen: track.frozen,
                            })
                            .collect::<Vec<_>>()
                    };
                    let actions = self.collect_track_automation_actions(now_sample, &tracks);
                    if !actions.is_empty() {
                        tasks.extend(actions.into_iter().map(|a| self.send(a)));
                    }
                }
                if !tasks.is_empty() {
                    return Task::batch(tasks);
                }
                Task::none()
            }
            Message::AutosaveSnapshotTick => {
                if !self.is_dirty()
                    || self.session_restore_in_progress
                    || self.pending_save_path.is_some()
                {
                    return Task::none();
                }
                let Some(autosave_root) = self.autosave_snapshot_root() else {
                    return Task::none();
                };
                let now = Instant::now();
                if self
                    .last_autosave_snapshot
                    .is_some_and(|last| now.duration_since(last) < AUTOSAVE_SNAPSHOT_INTERVAL)
                {
                    return Task::none();
                }
                let stamp = SystemTime::now()
                    .duration_since(UNIX_EPOCH)
                    .map(|d| d.as_secs())
                    .unwrap_or(0);
                let snapshot_dir = autosave_root.join("snapshots").join(format!("{stamp}"));
                match self.save(snapshot_dir.to_string_lossy().to_string()) {
                    Ok(()) => {
                        self.last_autosave_snapshot = Some(now);
                        let branch = self.session_branch.clone();
                        let mut snapshots = self
                            .session_dir
                            .as_ref()
                            .map(|path| Self::list_autosave_snapshots_for(path, &branch))
                            .unwrap_or_default();
                        if snapshots.len()
                            > crate::consts::gui_update_dispatch_transport::AUTOSAVE_KEEP_COUNT
                        {
                            snapshots.sort();
                            let remove_count = snapshots.len().saturating_sub(
                                crate::consts::gui_update_dispatch_transport::AUTOSAVE_KEEP_COUNT,
                            );
                            for stale in snapshots.into_iter().take(remove_count) {
                                if let Err(err) = fs::remove_dir_all(&stale) {
                                    error!(
                                        "Failed to remove stale autosave snapshot '{}': {err}",
                                        stale.display()
                                    );
                                }
                            }
                        }
                    }
                    Err(e) => {
                        self.state.blocking_write().message = format!("Autosave failed: {e}");
                    }
                }
                Task::none()
            }
            Message::SetLoopRange(range) => {
                let normalized = range.and_then(|(start, end)| {
                    if end > start {
                        Some((start, end))
                    } else {
                        None
                    }
                });
                self.loop_enabled = normalized.is_some();
                self.loop_range_samples = normalized;
                self.clip_snap_targets.clear();
                self.send(Action::SetLoopRange(normalized))
            }
            Message::SetPunchRange(range) => {
                let normalized = range.and_then(|(start, end)| {
                    if end > start {
                        Some((start, end))
                    } else {
                        None
                    }
                });
                self.punch_enabled = normalized.is_some();
                self.punch_range_samples = normalized;
                self.clip_snap_targets.clear();
                self.send(Action::SetPunchRange(normalized))
            }
            _ => Task::none(),
        }
    }
}