1use std::path::Path;
7use serde::{Serialize, Deserialize};
8use anyhow::Result;
9
10use crate::state::{NavState, InstrumentType};
11use phosphor_core::transport::Transport;
12
13pub const FORMAT_VERSION: u32 = 2;
26
27#[derive(Serialize, Deserialize)]
28pub struct SessionFile {
29 pub version: u32,
30 pub transport: SessionTransport,
31 pub tracks: Vec<SessionTrack>,
32}
33
34#[derive(Serialize, Deserialize)]
35pub struct SessionTransport {
36 pub tempo_bpm: f64,
37 pub loop_enabled: bool,
38 pub loop_start_bar: u32,
39 pub loop_end_bar: u32,
40 pub metronome: bool,
41}
42
43#[derive(Serialize, Deserialize)]
44pub struct SessionTrack {
45 pub name: String,
46 pub instrument_type: String,
47 pub synth_params: Vec<f32>,
48 #[serde(default)]
51 pub discrete: Vec<SessionSelector>,
52 pub muted: bool,
53 pub soloed: bool,
54 pub armed: bool,
55 pub volume: f32,
56 pub color_index: usize,
57 pub clips: Vec<SessionClip>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub sequencer: Option<crate::sequencer::SessionSequencer>,
65}
66
67#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
73pub struct SessionSelector {
74 pub param: usize,
76 pub index: usize,
78}
79
80#[derive(Serialize, Deserialize)]
81pub struct SessionClip {
82 pub start_tick: i64,
83 pub length_ticks: i64,
84 pub notes: Vec<SessionNote>,
85}
86
87#[derive(Serialize, Deserialize)]
88pub struct SessionNote {
89 pub note: u8,
90 pub velocity: u8,
91 pub start_frac: f64,
92 pub duration_frac: f64,
93}
94
95pub fn instrument_key(t: InstrumentType) -> &'static str {
103 match t {
104 InstrumentType::Synth => "synth",
105 InstrumentType::DrumRack => "drums",
106 InstrumentType::DX7 => "dx7",
107 InstrumentType::Jupiter8 => "jupiter8",
108 InstrumentType::Odyssey => "odyssey",
109 InstrumentType::Juno60 => "juno60",
110 InstrumentType::Rhodes => "rhodes",
111 InstrumentType::Sampler => "sampler",
112 InstrumentType::LittlePhatty => "phatty",
113 InstrumentType::Prophet6 => "prophet6",
114 InstrumentType::Sequencer => "sequencer",
115 }
116}
117
118fn instrument_type_to_string(t: InstrumentType) -> String {
119 instrument_key(t).to_string()
120}
121
122fn string_to_instrument_type(s: &str) -> Option<InstrumentType> {
123 match s {
124 "synth" => Some(InstrumentType::Synth),
125 "drums" => Some(InstrumentType::DrumRack),
126 "dx7" => Some(InstrumentType::DX7),
127 "jupiter8" => Some(InstrumentType::Jupiter8),
128 "odyssey" => Some(InstrumentType::Odyssey),
129 "juno60" => Some(InstrumentType::Juno60),
130 "rhodes" => Some(InstrumentType::Rhodes),
131 "sampler" => Some(InstrumentType::Sampler),
132 "phatty" => Some(InstrumentType::LittlePhatty),
133 "prophet6" => Some(InstrumentType::Prophet6),
134 "sequencer" => Some(InstrumentType::Sequencer),
135 _ => None,
136 }
137}
138
139pub fn save(path: &Path, nav: &NavState, transport: &Transport) -> Result<()> {
142 let session = extract_session(nav, transport);
143 let json = serde_json::to_string_pretty(&session)?;
144
145 if let Some(parent) = path.parent() {
147 if !parent.exists() {
148 std::fs::create_dir_all(parent)?;
149 }
150 }
151
152 let tmp = path.with_extension("phos.tmp");
154 std::fs::write(&tmp, &json)?;
155 std::fs::rename(&tmp, path)?;
156
157 tracing::debug!("session saved: {}", path.display());
158 Ok(())
159}
160
161fn extract_session(nav: &NavState, transport: &Transport) -> SessionFile {
162 let mut tracks = Vec::new();
163
164 for track in &nav.tracks {
165 if track.instrument_type.is_none() {
167 continue;
168 }
169
170 let clips: Vec<SessionClip> = track.clips.iter().map(|clip| {
171 SessionClip {
172 start_tick: clip.start_tick,
173 length_ticks: clip.length_ticks,
174 notes: clip.notes.iter().map(|n| SessionNote {
175 note: n.note,
176 velocity: n.velocity,
177 start_frac: n.start_frac,
178 duration_frac: n.duration_frac,
179 }).collect(),
180 }
181 }).collect();
182
183 tracks.push(SessionTrack {
184 name: track.name.clone(),
185 instrument_type: track.instrument_type
186 .map(instrument_type_to_string)
187 .unwrap_or_default(),
188 synth_params: track.synth_params.clone(),
189 discrete: track.instrument_type
190 .map(|i| selectors_of(i, &track.synth_params))
191 .unwrap_or_default(),
192 muted: track.muted,
193 soloed: track.soloed,
194 armed: track.armed,
195 volume: track.volume,
196 color_index: track.color_index,
197 clips,
198 sequencer: track.sequencer.as_ref().map(|state| {
199 crate::sequencer::SessionSequencer::from_state(
200 state,
201 track.instrument_type.unwrap_or(crate::sequencer::DEFAULT_CHILD),
202 &track.synth_params,
203 )
204 }),
205 });
206 }
207
208 SessionFile {
209 version: FORMAT_VERSION,
210 transport: SessionTransport {
211 tempo_bpm: transport.tempo_bpm(),
212 loop_enabled: nav.loop_editor.enabled,
213 loop_start_bar: nav.loop_editor.start_bar,
214 loop_end_bar: nav.loop_editor.end_bar,
215 metronome: transport.is_metronome_on(),
216 },
217 tracks,
218 }
219}
220
221pub fn load(path: &Path) -> Result<SessionFile> {
224 let json = std::fs::read_to_string(path)?;
225 let session: SessionFile = serde_json::from_str(&json)?;
226 tracing::debug!("session loaded: {} (v{}, {} tracks)",
227 path.display(), session.version, session.tracks.len());
228 Ok(session)
229}
230
231pub fn parse_instrument_type(s: &str) -> Option<InstrumentType> {
233 string_to_instrument_type(s)
234}
235
236#[must_use]
245pub fn selectors_of(instrument: InstrumentType, params: &[f32]) -> Vec<SessionSelector> {
246 (0..params.len())
247 .filter(|¶m| crate::discrete::is_discrete(instrument, param))
248 .filter_map(|param| {
249 crate::discrete::index_of(instrument, param, params[param])
250 .map(|index| SessionSelector { param, index })
251 })
252 .collect()
253}
254
255pub fn apply_selectors(
263 instrument: InstrumentType,
264 params: &mut [f32],
265 stored: &[SessionSelector],
266) -> Vec<(usize, usize, usize)> {
267 let mut clamped = Vec::new();
268 for selector in stored {
269 if selector.param >= params.len() {
270 continue;
271 }
272 let Some(positions) = crate::discrete::positions(instrument, selector.param) else {
273 continue;
274 };
275 let index = selector.index.min(positions.len().saturating_sub(1));
276 let Some(&knob) = positions.get(index) else { continue };
277 if index != selector.index {
278 clamped.push((selector.param, selector.index, index));
279 }
280 params[selector.param] = knob;
281 }
282 clamped
283}
284
285pub fn session_notes_to_snapshots(notes: &[SessionNote]) -> Vec<phosphor_core::clip::NoteSnapshot> {
287 notes.iter().map(|n| phosphor_core::clip::NoteSnapshot {
288 note: n.note,
289 velocity: n.velocity,
290 start_frac: n.start_frac,
291 duration_frac: n.duration_frac,
292 }).collect()
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn round_trip_serialize() {
301 let session = SessionFile {
302 version: FORMAT_VERSION,
303 transport: SessionTransport {
304 tempo_bpm: 120.0,
305 loop_enabled: true,
306 loop_start_bar: 1,
307 loop_end_bar: 5,
308 metronome: true,
309 },
310 tracks: vec![
311 SessionTrack {
312 name: "synth".into(),
313 instrument_type: "dx7".into(),
314 synth_params: vec![0.0, 0.5, 0.7],
315 discrete: vec![SessionSelector { param: 0, index: 3 }],
316 muted: false,
317 soloed: false,
318 armed: true,
319 volume: 0.75,
320 color_index: 2,
321 sequencer: None,
322 clips: vec![
323 SessionClip {
324 start_tick: 0,
325 length_ticks: 3840,
326 notes: vec![
327 SessionNote { note: 60, velocity: 100, start_frac: 0.0, duration_frac: 0.25 },
328 SessionNote { note: 64, velocity: 80, start_frac: 0.25, duration_frac: 0.25 },
329 ],
330 },
331 ],
332 },
333 ],
334 };
335
336 let json = serde_json::to_string_pretty(&session).unwrap();
337 let loaded: SessionFile = serde_json::from_str(&json).unwrap();
338
339 assert_eq!(loaded.version, FORMAT_VERSION);
340 assert_eq!(loaded.transport.tempo_bpm, 120.0);
341 assert!(loaded.transport.loop_enabled);
342 assert_eq!(loaded.tracks.len(), 1);
343 assert_eq!(loaded.tracks[0].name, "synth");
344 assert_eq!(loaded.tracks[0].instrument_type, "dx7");
345 assert_eq!(loaded.tracks[0].synth_params, vec![0.0, 0.5, 0.7]);
346 assert_eq!(
347 loaded.tracks[0].discrete,
348 vec![SessionSelector { param: 0, index: 3 }]
349 );
350 assert_eq!(loaded.tracks[0].clips.len(), 1);
351 assert_eq!(loaded.tracks[0].clips[0].notes.len(), 2);
352 assert_eq!(loaded.tracks[0].clips[0].notes[0].note, 60);
353 }
354
355 #[test]
356 fn instrument_type_round_trip() {
357 for inst in InstrumentType::ALL {
358 let s = instrument_type_to_string(*inst);
359 let back = string_to_instrument_type(&s);
360 assert_eq!(back, Some(*inst), "Failed round-trip for {s}");
361 }
362 }
363}