Skip to main content

max / audiofiles

8.5 KB · 240 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, crate::error::PreviewError> {
10 let ext = self.backend.sample_extension(hash).unwrap_or_default();
11 let path = self.backend.sample_path(hash, &ext)
12 .map_err(|e| crate::error::PreviewError::InvalidHash(e.to_string()))?;
13 if !path.exists() {
14 return Err(crate::error::PreviewError::FileNotFound(path));
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, crate::error::PreviewError> {
21 let path = self.resolve_sample_path(hash)?;
22 crate::preview::decode_to_f32(&path)
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.to_string();
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 && let Some(hash) = &node.node.sample_hash {
104 let hash = hash.clone();
105 self.trigger_preview(&hash);
106 }
107 }
108
109 /// If autoplay is enabled and the focused node is a sample, preview it.
110 pub fn autoplay_current(&mut self) {
111 if !self.autoplay {
112 return;
113 }
114 if let Some(node) = self.selected_node()
115 && let Some(hash) = &node.node.sample_hash {
116 let hash = hash.clone();
117 self.trigger_preview(&hash);
118 }
119 }
120
121 /// Toggle loop mode and persist the setting.
122 pub fn toggle_loop(&mut self) {
123 self.loop_enabled = !self.loop_enabled;
124 let _ = self.backend.set_config("preview_loop", if self.loop_enabled { "1" } else { "0" });
125 // Sync to the live playback state
126 self.shared.preview.lock().loop_enabled = self.loop_enabled;
127 }
128
129 /// Toggle autoplay mode and persist the setting.
130 pub fn toggle_autoplay(&mut self) {
131 self.autoplay = !self.autoplay;
132 let _ = self.backend.set_config("preview_autoplay", if self.autoplay { "1" } else { "0" });
133 }
134
135 // --- Instrument ---
136
137 /// Load a sample for chromatic instrument playback (pitch-shift across the keyboard).
138 pub fn load_chromatic_sample(&mut self, hash: &str) {
139 let buf = match self.resolve_and_decode(hash) {
140 Ok(b) => b,
141 Err(e) => {
142 self.status = e.to_string();
143 return;
144 }
145 };
146
147 // Derive root note from analysis, default to C3 (48)
148 let root_note = self
149 .backend
150 .get_analysis(hash)
151 .ok()
152 .flatten()
153 .and_then(|a| a.musical_key)
154 .and_then(|k| audiofiles_core::instrument::key_to_root_note(&k))
155 .unwrap_or(48);
156
157 let zone = crate::instrument::LoadedZone {
158 buffer: buf,
159 root_note,
160 low_note: 0,
161 high_note: 127,
162 vel_low: 0.0,
163 vel_high: 1.0,
164 };
165
166 let mut inst = self.shared.instrument.lock();
167 inst.config.mode = audiofiles_core::instrument::InstrumentMode::Chromatic;
168 inst.zone_buffers.clear();
169 inst.zone_buffers.push(zone);
170 inst.active = true;
171 inst.sample_rate = self.sample_rate;
172 // Kill all voices
173 for voice in &mut inst.voices {
174 voice.active = false;
175 voice.envelope_phase = crate::instrument::EnvelopePhase::Idle;
176 voice.envelope_level = 0.0;
177 }
178 drop(inst);
179
180 self.instrument_root_note = root_note;
181 }
182
183 /// Toggle instrument mode on/off.
184 pub fn toggle_instrument(&mut self) {
185 let mut inst = self.shared.instrument.lock();
186 inst.active = !inst.active;
187 self.instrument_visible = inst.active;
188 self.show_midi_window = inst.active;
189 }
190
191 /// Add a sample as a new zone in multi-sample instrument mode.
192 pub fn add_instrument_zone(&mut self, hash: &str, name: &str, low: u8, high: u8, root: u8) {
193 let buf = match self.resolve_and_decode(hash) {
194 Ok(b) => b,
195 Err(e) => {
196 self.status = e.to_string();
197 return;
198 }
199 };
200
201 let zone = crate::instrument::LoadedZone {
202 buffer: buf,
203 root_note: root,
204 low_note: low,
205 high_note: high,
206 vel_low: 0.0,
207 vel_high: 1.0,
208 };
209
210 let mut inst = self.shared.instrument.lock();
211 inst.config.mode = audiofiles_core::instrument::InstrumentMode::MultiSample;
212 inst.zone_buffers.push(zone);
213 inst.active = true;
214 inst.sample_rate = self.sample_rate;
215 drop(inst);
216
217 self.instrument_visible = true;
218 self.show_midi_window = true;
219 self.status = format!("Added zone: {name} ({}-{})", low, high);
220 }
221
222 /// Remove a zone by index and kill any voices using it.
223 pub fn remove_instrument_zone(&mut self, index: usize) {
224 let mut inst = self.shared.instrument.lock();
225 if index >= inst.zone_buffers.len() {
226 return;
227 }
228 inst.zone_buffers.remove(index);
229 // Kill voices using this zone or higher indices
230 for voice in &mut inst.voices {
231 if voice.active && voice.zone_index == index {
232 voice.active = false;
233 voice.envelope_phase = crate::instrument::EnvelopePhase::Idle;
234 } else if voice.active && voice.zone_index > index {
235 voice.zone_index -= 1;
236 }
237 }
238 }
239 }
240