1use std::path::Path;
7use serde::{Serialize, Deserialize};
8use anyhow::Result;
9
10use crate::state::{NavState, InstrumentType};
11use phosphor_core::transport::Transport;
12
13#[derive(Serialize, Deserialize)]
16pub struct SessionFile {
17 pub version: u32,
18 pub transport: SessionTransport,
19 pub tracks: Vec<SessionTrack>,
20}
21
22#[derive(Serialize, Deserialize)]
23pub struct SessionTransport {
24 pub tempo_bpm: f64,
25 pub loop_enabled: bool,
26 pub loop_start_bar: u32,
27 pub loop_end_bar: u32,
28 pub metronome: bool,
29}
30
31#[derive(Serialize, Deserialize)]
32pub struct SessionTrack {
33 pub name: String,
34 pub instrument_type: String,
35 pub synth_params: Vec<f32>,
36 pub muted: bool,
37 pub soloed: bool,
38 pub armed: bool,
39 pub volume: f32,
40 pub color_index: usize,
41 pub clips: Vec<SessionClip>,
42}
43
44#[derive(Serialize, Deserialize)]
45pub struct SessionClip {
46 pub start_tick: i64,
47 pub length_ticks: i64,
48 pub notes: Vec<SessionNote>,
49}
50
51#[derive(Serialize, Deserialize)]
52pub struct SessionNote {
53 pub note: u8,
54 pub velocity: u8,
55 pub start_frac: f64,
56 pub duration_frac: f64,
57}
58
59fn instrument_type_to_string(t: InstrumentType) -> String {
62 match t {
63 InstrumentType::Synth => "synth".into(),
64 InstrumentType::DrumRack => "drums".into(),
65 InstrumentType::DX7 => "dx7".into(),
66 InstrumentType::Jupiter8 => "jupiter8".into(),
67 InstrumentType::Odyssey => "odyssey".into(),
68 InstrumentType::Juno60 => "juno60".into(),
69 InstrumentType::Sampler => "sampler".into(),
70 }
71}
72
73fn string_to_instrument_type(s: &str) -> Option<InstrumentType> {
74 match s {
75 "synth" => Some(InstrumentType::Synth),
76 "drums" => Some(InstrumentType::DrumRack),
77 "dx7" => Some(InstrumentType::DX7),
78 "jupiter8" => Some(InstrumentType::Jupiter8),
79 "odyssey" => Some(InstrumentType::Odyssey),
80 "juno60" => Some(InstrumentType::Juno60),
81 "sampler" => Some(InstrumentType::Sampler),
82 _ => None,
83 }
84}
85
86pub fn save(path: &Path, nav: &NavState, transport: &Transport) -> Result<()> {
89 let session = extract_session(nav, transport);
90 let json = serde_json::to_string_pretty(&session)?;
91
92 if let Some(parent) = path.parent() {
94 if !parent.exists() {
95 std::fs::create_dir_all(parent)?;
96 }
97 }
98
99 let tmp = path.with_extension("phos.tmp");
101 std::fs::write(&tmp, &json)?;
102 std::fs::rename(&tmp, path)?;
103
104 tracing::debug!("session saved: {}", path.display());
105 Ok(())
106}
107
108fn extract_session(nav: &NavState, transport: &Transport) -> SessionFile {
109 let mut tracks = Vec::new();
110
111 for track in &nav.tracks {
112 if track.instrument_type.is_none() {
114 continue;
115 }
116
117 let clips: Vec<SessionClip> = track.clips.iter().map(|clip| {
118 SessionClip {
119 start_tick: clip.start_tick,
120 length_ticks: clip.length_ticks,
121 notes: clip.notes.iter().map(|n| SessionNote {
122 note: n.note,
123 velocity: n.velocity,
124 start_frac: n.start_frac,
125 duration_frac: n.duration_frac,
126 }).collect(),
127 }
128 }).collect();
129
130 tracks.push(SessionTrack {
131 name: track.name.clone(),
132 instrument_type: track.instrument_type
133 .map(instrument_type_to_string)
134 .unwrap_or_default(),
135 synth_params: track.synth_params.clone(),
136 muted: track.muted,
137 soloed: track.soloed,
138 armed: track.armed,
139 volume: track.volume,
140 color_index: track.color_index,
141 clips,
142 });
143 }
144
145 SessionFile {
146 version: 1,
147 transport: SessionTransport {
148 tempo_bpm: transport.tempo_bpm(),
149 loop_enabled: nav.loop_editor.enabled,
150 loop_start_bar: nav.loop_editor.start_bar,
151 loop_end_bar: nav.loop_editor.end_bar,
152 metronome: transport.is_metronome_on(),
153 },
154 tracks,
155 }
156}
157
158pub fn load(path: &Path) -> Result<SessionFile> {
161 let json = std::fs::read_to_string(path)?;
162 let session: SessionFile = serde_json::from_str(&json)?;
163 tracing::debug!("session loaded: {} (v{}, {} tracks)",
164 path.display(), session.version, session.tracks.len());
165 Ok(session)
166}
167
168pub fn parse_instrument_type(s: &str) -> Option<InstrumentType> {
170 string_to_instrument_type(s)
171}
172
173pub fn session_notes_to_snapshots(notes: &[SessionNote]) -> Vec<phosphor_core::clip::NoteSnapshot> {
175 notes.iter().map(|n| phosphor_core::clip::NoteSnapshot {
176 note: n.note,
177 velocity: n.velocity,
178 start_frac: n.start_frac,
179 duration_frac: n.duration_frac,
180 }).collect()
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn round_trip_serialize() {
189 let session = SessionFile {
190 version: 1,
191 transport: SessionTransport {
192 tempo_bpm: 120.0,
193 loop_enabled: true,
194 loop_start_bar: 1,
195 loop_end_bar: 5,
196 metronome: true,
197 },
198 tracks: vec![
199 SessionTrack {
200 name: "synth".into(),
201 instrument_type: "dx7".into(),
202 synth_params: vec![0.0, 0.5, 0.7],
203 muted: false,
204 soloed: false,
205 armed: true,
206 volume: 0.75,
207 color_index: 2,
208 clips: vec![
209 SessionClip {
210 start_tick: 0,
211 length_ticks: 3840,
212 notes: vec![
213 SessionNote { note: 60, velocity: 100, start_frac: 0.0, duration_frac: 0.25 },
214 SessionNote { note: 64, velocity: 80, start_frac: 0.25, duration_frac: 0.25 },
215 ],
216 },
217 ],
218 },
219 ],
220 };
221
222 let json = serde_json::to_string_pretty(&session).unwrap();
223 let loaded: SessionFile = serde_json::from_str(&json).unwrap();
224
225 assert_eq!(loaded.version, 1);
226 assert_eq!(loaded.transport.tempo_bpm, 120.0);
227 assert_eq!(loaded.transport.loop_enabled, true);
228 assert_eq!(loaded.tracks.len(), 1);
229 assert_eq!(loaded.tracks[0].name, "synth");
230 assert_eq!(loaded.tracks[0].instrument_type, "dx7");
231 assert_eq!(loaded.tracks[0].synth_params, vec![0.0, 0.5, 0.7]);
232 assert_eq!(loaded.tracks[0].clips.len(), 1);
233 assert_eq!(loaded.tracks[0].clips[0].notes.len(), 2);
234 assert_eq!(loaded.tracks[0].clips[0].notes[0].note, 60);
235 }
236
237 #[test]
238 fn instrument_type_round_trip() {
239 for inst in InstrumentType::ALL {
240 let s = instrument_type_to_string(*inst);
241 let back = string_to_instrument_type(&s);
242 assert_eq!(back, Some(*inst), "Failed round-trip for {s}");
243 }
244 }
245}