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
59pub fn instrument_key(t: InstrumentType) -> &'static str {
67 match t {
68 InstrumentType::Synth => "synth",
69 InstrumentType::DrumRack => "drums",
70 InstrumentType::DX7 => "dx7",
71 InstrumentType::Jupiter8 => "jupiter8",
72 InstrumentType::Odyssey => "odyssey",
73 InstrumentType::Juno60 => "juno60",
74 InstrumentType::Sampler => "sampler",
75 }
76}
77
78fn instrument_type_to_string(t: InstrumentType) -> String {
79 instrument_key(t).to_string()
80}
81
82fn string_to_instrument_type(s: &str) -> Option<InstrumentType> {
83 match s {
84 "synth" => Some(InstrumentType::Synth),
85 "drums" => Some(InstrumentType::DrumRack),
86 "dx7" => Some(InstrumentType::DX7),
87 "jupiter8" => Some(InstrumentType::Jupiter8),
88 "odyssey" => Some(InstrumentType::Odyssey),
89 "juno60" => Some(InstrumentType::Juno60),
90 "sampler" => Some(InstrumentType::Sampler),
91 _ => None,
92 }
93}
94
95pub fn save(path: &Path, nav: &NavState, transport: &Transport) -> Result<()> {
98 let session = extract_session(nav, transport);
99 let json = serde_json::to_string_pretty(&session)?;
100
101 if let Some(parent) = path.parent() {
103 if !parent.exists() {
104 std::fs::create_dir_all(parent)?;
105 }
106 }
107
108 let tmp = path.with_extension("phos.tmp");
110 std::fs::write(&tmp, &json)?;
111 std::fs::rename(&tmp, path)?;
112
113 tracing::debug!("session saved: {}", path.display());
114 Ok(())
115}
116
117fn extract_session(nav: &NavState, transport: &Transport) -> SessionFile {
118 let mut tracks = Vec::new();
119
120 for track in &nav.tracks {
121 if track.instrument_type.is_none() {
123 continue;
124 }
125
126 let clips: Vec<SessionClip> = track.clips.iter().map(|clip| {
127 SessionClip {
128 start_tick: clip.start_tick,
129 length_ticks: clip.length_ticks,
130 notes: clip.notes.iter().map(|n| SessionNote {
131 note: n.note,
132 velocity: n.velocity,
133 start_frac: n.start_frac,
134 duration_frac: n.duration_frac,
135 }).collect(),
136 }
137 }).collect();
138
139 tracks.push(SessionTrack {
140 name: track.name.clone(),
141 instrument_type: track.instrument_type
142 .map(instrument_type_to_string)
143 .unwrap_or_default(),
144 synth_params: track.synth_params.clone(),
145 muted: track.muted,
146 soloed: track.soloed,
147 armed: track.armed,
148 volume: track.volume,
149 color_index: track.color_index,
150 clips,
151 });
152 }
153
154 SessionFile {
155 version: 1,
156 transport: SessionTransport {
157 tempo_bpm: transport.tempo_bpm(),
158 loop_enabled: nav.loop_editor.enabled,
159 loop_start_bar: nav.loop_editor.start_bar,
160 loop_end_bar: nav.loop_editor.end_bar,
161 metronome: transport.is_metronome_on(),
162 },
163 tracks,
164 }
165}
166
167pub fn load(path: &Path) -> Result<SessionFile> {
170 let json = std::fs::read_to_string(path)?;
171 let session: SessionFile = serde_json::from_str(&json)?;
172 tracing::debug!("session loaded: {} (v{}, {} tracks)",
173 path.display(), session.version, session.tracks.len());
174 Ok(session)
175}
176
177pub fn parse_instrument_type(s: &str) -> Option<InstrumentType> {
179 string_to_instrument_type(s)
180}
181
182pub fn session_notes_to_snapshots(notes: &[SessionNote]) -> Vec<phosphor_core::clip::NoteSnapshot> {
184 notes.iter().map(|n| phosphor_core::clip::NoteSnapshot {
185 note: n.note,
186 velocity: n.velocity,
187 start_frac: n.start_frac,
188 duration_frac: n.duration_frac,
189 }).collect()
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn round_trip_serialize() {
198 let session = SessionFile {
199 version: 1,
200 transport: SessionTransport {
201 tempo_bpm: 120.0,
202 loop_enabled: true,
203 loop_start_bar: 1,
204 loop_end_bar: 5,
205 metronome: true,
206 },
207 tracks: vec![
208 SessionTrack {
209 name: "synth".into(),
210 instrument_type: "dx7".into(),
211 synth_params: vec![0.0, 0.5, 0.7],
212 muted: false,
213 soloed: false,
214 armed: true,
215 volume: 0.75,
216 color_index: 2,
217 clips: vec![
218 SessionClip {
219 start_tick: 0,
220 length_ticks: 3840,
221 notes: vec![
222 SessionNote { note: 60, velocity: 100, start_frac: 0.0, duration_frac: 0.25 },
223 SessionNote { note: 64, velocity: 80, start_frac: 0.25, duration_frac: 0.25 },
224 ],
225 },
226 ],
227 },
228 ],
229 };
230
231 let json = serde_json::to_string_pretty(&session).unwrap();
232 let loaded: SessionFile = serde_json::from_str(&json).unwrap();
233
234 assert_eq!(loaded.version, 1);
235 assert_eq!(loaded.transport.tempo_bpm, 120.0);
236 assert_eq!(loaded.transport.loop_enabled, true);
237 assert_eq!(loaded.tracks.len(), 1);
238 assert_eq!(loaded.tracks[0].name, "synth");
239 assert_eq!(loaded.tracks[0].instrument_type, "dx7");
240 assert_eq!(loaded.tracks[0].synth_params, vec![0.0, 0.5, 0.7]);
241 assert_eq!(loaded.tracks[0].clips.len(), 1);
242 assert_eq!(loaded.tracks[0].clips[0].notes.len(), 2);
243 assert_eq!(loaded.tracks[0].clips[0].notes[0].note, 60);
244 }
245
246 #[test]
247 fn instrument_type_round_trip() {
248 for inst in InstrumentType::ALL {
249 let s = instrument_type_to_string(*inst);
250 let back = string_to_instrument_type(&s);
251 assert_eq!(back, Some(*inst), "Failed round-trip for {s}");
252 }
253 }
254}