maolan 0.2.2

Rust DAW application for recording, editing, routing, automation, export, and plugin hosting
use serde::Deserialize;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default)]
pub struct CliConfig {
    pub default_audio_bit_depth: usize,
    pub default_export_sample_rate_hz: u32,
    pub osc_enabled: bool,
    pub default_output_device_id: Option<String>,
    pub default_input_device_id: Option<String>,
}

impl CliConfig {
    pub fn load() -> Result<Self, String> {
        let config_path = config_path()?;
        if !config_path.exists() {
            return Ok(Self::default());
        }
        let contents = std::fs::read_to_string(&config_path)
            .map_err(|err| format!("Failed to read {}: {err}", config_path.display()))?;
        toml::from_str(&contents)
            .map_err(|err| format!("Failed to parse {}: {err}", config_path.display()))
    }
}

fn config_path() -> Result<PathBuf, String> {
    let home = std::env::var("HOME")
        .or_else(|_| std::env::var("USERPROFILE"))
        .map_err(|_| "Could not determine home directory".to_string())?;
    Ok(PathBuf::from(home)
        .join(".config")
        .join("maolan")
        .join("config.toml"))
}

pub fn load_session_end_sample(session_dir: &Path, branch: &str) -> Result<usize, String> {
    let session = load_session_json(session_dir, branch)?;
    Ok(session
        .get("tracks")
        .and_then(serde_json::Value::as_array)
        .map(|tracks| tracks.iter().map(track_end_sample).max().unwrap_or(0))
        .unwrap_or(0))
}

fn load_session_json(session_dir: &Path, branch: &str) -> Result<serde_json::Value, String> {
    let session_path = session_dir.join(format!("{}.json", branch));
    let file = std::fs::File::open(&session_path)
        .map_err(|err| format!("Failed to open {}: {err}", session_path.display()))?;
    let reader = std::io::BufReader::new(file);
    serde_json::from_reader(reader)
        .map_err(|err| format!("Failed to parse {}: {err}", session_path.display()))
}

fn track_end_sample(track: &serde_json::Value) -> usize {
    ["audio", "midi"]
        .into_iter()
        .filter_map(|kind| track.get(kind))
        .filter_map(|section| section.get("clips").and_then(serde_json::Value::as_array))
        .flat_map(|clips| clips.iter())
        .map(clip_end_sample)
        .max()
        .unwrap_or(0)
}

fn clip_end_sample(clip: &serde_json::Value) -> usize {
    let own_end = clip
        .get("start")
        .and_then(serde_json::Value::as_u64)
        .unwrap_or(0)
        .saturating_add(
            clip.get("length")
                .and_then(serde_json::Value::as_u64)
                .unwrap_or(0),
        ) as usize;
    let child_end = clip
        .get("grouped_clips")
        .and_then(serde_json::Value::as_array)
        .map(|clips| clips.iter().map(clip_end_sample).max().unwrap_or(0))
        .unwrap_or(0);
    own_end.max(child_end)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn load_session_end_sample_uses_latest_audio_midi_and_grouped_clip_end() {
        let dir = std::env::temp_dir().join(format!(
            "maolan-cli-support-end-sample-test-{}",
            std::process::id()
        ));
        let _ = std::fs::remove_dir_all(&dir);
        std::fs::create_dir_all(&dir).expect("create temp dir");
        std::fs::write(
            dir.join("main.json"),
            serde_json::json!({
                "tracks": [{
                    "name": "Track 1",
                    "audio": {"ins": 2, "outs": 2, "clips": [{
                        "name": "Audio A",
                        "start": 100,
                        "length": 50
                    }, {
                        "name": "Grouped",
                        "start": 10,
                        "length": 5,
                        "grouped_clips": [{
                            "name": "Child",
                            "start": 400,
                            "length": 25
                        }]
                    }]},
                    "midi": {"ins": 1, "outs": 1, "clips": [{
                        "name": "Midi A",
                        "start": 250,
                        "length": 100
                    }]}
                }]
            })
            .to_string(),
        )
        .expect("write session");

        assert_eq!(
            load_session_end_sample(&dir, "main").expect("end sample"),
            425
        );

        let _ = std::fs::remove_dir_all(&dir);
    }
}