704 lines
25 KiB
Rust
704 lines
25 KiB
Rust
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
|
|
use iced_wgpu::core::{Element, Theme};
|
|
use iced_widget::scrollable::AbsoluteOffset;
|
|
|
|
use crate::analyzer::FrameData;
|
|
use crate::analyzer_worker::AnalyzerWorker;
|
|
use crate::engine::{AudioEngine, EngineEvent};
|
|
use crate::library::{self, Track};
|
|
use crate::library_worker::{LibraryUpdate, LibraryWorker};
|
|
|
|
use super::player;
|
|
|
|
/// owns the library, audio engine, background workers, and every UI-toggleable setting.
|
|
pub struct App {
|
|
pub library: Library,
|
|
pub selected_track: Option<usize>,
|
|
pub playing: bool,
|
|
pub immersive: bool,
|
|
pub engine: Option<AudioEngine>,
|
|
pub worker: AnalyzerWorker,
|
|
pub library_worker: LibraryWorker,
|
|
pub frame_data: Arc<Vec<FrameData>>,
|
|
pub current_palette: Option<Arc<Vec<[f32; 3]>>>,
|
|
pub settings: Settings,
|
|
pub show_settings: bool,
|
|
|
|
/// shell-side picker request flag drained by the iOS host once per tick.
|
|
pub pending_pick: u8,
|
|
|
|
/// monotonic id stamped onto every decode request, matched against returning results.
|
|
pub current_decode_id: u64,
|
|
pub next_decode_id: u64,
|
|
|
|
pub track_loading: bool,
|
|
|
|
/// running count and total of the iOS library import progress.
|
|
pub library_progress: Option<(u32, u32)>,
|
|
|
|
/// modal status message shown over the UI while waiting on iOS file-coordinator caching.
|
|
pub coordinating_message: Option<String>,
|
|
|
|
/// running count of decode failures since the last successful track load.
|
|
pub consecutive_failures: usize,
|
|
|
|
/// settings panel scroll offset mirrored from the on_scroll callback.
|
|
pub settings_scroll: AbsoluteOffset,
|
|
|
|
/// pending sidebar snap to the currently playing track row.
|
|
pub restore_sidebar_scroll: bool,
|
|
|
|
/// pending settings scroll restore against the live scrollable widget.
|
|
pub restore_settings_scroll: bool,
|
|
|
|
/// show_settings copy bridging the middle-tap collapse cycle.
|
|
pub saved_show_settings: bool,
|
|
}
|
|
|
|
/// every visualizer toggle, slider value, and DSP parameter the settings panel exposes.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct Settings {
|
|
pub glass: bool,
|
|
pub entropy_on: bool,
|
|
pub album_colors: bool,
|
|
pub mirrored: bool,
|
|
pub inverted: bool,
|
|
pub entropy_strength: f32,
|
|
pub hue: f32,
|
|
pub contrast: f32,
|
|
pub brightness: f32,
|
|
pub num_bins: u32,
|
|
pub fft: u32,
|
|
pub hop: u32,
|
|
pub granularity: i32,
|
|
pub detail: i32,
|
|
pub strength: f32,
|
|
|
|
/// fraction of FFT work routed to the GPU pipeline, blended against the CPU result.
|
|
pub gpu_blend: f32,
|
|
}
|
|
|
|
impl Default for Settings {
|
|
fn default() -> Self {
|
|
Self {
|
|
glass: true,
|
|
entropy_on: false,
|
|
album_colors: false,
|
|
mirrored: true,
|
|
inverted: false,
|
|
entropy_strength: 0.0,
|
|
hue: 0.9,
|
|
contrast: 1.0,
|
|
brightness: 1.0,
|
|
num_bins: 26,
|
|
fft: 16384,
|
|
hop: 4096,
|
|
granularity: 33,
|
|
detail: 50,
|
|
strength: 0.0,
|
|
gpu_blend: 0.7,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// loaded track list paired with the folder path of origin.
|
|
#[derive(Default)]
|
|
pub struct Library {
|
|
pub folder: Option<PathBuf>,
|
|
pub tracks: Vec<Track>,
|
|
}
|
|
|
|
/// every UI event the player widget tree can emit.
|
|
#[derive(Debug, Clone)]
|
|
pub enum Message {
|
|
OpenFolder,
|
|
OpenFile,
|
|
SelectTrack(usize),
|
|
TogglePlayPause,
|
|
Next,
|
|
Prev,
|
|
|
|
Seek(f32),
|
|
ToggleImmersive,
|
|
ToggleChrome,
|
|
ToggleSettings,
|
|
NoOp,
|
|
SettingsScrolled(AbsoluteOffset),
|
|
SetGlass(bool),
|
|
SetEntropy(bool),
|
|
SetAlbumColors(bool),
|
|
SetMirrored(bool),
|
|
SetInverted(bool),
|
|
SetEntropyStrength(f32),
|
|
SetHue(f32),
|
|
SetContrast(f32),
|
|
SetBrightness(f32),
|
|
SetNumBins(u32),
|
|
SetFft(u32),
|
|
SetHop(u32),
|
|
SetGranularity(i32),
|
|
SetDetail(i32),
|
|
SetStrength(f32),
|
|
SetGpuBlend(f32),
|
|
PickedFolder(PathBuf),
|
|
PickedFile(PathBuf),
|
|
PickedFiles(Vec<PathBuf>),
|
|
}
|
|
|
|
impl App {
|
|
/// builds the analyzer worker, library worker, audio engine, and seeds defaults.
|
|
pub fn new(device: wgpu::Device, queue: wgpu::Queue) -> Self {
|
|
let settings = Settings::default();
|
|
let worker = AnalyzerWorker::spawn(device, queue);
|
|
worker.set_num_bins(settings.num_bins as usize);
|
|
worker.set_dsp_params(settings.fft as usize, settings.hop as usize);
|
|
worker.set_smoothing(settings.granularity, settings.detail, settings.strength);
|
|
worker.set_gpu_blend(settings.gpu_blend);
|
|
let library_worker = LibraryWorker::spawn();
|
|
Self {
|
|
library: Library::default(),
|
|
selected_track: None,
|
|
playing: false,
|
|
immersive: false,
|
|
engine: AudioEngine::new()
|
|
.map_err(|e| eprintln!("yr_crystals: audio engine unavailable: {e}"))
|
|
.ok(),
|
|
worker,
|
|
library_worker,
|
|
frame_data: Arc::new(Vec::new()),
|
|
current_palette: None,
|
|
settings,
|
|
show_settings: false,
|
|
pending_pick: 0,
|
|
current_decode_id: 0,
|
|
next_decode_id: 0,
|
|
track_loading: false,
|
|
library_progress: None,
|
|
coordinating_message: None,
|
|
consecutive_failures: 0,
|
|
settings_scroll: AbsoluteOffset::default(),
|
|
restore_sidebar_scroll: false,
|
|
restore_settings_scroll: false,
|
|
saved_show_settings: false,
|
|
}
|
|
}
|
|
|
|
/// returns the picker flag and clears the slot in one step.
|
|
pub fn take_pending_pick(&mut self) -> u8 {
|
|
let p = self.pending_pick;
|
|
self.pending_pick = 0;
|
|
#[cfg(all(target_os = "ios", debug_assertions))]
|
|
if p != 0 {
|
|
eprintln!("[Rust.dbg] take_pending_pick -> {p}");
|
|
}
|
|
p
|
|
}
|
|
|
|
/// checks whether a logical-coords point falls inside the scrollable sidebar region.
|
|
pub fn point_in_sidebar(&self, x: f32, y: f32, viewport_height: f32) -> bool {
|
|
if self.immersive {
|
|
return false;
|
|
}
|
|
x >= 0.0
|
|
&& x < player::SIDEBAR_W
|
|
&& y >= player::TOP_BAR_H
|
|
&& y < viewport_height - player::TRANSPORT_H
|
|
}
|
|
|
|
/// checks whether a logical-coords point falls inside the right-aligned settings overlay panel.
|
|
pub fn point_in_settings(&self, x: f32, _y: f32, viewport_width: f32) -> bool {
|
|
self.show_settings && x >= viewport_width - player::SETTINGS_W && x <= viewport_width
|
|
}
|
|
|
|
/// scans a folder, replaces the library, queues art, and starts decoding the first track.
|
|
fn apply_picked_folder(&mut self, folder: PathBuf) {
|
|
#[cfg(all(target_os = "ios", debug_assertions))]
|
|
eprintln!("[Rust.dbg] apply_picked_folder enter path={}", folder.display());
|
|
let tracks = library::scan_folder(&folder);
|
|
#[cfg(all(target_os = "ios", debug_assertions))]
|
|
{
|
|
eprintln!("[Rust.dbg] apply_picked_folder scan -> {} tracks", tracks.len());
|
|
for (i, t) in tracks.iter().take(5).enumerate() {
|
|
eprintln!("[Rust.dbg] track[{i}] title={:?} path={}", t.title, t.path.display());
|
|
}
|
|
}
|
|
self.library.folder = Some(folder);
|
|
self.library.tracks = tracks;
|
|
self.queue_art_for_all();
|
|
self.selected_track = self.library.tracks.first().map(|_| 0);
|
|
self.playing = self.selected_track.is_some();
|
|
if let Some(idx) = self.selected_track {
|
|
self.load_index(idx);
|
|
}
|
|
}
|
|
|
|
/// loads a single audio file as a one-track library and starts playback.
|
|
fn apply_picked_file(&mut self, path: PathBuf) {
|
|
match library::read_track_meta(&path) {
|
|
Ok(track) => {
|
|
self.library.folder = path.parent().map(|p| p.to_path_buf());
|
|
self.library.tracks = vec![track];
|
|
self.queue_art_for_all();
|
|
self.selected_track = Some(0);
|
|
self.playing = true;
|
|
self.load_index(0);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("yr_crystals: read_track_meta failed: {}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// builds an ad-hoc library from a multi-file pick and queues meta and art lookups.
|
|
fn apply_picked_files(&mut self, paths: Vec<PathBuf>) {
|
|
|
|
let mut tracks = Vec::with_capacity(paths.len());
|
|
let mut folder = None;
|
|
for path in &paths {
|
|
if folder.is_none() {
|
|
folder = path.parent().map(|p| p.to_path_buf());
|
|
}
|
|
let title = path
|
|
.file_stem()
|
|
.and_then(|s| s.to_str())
|
|
.unwrap_or("Untitled")
|
|
.to_string();
|
|
tracks.push(library::Track {
|
|
path: path.clone(),
|
|
title,
|
|
artist: None,
|
|
album: None,
|
|
track_number: None,
|
|
art: None,
|
|
palette: None,
|
|
exporting: false,
|
|
});
|
|
}
|
|
self.library.folder = folder;
|
|
self.library.tracks = tracks;
|
|
self.queue_meta_for_all();
|
|
self.queue_art_for_all();
|
|
self.selected_track = self.library.tracks.first().map(|_| 0);
|
|
self.playing = self.selected_track.is_some();
|
|
if let Some(idx) = self.selected_track {
|
|
self.load_index(idx);
|
|
}
|
|
}
|
|
|
|
/// seeds the sidebar with placeholder rows for tracks pending export from the iOS host.
|
|
pub fn set_pending_titles(&mut self, entries: Vec<(String, Option<u32>)>) {
|
|
let mut tracks: Vec<library::Track> = entries
|
|
.into_iter()
|
|
.map(|(title, track_number)| library::Track {
|
|
path: PathBuf::new(),
|
|
title,
|
|
artist: None,
|
|
album: None,
|
|
track_number,
|
|
art: None,
|
|
palette: None,
|
|
exporting: true,
|
|
})
|
|
.collect();
|
|
library::sort_tracks(&mut tracks);
|
|
self.library.folder = None;
|
|
self.library.tracks = tracks;
|
|
self.selected_track = self.library.tracks.first().map(|_| 0);
|
|
self.playing = self.selected_track.is_some();
|
|
|
|
self.track_loading = self.selected_track.is_some();
|
|
}
|
|
|
|
/// resolves a placeholder track to a real file path once iOS export finishes.
|
|
pub fn set_track_path(&mut self, idx: usize, path: PathBuf) {
|
|
let Some(track) = self.library.tracks.get_mut(idx) else { return };
|
|
track.path = path.clone();
|
|
track.exporting = false;
|
|
self.library_worker.request_meta(path.clone());
|
|
self.library_worker.request_art(path);
|
|
if self.selected_track == Some(idx) {
|
|
self.load_index(idx);
|
|
}
|
|
}
|
|
|
|
/// hands raw artwork bytes to the art worker.
|
|
pub fn set_track_art_bytes(&mut self, idx: usize, bytes: Vec<u8>) {
|
|
if idx >= self.library.tracks.len() || bytes.is_empty() {
|
|
return;
|
|
}
|
|
self.library_worker.request_art_bytes(idx, bytes);
|
|
}
|
|
|
|
/// queues artwork extraction across the whole library on the art worker.
|
|
fn queue_art_for_all(&self) {
|
|
for t in &self.library.tracks {
|
|
self.library_worker.request_art(t.path.clone());
|
|
}
|
|
}
|
|
|
|
/// queues tag reads across the whole library on the meta worker.
|
|
fn queue_meta_for_all(&self) {
|
|
for t in &self.library.tracks {
|
|
self.library_worker.request_meta(t.path.clone());
|
|
}
|
|
}
|
|
|
|
/// reports the engine playhead as a normalised 0..=1 fraction of the loaded track.
|
|
pub fn position(&self) -> f32 {
|
|
self.engine.as_ref().map(|e| e.position()).unwrap_or(0.0)
|
|
}
|
|
|
|
/// recomputes the active album palette from the currently selected track.
|
|
fn refresh_palette(&mut self) {
|
|
self.current_palette = self
|
|
.selected_track
|
|
.and_then(|i| self.library.tracks.get(i))
|
|
.and_then(|t| t.palette.clone());
|
|
}
|
|
|
|
/// drains worker updates, advances tracks, and refreshes analyzer frames each frame.
|
|
pub fn tick(&mut self) {
|
|
for upd in self.library_worker.drain_updates() {
|
|
match upd {
|
|
LibraryUpdate::Meta {
|
|
path,
|
|
title,
|
|
artist,
|
|
album,
|
|
track_number,
|
|
} => {
|
|
if let Some(t) = self.library.tracks.iter_mut().find(|t| t.path == path) {
|
|
|
|
if let Some(title) = title {
|
|
t.title = title;
|
|
}
|
|
if artist.is_some() {
|
|
t.artist = artist;
|
|
}
|
|
if album.is_some() {
|
|
t.album = album;
|
|
}
|
|
if let Some(tn) = track_number {
|
|
t.track_number = Some(tn);
|
|
}
|
|
}
|
|
}
|
|
LibraryUpdate::Art {
|
|
path,
|
|
art,
|
|
palette,
|
|
} => {
|
|
let match_idx = self
|
|
.library
|
|
.tracks
|
|
.iter()
|
|
.position(|t| t.path == path);
|
|
if let Some(idx) = match_idx {
|
|
|
|
if art.is_some() {
|
|
self.library.tracks[idx].art = art;
|
|
}
|
|
if palette.is_some() {
|
|
self.library.tracks[idx].palette = palette;
|
|
}
|
|
if self.selected_track == Some(idx) {
|
|
self.refresh_palette();
|
|
}
|
|
}
|
|
}
|
|
LibraryUpdate::ArtForIdx {
|
|
idx,
|
|
art,
|
|
palette,
|
|
} => {
|
|
if let Some(t) = self.library.tracks.get_mut(idx) {
|
|
t.art = art;
|
|
t.palette = palette;
|
|
if self.selected_track == Some(idx) {
|
|
self.refresh_palette();
|
|
}
|
|
}
|
|
}
|
|
LibraryUpdate::Decoded {
|
|
request_id,
|
|
path: _,
|
|
result,
|
|
} => {
|
|
if request_id != self.current_decode_id {
|
|
continue;
|
|
}
|
|
self.track_loading = false;
|
|
match result {
|
|
Ok(td) => {
|
|
if td.is_valid() {
|
|
self.consecutive_failures = 0;
|
|
}
|
|
if let Some(eng) = &self.engine {
|
|
eng.load(td.clone(), self.current_decode_id);
|
|
if self.playing {
|
|
eng.play();
|
|
} else {
|
|
eng.pause();
|
|
}
|
|
}
|
|
self.worker.set_track(td);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("yr_crystals: decode failed: {e}");
|
|
self.note_failure_and_skip();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.handle_engine_events();
|
|
|
|
self.worker.publish_playhead(self.position());
|
|
self.frame_data = self.worker.latest_frames();
|
|
}
|
|
|
|
/// drains engine end-of-track notifications and advances when the id matches the current load.
|
|
fn handle_engine_events(&mut self) {
|
|
let Some(eng) = self.engine.as_ref() else { return };
|
|
for ev in eng.drain_events() {
|
|
match ev {
|
|
EngineEvent::TrackEnded { id } => {
|
|
if id == self.current_decode_id && self.playing && !self.track_loading {
|
|
self.advance_to_next();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// wraps the playhead onto the next track in the current sidebar order.
|
|
fn advance_to_next(&mut self) {
|
|
let Some(idx) = self.selected_track else { return };
|
|
if self.library.tracks.is_empty() { return; }
|
|
let next = (idx + 1) % self.library.tracks.len();
|
|
self.selected_track = Some(next);
|
|
self.load_index(next);
|
|
}
|
|
|
|
/// counts a decode failure and skips past the failed track, halting once the whole library has cycled.
|
|
fn note_failure_and_skip(&mut self) {
|
|
self.consecutive_failures = self.consecutive_failures.saturating_add(1);
|
|
if self.library.tracks.len() <= 1 || self.consecutive_failures >= self.library.tracks.len() {
|
|
self.playing = false;
|
|
if let Some(eng) = &self.engine { eng.pause(); }
|
|
return;
|
|
}
|
|
self.advance_to_next();
|
|
}
|
|
|
|
/// re-sorts the library on arrival of track-number tags and preserves the current selection.
|
|
#[allow(dead_code)]
|
|
fn resort_library(&mut self) {
|
|
let selected_path = self
|
|
.selected_track
|
|
.and_then(|i| self.library.tracks.get(i))
|
|
.map(|t| t.path.clone());
|
|
library::sort_tracks(&mut self.library.tracks);
|
|
if let Some(p) = selected_path {
|
|
self.selected_track = self.library.tracks.iter().position(|t| t.path == p);
|
|
}
|
|
}
|
|
|
|
/// dispatches a UI message into the matching state mutation and worker call.
|
|
pub fn update(&mut self, msg: Message) {
|
|
match msg {
|
|
Message::OpenFolder => {
|
|
#[cfg(all(not(target_os = "ios"), not(target_os = "android")))]
|
|
{
|
|
if let Some(folder) = rfd::FileDialog::new().pick_folder() {
|
|
self.apply_picked_folder(folder);
|
|
}
|
|
}
|
|
#[cfg(any(target_os = "ios", target_os = "android"))]
|
|
{
|
|
#[cfg(all(target_os = "ios", debug_assertions))]
|
|
eprintln!("[Rust.dbg] Message::OpenFolder -> pending_pick=1");
|
|
self.pending_pick = 1;
|
|
}
|
|
}
|
|
Message::OpenFile => {
|
|
#[cfg(all(not(target_os = "ios"), not(target_os = "android")))]
|
|
{
|
|
let file = rfd::FileDialog::new()
|
|
.add_filter(
|
|
"Audio",
|
|
&[
|
|
"mp3", "m4a", "m4b", "mp4", "flac", "wav", "ogg", "oga",
|
|
"opus", "aac", "aiff",
|
|
],
|
|
)
|
|
.pick_file();
|
|
if let Some(path) = file {
|
|
self.apply_picked_file(path);
|
|
}
|
|
}
|
|
#[cfg(any(target_os = "ios", target_os = "android"))]
|
|
{
|
|
#[cfg(all(target_os = "ios", debug_assertions))]
|
|
eprintln!("[Rust.dbg] Message::OpenFile -> pending_pick=2");
|
|
self.pending_pick = 2;
|
|
}
|
|
}
|
|
Message::PickedFolder(folder) => self.apply_picked_folder(folder),
|
|
Message::PickedFile(path) => self.apply_picked_file(path),
|
|
Message::PickedFiles(paths) => self.apply_picked_files(paths),
|
|
Message::SelectTrack(idx) => {
|
|
if idx < self.library.tracks.len() {
|
|
self.selected_track = Some(idx);
|
|
self.playing = true;
|
|
self.consecutive_failures = 0;
|
|
self.load_index(idx);
|
|
}
|
|
}
|
|
Message::TogglePlayPause => {
|
|
if self.selected_track.is_some() {
|
|
self.playing = !self.playing;
|
|
if let Some(eng) = &self.engine {
|
|
if self.playing { eng.play(); } else { eng.pause(); }
|
|
}
|
|
}
|
|
}
|
|
Message::Next => {
|
|
if let Some(i) = self.selected_track {
|
|
if i + 1 < self.library.tracks.len() {
|
|
let next = i + 1;
|
|
self.selected_track = Some(next);
|
|
self.load_index(next);
|
|
}
|
|
}
|
|
}
|
|
Message::Prev => {
|
|
if let Some(i) = self.selected_track {
|
|
if i > 0 {
|
|
let prev = i - 1;
|
|
self.selected_track = Some(prev);
|
|
self.load_index(prev);
|
|
}
|
|
}
|
|
}
|
|
Message::Seek(pos) => {
|
|
if let Some(eng) = &self.engine {
|
|
eng.seek_normalised(pos.clamp(0.0, 1.0));
|
|
}
|
|
}
|
|
Message::ToggleImmersive => self.immersive = !self.immersive,
|
|
Message::ToggleChrome => {
|
|
if self.immersive {
|
|
self.immersive = false;
|
|
self.restore_sidebar_scroll = true;
|
|
if self.saved_show_settings {
|
|
self.show_settings = true;
|
|
self.restore_settings_scroll = true;
|
|
}
|
|
} else {
|
|
self.saved_show_settings = self.show_settings;
|
|
self.show_settings = false;
|
|
self.immersive = true;
|
|
}
|
|
}
|
|
Message::ToggleSettings => {
|
|
self.show_settings = !self.show_settings;
|
|
if self.show_settings {
|
|
self.restore_settings_scroll = true;
|
|
}
|
|
}
|
|
Message::NoOp => {}
|
|
Message::SettingsScrolled(off) => self.settings_scroll = off,
|
|
Message::SetGlass(on) => self.settings.glass = on,
|
|
Message::SetEntropy(on) => self.settings.entropy_on = on,
|
|
Message::SetAlbumColors(on) => self.settings.album_colors = on,
|
|
Message::SetMirrored(on) => self.settings.mirrored = on,
|
|
Message::SetInverted(on) => self.settings.inverted = on,
|
|
Message::SetEntropyStrength(v) => self.settings.entropy_strength = v.clamp(-1.5, 1.5),
|
|
Message::SetHue(v) => self.settings.hue = v.clamp(0.0, 1.0),
|
|
Message::SetContrast(v) => self.settings.contrast = v.clamp(0.0, 2.0),
|
|
Message::SetBrightness(v) => self.settings.brightness = v.clamp(0.1, 2.0),
|
|
Message::SetNumBins(n) => {
|
|
let n = n.clamp(8, 256);
|
|
self.settings.num_bins = n;
|
|
self.worker.set_num_bins(n as usize);
|
|
}
|
|
Message::SetFft(n) => {
|
|
let fft = n.clamp(512, 65536).next_power_of_two();
|
|
let hop = self.settings.hop.min(fft / 2).max(64);
|
|
self.settings.fft = fft;
|
|
self.settings.hop = hop;
|
|
self.worker.set_dsp_params(fft as usize, hop as usize);
|
|
}
|
|
Message::SetHop(n) => {
|
|
let hop = n.clamp(64, self.settings.fft / 2);
|
|
self.settings.hop = hop;
|
|
self.worker
|
|
.set_dsp_params(self.settings.fft as usize, hop as usize);
|
|
}
|
|
Message::SetGranularity(v) => {
|
|
self.settings.granularity = v.clamp(1, 100);
|
|
self.worker.set_smoothing(
|
|
self.settings.granularity,
|
|
self.settings.detail,
|
|
self.settings.strength,
|
|
);
|
|
}
|
|
Message::SetDetail(v) => {
|
|
self.settings.detail = v.clamp(1, 100);
|
|
self.worker.set_smoothing(
|
|
self.settings.granularity,
|
|
self.settings.detail,
|
|
self.settings.strength,
|
|
);
|
|
}
|
|
Message::SetStrength(v) => {
|
|
self.settings.strength = v.clamp(0.0, 1.0);
|
|
self.worker.set_smoothing(
|
|
self.settings.granularity,
|
|
self.settings.detail,
|
|
self.settings.strength,
|
|
);
|
|
}
|
|
Message::SetGpuBlend(v) => {
|
|
self.settings.gpu_blend = v.clamp(0.0, 1.0);
|
|
self.worker.set_gpu_blend(self.settings.gpu_blend);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// queues a fresh decode for the given track, stamping a new request id.
|
|
fn load_index(&mut self, idx: usize) {
|
|
let track = match self.library.tracks.get(idx) {
|
|
Some(t) => t,
|
|
None => return,
|
|
};
|
|
if track.exporting {
|
|
self.track_loading = true;
|
|
return;
|
|
}
|
|
let path = track.path.clone();
|
|
self.refresh_palette();
|
|
|
|
let Some(eng) = self.engine.as_ref() else {
|
|
return;
|
|
};
|
|
let target_sr = eng.output_sample_rate();
|
|
|
|
self.track_loading = true;
|
|
self.next_decode_id += 1;
|
|
self.current_decode_id = self.next_decode_id;
|
|
self.library_worker
|
|
.request_decode(self.current_decode_id, path, target_sr);
|
|
}
|
|
|
|
/// builds the iced widget tree for the current frame.
|
|
pub fn view(&self) -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
|
|
player::view(self)
|
|
}
|
|
}
|
|
|