player bar: the name sets the width, and the waveform stops flattering the clamp
The source chips are gone — the rail already tells that story — and with them the measured slot, its effects, and the bar's only container query. The now-playing cluster hugs the song name with no cap, giving ground only once the scrubber hits its 260px floor; the artist's 55% ceiling applies only when an album sits beside it, so a lone loading artist no longer renders as two letters in a twelve-pixel box. The waveform is rebuilt on honesty. WAVE_BUCKETS rises 400 → 2000 (~128ms per bucket — at 400, every display bar was ~2s of audio averaged twice), and one shared shaping in waveshape.js replaces both lanes' gain-into-clamp rule, which flat-topped 53.8% of the bars on a real decode and was propped up by per-bar noise in the rail — deleted; the lane shows the record, not a hash of the bar index. Bars fold with a mean+peak blend and stretch across the track's own p05–p97, so a loud master stops pinning to the ceiling and a quiet one stops vanishing. The playbar copies the rail's fade verbatim: whole bars blend accent→grey with cuts landing in the 4px gap, and the one bar under the playhead crossfades toward whatever its neighbours are — the wash while hovering, the muted grey otherwise. The redraw gate watches that blend instead of a 0.75px playhead move, so both lanes now step on every position event. Loading is drawn, not implied: the solid fallback bar is gone and the lane is always the bars, always seekable. Empty, it rests as the rail's dots; a swell crosses them while a waveform is computed (2s sweep, 0.5s rest); a streaming track's bars stand up left→right on a 40ms cadence, each rising over 2s. The rise frontier is the waveform's OWN coverage, reported by stream_waveform beside the peaks — the old contract zero-padded partial peaks to full length and let the UI fold them against a fresher byte count, which stretched the whole shape between refreshes. Where the bars end is how much of the track has actually loaded; cached and local tracks arrive at coverage 1 and pop in complete. The first partial is also requested eagerly (fresh refresh clock per track, 700ms retries until peaks exist), closing most of the audio-before-lane gap.
This commit is contained in:
1 parent
3aaec30dc6
commit
b33d403634
9 files changed
+740
-323
No files matched your search
+48
-10
@@ -3807,7 +3807,7 @@ pub async fn stream_waveform(
|
||||
path: String,
|
||||
size: u64,
|
||||
buckets: usize,
|
||||
) -> Result<Vec<f32>, String> {
|
||||
) -> Result<WaveformSlice, String> {
|
||||
const MAX_WAVEFORM_BUCKETS: usize = 4096;
|
||||
if buckets > MAX_WAVEFORM_BUCKETS {
|
||||
return Err(format!(
|
||||
@@ -3815,10 +3815,10 @@ pub async fn stream_waveform(
|
||||
));
|
||||
}
|
||||
if buckets == 0 {
|
||||
return Ok(Vec::new());
|
||||
return Ok(WaveformSlice::empty());
|
||||
}
|
||||
let Some(cache) = playback::cache_path(&app, &username, &path) else {
|
||||
return Ok(Vec::new());
|
||||
return Ok(WaveformSlice::empty());
|
||||
};
|
||||
let key = format!("{}#{buckets}", cache.display());
|
||||
let job = players.waveform_job(&key);
|
||||
@@ -3830,7 +3830,7 @@ pub async fn stream_waveform(
|
||||
if matches!(std::fs::metadata(&cache), Ok(md) if size == 0 || md.len() == size) {
|
||||
players.remove_progressive_waveform(&key);
|
||||
if let Some(cached) = players.cached_waveform(&key) {
|
||||
return Ok(cached);
|
||||
return Ok(WaveformSlice::complete(cached));
|
||||
}
|
||||
|
||||
let p = cache.clone();
|
||||
@@ -3844,7 +3844,7 @@ pub async fn stream_waveform(
|
||||
players.cache_waveform(key.clone(), peaks.clone());
|
||||
}
|
||||
drop(job_guard);
|
||||
return Ok(peaks);
|
||||
return Ok(WaveformSlice::complete(peaks));
|
||||
}
|
||||
|
||||
// Seeing a partial file invalidates any completed waveform retained
|
||||
@@ -3853,11 +3853,14 @@ pub async fn stream_waveform(
|
||||
let partial = playback::partial_path(&cache);
|
||||
let have = std::fs::metadata(&partial).map(|m| m.len()).unwrap_or(0);
|
||||
if have == 0 || size == 0 {
|
||||
return Ok(Vec::new());
|
||||
return Ok(WaveformSlice::empty());
|
||||
}
|
||||
if let Some(previous) = players.progressive_waveform(&key) {
|
||||
if !previous.should_recompute(have) {
|
||||
return Ok(previous.peaks());
|
||||
return Ok(WaveformSlice {
|
||||
coverage: (previous.decoded_bytes() as f64 / size as f64).min(1.0) as f32,
|
||||
peaks: previous.peaks(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3867,27 +3870,62 @@ pub async fn stream_waveform(
|
||||
// three seconds.
|
||||
let frac = (have as f64 / size as f64).min(1.0);
|
||||
let n = ((buckets as f64 * frac).round() as usize).clamp(1, buckets);
|
||||
let (job_guard, mut peaks) = tauri::async_runtime::spawn_blocking(move || {
|
||||
let (job_guard, peaks) = tauri::async_runtime::spawn_blocking(move || {
|
||||
let peaks = player::compute_waveform(&partial, n);
|
||||
(job_guard, peaks)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !peaks.is_empty() {
|
||||
peaks.resize(buckets, 0.0);
|
||||
players.cache_progressive_waveform(
|
||||
key.clone(),
|
||||
playback::ProgressiveWaveform::new(have, size, peaks.clone()),
|
||||
);
|
||||
}
|
||||
drop(job_guard);
|
||||
Ok(peaks)
|
||||
// The peaks are ONLY the decoded audio — no zero padding to the full
|
||||
// bucket count any more. The old padding made the array's length lie
|
||||
// about its contents: the UI folded real-peaks-plus-zero-tail across a
|
||||
// frontier computed from a FRESHER byte count, so the drawn shape
|
||||
// stretched between refreshes and the loudness scale ate the zeros.
|
||||
// Coverage says exactly how much of the track these peaks describe;
|
||||
// where the standing bars end IS how much of the track has loaded.
|
||||
Ok(WaveformSlice {
|
||||
coverage: frac as f32,
|
||||
peaks,
|
||||
})
|
||||
}
|
||||
.await;
|
||||
players.release_waveform_job(&key, &job);
|
||||
result
|
||||
}
|
||||
|
||||
/// A waveform plus how much of the track it actually describes. `coverage`
|
||||
/// is the byte share of the transfer the peaks were decoded from (1.0 for a
|
||||
/// complete file); the peaks span exactly that prefix of the track and
|
||||
/// nothing more.
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
pub struct WaveformSlice {
|
||||
coverage: f32,
|
||||
peaks: Vec<f32>,
|
||||
}
|
||||
|
||||
impl WaveformSlice {
|
||||
fn empty() -> Self {
|
||||
WaveformSlice {
|
||||
coverage: 0.0,
|
||||
peaks: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn complete(peaks: Vec<f32>) -> Self {
|
||||
WaveformSlice {
|
||||
coverage: 1.0,
|
||||
peaks,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Play an already-downloaded local file.
|
||||
#[tauri::command]
|
||||
pub fn play_local(
|
||||
|
||||
@@ -98,6 +98,12 @@ impl ProgressiveWaveform {
|
||||
pub(crate) fn peaks(&self) -> Vec<f32> {
|
||||
self.peaks.clone()
|
||||
}
|
||||
|
||||
/// How much of the file these peaks were decoded from — the numerator of
|
||||
/// the coverage the UI anchors its rise frontier to.
|
||||
pub(crate) fn decoded_bytes(&self) -> u64 {
|
||||
self.decoded_bytes
|
||||
}
|
||||
}
|
||||
|
||||
/// When the next partial re-decode is due. Early on the file doubles between
|
||||
|
||||
Reference in new issue
Block a user