//! Playback state: preview, instrument, sample resolution. use super::*; impl BrowserState { // --- Sample resolution --- /// Resolve a sample hash to its filesystem path via the backend. pub fn resolve_sample_path(&self, hash: &str) -> Result { let ext = self.backend.sample_extension(hash).unwrap_or_default(); let path = self.backend.sample_path(hash, &ext) .map_err(|e| crate::error::PreviewError::InvalidHash(e.to_string()))?; if !path.exists() { return Err(crate::error::PreviewError::FileNotFound(path)); } Ok(path) } /// Resolve a sample hash and decode it to an interleaved stereo f32 buffer. pub fn resolve_and_decode(&self, hash: &str) -> Result { let path = self.resolve_sample_path(hash)?; crate::preview::decode_to_f32(&path) } // --- Preview --- /// Decode a sample by hash and start playback through the shared preview buffer. /// /// Short files (<=30s or unknown duration) are decoded fully on the GUI thread. /// Long files use streaming: a background thread decodes while playback starts /// after a 0.5s pre-fill, avoiding UI freezes. pub fn trigger_preview(&mut self, hash: &str) { let path = match self.resolve_sample_path(hash) { Ok(p) => p, Err(e) => { self.status = e.to_string(); self.previewing_hash = None; return; } }; let duration = crate::preview::estimate_duration(&path); let use_streaming = duration.is_some_and(|d| d > crate::preview::STREAMING_THRESHOLD_SECS); let file_name = path.file_name().unwrap_or_default().to_string_lossy().to_string(); let ok = if use_streaming { match crate::preview::start_streaming_decode(&path, &self.shared) { Ok(()) => { self.previewing_hash = Some(hash.to_string()); self.status = format!("Playing: {file_name}"); true } Err(e) => { self.status = format!("Decode error: {e}"); self.previewing_hash = None; false } } } else { match crate::preview::decode_to_f32(&path) { Ok(buf) => { let mut playback = self.shared.preview.lock(); playback.buffer = Some(buf); playback.position_frac = 0.0; playback.playing = true; playback.loop_enabled = self.loop_enabled; playback.streaming = false; playback.decoded_frames = 0; playback.total_frames_estimate = None; self.previewing_hash = Some(hash.to_string()); self.status = format!("Playing: {file_name}"); true } Err(e) => { self.status = format!("Decode error: {e}"); self.previewing_hash = None; false } } }; // Auto-load previewed sample into the instrument (unless locked) if ok && !self.instrument_locked { let hash_owned = hash.to_string(); self.load_chromatic_sample(&hash_owned); } } /// Stop playback and clear the previewing state. pub fn stop_preview(&mut self) { let mut playback = self.shared.preview.lock(); playback.playing = false; playback.position_frac = 0.0; self.previewing_hash = None; self.status.clear(); } /// Toggle preview: stop if playing, otherwise preview the focused sample. pub fn toggle_preview(&mut self) { if self.shared.preview.lock().playing { self.stop_preview(); } else if let Some(node) = self.selected_node() && let Some(hash) = &node.node.sample_hash { let hash = hash.clone(); self.trigger_preview(&hash); } } /// If autoplay is enabled and the focused node is a sample, preview it. pub fn autoplay_current(&mut self) { if !self.autoplay { return; } if let Some(node) = self.selected_node() && let Some(hash) = &node.node.sample_hash { let hash = hash.clone(); self.trigger_preview(&hash); } } /// Toggle loop mode and persist the setting. pub fn toggle_loop(&mut self) { self.loop_enabled = !self.loop_enabled; let _ = self.backend.set_config("preview_loop", if self.loop_enabled { "1" } else { "0" }); // Sync to the live playback state self.shared.preview.lock().loop_enabled = self.loop_enabled; } /// Toggle autoplay mode and persist the setting. pub fn toggle_autoplay(&mut self) { self.autoplay = !self.autoplay; let _ = self.backend.set_config("preview_autoplay", if self.autoplay { "1" } else { "0" }); } // --- Instrument --- /// Load a sample for chromatic instrument playback (pitch-shift across the keyboard). pub fn load_chromatic_sample(&mut self, hash: &str) { let buf = match self.resolve_and_decode(hash) { Ok(b) => b, Err(e) => { self.status = e.to_string(); return; } }; // Derive root note from analysis, default to C3 (48) let root_note = self .backend .get_analysis(hash) .ok() .flatten() .and_then(|a| a.musical_key) .and_then(|k| audiofiles_core::instrument::key_to_root_note(&k)) .unwrap_or(48); let zone = crate::instrument::LoadedZone { buffer: buf, root_note, low_note: 0, high_note: 127, vel_low: 0.0, vel_high: 1.0, }; let mut inst = self.shared.instrument.lock(); inst.config.mode = audiofiles_core::instrument::InstrumentMode::Chromatic; inst.zone_buffers.clear(); inst.zone_buffers.push(zone); inst.active = true; inst.sample_rate = self.sample_rate; // Kill all voices for voice in &mut inst.voices { voice.active = false; voice.envelope_phase = crate::instrument::EnvelopePhase::Idle; voice.envelope_level = 0.0; } drop(inst); self.instrument_root_note = root_note; } /// Toggle instrument mode on/off. pub fn toggle_instrument(&mut self) { let mut inst = self.shared.instrument.lock(); inst.active = !inst.active; self.instrument_visible = inst.active; self.show_midi_window = inst.active; } /// Add a sample as a new zone in multi-sample instrument mode. pub fn add_instrument_zone(&mut self, hash: &str, name: &str, low: u8, high: u8, root: u8) { let buf = match self.resolve_and_decode(hash) { Ok(b) => b, Err(e) => { self.status = e.to_string(); return; } }; let zone = crate::instrument::LoadedZone { buffer: buf, root_note: root, low_note: low, high_note: high, vel_low: 0.0, vel_high: 1.0, }; let mut inst = self.shared.instrument.lock(); inst.config.mode = audiofiles_core::instrument::InstrumentMode::MultiSample; inst.zone_buffers.push(zone); inst.active = true; inst.sample_rate = self.sample_rate; drop(inst); self.instrument_visible = true; self.show_midi_window = true; self.status = format!("Added zone: {name} ({}-{})", low, high); } /// Remove a zone by index and kill any voices using it. pub fn remove_instrument_zone(&mut self, index: usize) { let mut inst = self.shared.instrument.lock(); if index >= inst.zone_buffers.len() { return; } inst.zone_buffers.remove(index); // Kill voices using this zone or higher indices for voice in &mut inst.voices { if voice.active && voice.zone_index == index { voice.active = false; voice.envelope_phase = crate::instrument::EnvelopePhase::Idle; } else if voice.active && voice.zone_index > index { voice.zone_index -= 1; } } } }