Skip to main content

max / audiofiles

8.5 KB · 242 lines History Blame Raw
1 //! Playback state: preview, instrument, sample resolution.
2
3 use super::*;
4
5 impl BrowserState {
6 // --- Sample resolution ---
7
8 /// Resolve a sample hash to its filesystem path via the backend.
9 pub fn resolve_sample_path(&self, hash: &str) -> Result<PathBuf, String> {
10 let ext = self.backend.sample_extension(hash).unwrap_or_default();
11 let path = self.backend.sample_path(hash, &ext)
12 .map_err(|e| format!("Invalid hash: {e}"))?;
13 if !path.exists() {
14 return Err(format!("File not found: {}", path.display()));
15 }
16 Ok(path)
17 }
18
19 /// Resolve a sample hash and decode it to an interleaved stereo f32 buffer.
20 pub fn resolve_and_decode(&self, hash: &str) -> Result<crate::preview::PreviewBuffer, String> {
21 let path = self.resolve_sample_path(hash)?;
22 crate::preview::decode_to_f32(&path).map_err(|e| format!("Decode error: {e}"))
23 }
24
25 // --- Preview ---
26
27 /// Decode a sample by hash and start playback through the shared preview buffer.
28 ///
29 /// Short files (<=30s or unknown duration) are decoded fully on the GUI thread.
30 /// Long files use streaming: a background thread decodes while playback starts
31 /// after a 0.5s pre-fill, avoiding UI freezes.
32 pub fn trigger_preview(&mut self, hash: &str) {
33 let path = match self.resolve_sample_path(hash) {
34 Ok(p) => p,
35 Err(e) => {
36 self.status = e;
37 self.previewing_hash = None;
38 return;
39 }
40 };
41
42 let duration = crate::preview::estimate_duration(&path);
43 let use_streaming = duration.is_some_and(|d| d > crate::preview::STREAMING_THRESHOLD_SECS);
44
45 let file_name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
46 let ok = if use_streaming {
47 match crate::preview::start_streaming_decode(&path, &self.shared) {
48 Ok(()) => {
49 self.previewing_hash = Some(hash.to_string());
50 self.status = format!("Playing: {file_name}");
51 true
52 }
53 Err(e) => {
54 self.status = format!("Decode error: {e}");
55 self.previewing_hash = None;
56 false
57 }
58 }
59 } else {
60 match crate::preview::decode_to_f32(&path) {
61 Ok(buf) => {
62 let mut playback = self.shared.preview.lock();
63 playback.buffer = Some(buf);
64 playback.position_frac = 0.0;
65 playback.playing = true;
66 playback.loop_enabled = self.loop_enabled;
67 playback.streaming = false;
68 playback.decoded_frames = 0;
69 playback.total_frames_estimate = None;
70 self.previewing_hash = Some(hash.to_string());
71 self.status = format!("Playing: {file_name}");
72 true
73 }
74 Err(e) => {
75 self.status = format!("Decode error: {e}");
76 self.previewing_hash = None;
77 false
78 }
79 }
80 };
81
82 // Auto-load previewed sample into the instrument (unless locked)
83 if ok && !self.instrument_locked {
84 let hash_owned = hash.to_string();
85 self.load_chromatic_sample(&hash_owned);
86 }
87 }
88
89 /// Stop playback and clear the previewing state.
90 pub fn stop_preview(&mut self) {
91 let mut playback = self.shared.preview.lock();
92 playback.playing = false;
93 playback.position_frac = 0.0;
94 self.previewing_hash = None;
95 self.status.clear();
96 }
97
98 /// Toggle preview: stop if playing, otherwise preview the focused sample.
99 pub fn toggle_preview(&mut self) {
100 if self.shared.preview.lock().playing {
101 self.stop_preview();
102 } else if let Some(node) = self.selected_node() {
103 if let Some(hash) = &node.node.sample_hash {
104 let hash = hash.clone();
105 self.trigger_preview(&hash);
106 }
107 }
108 }
109
110 /// If autoplay is enabled and the focused node is a sample, preview it.
111 pub fn autoplay_current(&mut self) {
112 if !self.autoplay {
113 return;
114 }
115 if let Some(node) = self.selected_node() {
116 if let Some(hash) = &node.node.sample_hash {
117 let hash = hash.clone();
118 self.trigger_preview(&hash);
119 }
120 }
121 }
122
123 /// Toggle loop mode and persist the setting.
124 pub fn toggle_loop(&mut self) {
125 self.loop_enabled = !self.loop_enabled;
126 let _ = self.backend.set_config("preview_loop", if self.loop_enabled { "1" } else { "0" });
127 // Sync to the live playback state
128 self.shared.preview.lock().loop_enabled = self.loop_enabled;
129 }
130
131 /// Toggle autoplay mode and persist the setting.
132 pub fn toggle_autoplay(&mut self) {
133 self.autoplay = !self.autoplay;
134 let _ = self.backend.set_config("preview_autoplay", if self.autoplay { "1" } else { "0" });
135 }
136
137 // --- Instrument ---
138
139 /// Load a sample for chromatic instrument playback (pitch-shift across the keyboard).
140 pub fn load_chromatic_sample(&mut self, hash: &str) {
141 let buf = match self.resolve_and_decode(hash) {
142 Ok(b) => b,
143 Err(e) => {
144 self.status = e;
145 return;
146 }
147 };
148
149 // Derive root note from analysis, default to C3 (48)
150 let root_note = self
151 .backend
152 .get_analysis(hash)
153 .ok()
154 .flatten()
155 .and_then(|a| a.musical_key)
156 .and_then(|k| audiofiles_core::instrument::key_to_root_note(&k))
157 .unwrap_or(48);
158
159 let zone = crate::instrument::LoadedZone {
160 buffer: buf,
161 root_note,
162 low_note: 0,
163 high_note: 127,
164 vel_low: 0.0,
165 vel_high: 1.0,
166 };
167
168 let mut inst = self.shared.instrument.lock();
169 inst.config.mode = audiofiles_core::instrument::InstrumentMode::Chromatic;
170 inst.zone_buffers.clear();
171 inst.zone_buffers.push(zone);
172 inst.active = true;
173 inst.sample_rate = self.sample_rate;
174 // Kill all voices
175 for voice in &mut inst.voices {
176 voice.active = false;
177 voice.envelope_phase = crate::instrument::EnvelopePhase::Idle;
178 voice.envelope_level = 0.0;
179 }
180 drop(inst);
181
182 self.instrument_root_note = root_note;
183 }
184
185 /// Toggle instrument mode on/off.
186 pub fn toggle_instrument(&mut self) {
187 let mut inst = self.shared.instrument.lock();
188 inst.active = !inst.active;
189 self.instrument_visible = inst.active;
190 self.show_midi_window = inst.active;
191 }
192
193 /// Add a sample as a new zone in multi-sample instrument mode.
194 pub fn add_instrument_zone(&mut self, hash: &str, name: &str, low: u8, high: u8, root: u8) {
195 let buf = match self.resolve_and_decode(hash) {
196 Ok(b) => b,
197 Err(e) => {
198 self.status = e;
199 return;
200 }
201 };
202
203 let zone = crate::instrument::LoadedZone {
204 buffer: buf,
205 root_note: root,
206 low_note: low,
207 high_note: high,
208 vel_low: 0.0,
209 vel_high: 1.0,
210 };
211
212 let mut inst = self.shared.instrument.lock();
213 inst.config.mode = audiofiles_core::instrument::InstrumentMode::MultiSample;
214 inst.zone_buffers.push(zone);
215 inst.active = true;
216 inst.sample_rate = self.sample_rate;
217 drop(inst);
218
219 self.instrument_visible = true;
220 self.show_midi_window = true;
221 self.status = format!("Added zone: {name} ({}-{})", low, high);
222 }
223
224 /// Remove a zone by index and kill any voices using it.
225 pub fn remove_instrument_zone(&mut self, index: usize) {
226 let mut inst = self.shared.instrument.lock();
227 if index >= inst.zone_buffers.len() {
228 return;
229 }
230 inst.zone_buffers.remove(index);
231 // Kill voices using this zone or higher indices
232 for voice in &mut inst.voices {
233 if voice.active && voice.zone_index == index {
234 voice.active = false;
235 voice.envelope_phase = crate::instrument::EnvelopePhase::Idle;
236 } else if voice.active && voice.zone_index > index {
237 voice.zone_index -= 1;
238 }
239 }
240 }
241 }
242