mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
fix(analysis): gate partial LUFS, seed RAM paths, clarify logs, dedupe IPC (#320)
- Emit streaming partial loudness only when the loudness normalization engine is active - Seed waveform/loudness from in-memory full buffers (preloaded, stream cache, gapless reuse) so offline hot cache does not rely on HTTP backfill - Add explicit ranged/local/preload logs before full-file Symphonia + EBU analysis - Coalesce concurrent waveform DB reads and skip duplicate audio_set_normalization within 450ms (e.g. StrictMode double init)
This commit is contained in:
@@ -356,12 +356,16 @@ pub fn seed_from_bytes(
|
|||||||
return Ok(SeedFromBytesOutcome::SkippedWaveformCacheHit);
|
return Ok(SeedFromBytesOutcome::SkippedWaveformCacheHit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let mib = bytes.len() as f64 / (1024.0 * 1024.0);
|
||||||
crate::app_deprintln!(
|
crate::app_deprintln!(
|
||||||
"[analysis][waveform] build start track_id={} bytes={} md5_16kb={}",
|
"[analysis] full-track analysis start track_id={} input_mib={:.2} md5_16kb={}",
|
||||||
track_id,
|
track_id,
|
||||||
bytes.len(),
|
mib,
|
||||||
key.md5_16kb
|
key.md5_16kb
|
||||||
);
|
);
|
||||||
|
crate::app_deprintln!(
|
||||||
|
"[analysis] full-track analysis work: Symphonia decodes the entire buffer twice (frame timeline, then PCM peak bins), then EBU R128 integrated loudness + true-peak when that succeeds — CPU-bound; large lossless files often take minutes"
|
||||||
|
);
|
||||||
|
|
||||||
let build = (|| -> Result<(bool, usize), String> {
|
let build = (|| -> Result<(bool, usize), String> {
|
||||||
cache.touch_track_status(&key, "queued")?;
|
cache.touch_track_status(&key, "queued")?;
|
||||||
@@ -406,20 +410,21 @@ pub fn seed_from_bytes(
|
|||||||
match &build {
|
match &build {
|
||||||
Ok((used_pcm_decode, bins_len)) => {
|
Ok((used_pcm_decode, bins_len)) => {
|
||||||
crate::app_deprintln!(
|
crate::app_deprintln!(
|
||||||
"[analysis][waveform] build done track_id={} elapsed_ms={} path={} bins_len={}",
|
"[analysis] full-track analysis done track_id={} elapsed_ms={} decode_path={} bins_len={} ebu_loudness_cached={}",
|
||||||
track_id,
|
track_id,
|
||||||
elapsed_ms,
|
elapsed_ms,
|
||||||
if *used_pcm_decode {
|
if *used_pcm_decode {
|
||||||
"pcm_ebur128"
|
"pcm_ebur128"
|
||||||
} else {
|
} else {
|
||||||
"byte_envelope"
|
"byte_envelope_no_ebu"
|
||||||
},
|
},
|
||||||
bins_len
|
bins_len,
|
||||||
|
*used_pcm_decode
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
crate::app_deprintln!(
|
crate::app_deprintln!(
|
||||||
"[analysis][waveform] build failed track_id={} elapsed_ms={} err={}",
|
"[analysis] full-track analysis failed track_id={} elapsed_ms={} err={}",
|
||||||
track_id,
|
track_id,
|
||||||
elapsed_ms,
|
elapsed_ms,
|
||||||
e
|
e
|
||||||
|
|||||||
+124
-22
@@ -1051,6 +1051,7 @@ async fn track_download_task(
|
|||||||
mut prod: HeapProducer<u8>,
|
mut prod: HeapProducer<u8>,
|
||||||
done: Arc<AtomicBool>,
|
done: Arc<AtomicBool>,
|
||||||
promote_cache_slot: Arc<Mutex<Option<PreloadedTrack>>>,
|
promote_cache_slot: Arc<Mutex<Option<PreloadedTrack>>>,
|
||||||
|
normalization_engine: Arc<AtomicU32>,
|
||||||
normalization_target_lufs: Arc<AtomicU32>,
|
normalization_target_lufs: Arc<AtomicU32>,
|
||||||
loudness_pre_analysis_attenuation_db: Arc<AtomicU32>,
|
loudness_pre_analysis_attenuation_db: Arc<AtomicU32>,
|
||||||
cache_track_id: Option<String>,
|
cache_track_id: Option<String>,
|
||||||
@@ -1152,13 +1153,15 @@ async fn track_download_task(
|
|||||||
if !capture_over_limit
|
if !capture_over_limit
|
||||||
&& last_partial_loudness_emit.elapsed() >= Duration::from_millis(PARTIAL_LOUDNESS_EMIT_INTERVAL_MS)
|
&& last_partial_loudness_emit.elapsed() >= Duration::from_millis(PARTIAL_LOUDNESS_EMIT_INTERVAL_MS)
|
||||||
{
|
{
|
||||||
let target_lufs = f32::from_bits(normalization_target_lufs.load(Ordering::Relaxed));
|
|
||||||
let pre_db = f32::from_bits(
|
|
||||||
loudness_pre_analysis_attenuation_db.load(Ordering::Relaxed),
|
|
||||||
)
|
|
||||||
.clamp(-24.0, 0.0);
|
|
||||||
emit_partial_loudness_from_bytes(&app, &url, &capture, target_lufs, pre_db);
|
|
||||||
last_partial_loudness_emit = Instant::now();
|
last_partial_loudness_emit = Instant::now();
|
||||||
|
if normalization_engine.load(Ordering::Relaxed) == 2 {
|
||||||
|
let target_lufs = f32::from_bits(normalization_target_lufs.load(Ordering::Relaxed));
|
||||||
|
let pre_db = f32::from_bits(
|
||||||
|
loudness_pre_analysis_attenuation_db.load(Ordering::Relaxed),
|
||||||
|
)
|
||||||
|
.clamp(-24.0, 0.0);
|
||||||
|
emit_partial_loudness_from_bytes(&app, &url, &capture, target_lufs, pre_db);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
offset += pushed;
|
offset += pushed;
|
||||||
downloaded += pushed as u64;
|
downloaded += pushed as u64;
|
||||||
@@ -1167,6 +1170,11 @@ async fn track_download_task(
|
|||||||
}
|
}
|
||||||
if !capture_over_limit && !capture.is_empty() {
|
if !capture_over_limit && !capture.is_empty() {
|
||||||
if let Some(track_id) = cache_track_id {
|
if let Some(track_id) = cache_track_id {
|
||||||
|
crate::app_deprintln!(
|
||||||
|
"[stream] legacy stream: capture complete track_id={} capture_mib={:.2} — invoking full-track analysis (blocks until seed_from_bytes returns)",
|
||||||
|
track_id,
|
||||||
|
capture.len() as f64 / (1024.0 * 1024.0)
|
||||||
|
);
|
||||||
match crate::analysis_cache::seed_from_bytes(&app, &track_id, &capture) {
|
match crate::analysis_cache::seed_from_bytes(&app, &track_id, &capture) {
|
||||||
Err(e) => crate::app_eprintln!("[analysis] track seed failed for {}: {}", track_id, e),
|
Err(e) => crate::app_eprintln!("[analysis] track seed failed for {}: {}", track_id, e),
|
||||||
Ok(crate::analysis_cache::SeedFromBytesOutcome::Upserted) => {
|
Ok(crate::analysis_cache::SeedFromBytesOutcome::Upserted) => {
|
||||||
@@ -1207,6 +1215,7 @@ async fn ranged_download_task(
|
|||||||
downloaded_to: Arc<AtomicUsize>,
|
downloaded_to: Arc<AtomicUsize>,
|
||||||
done: Arc<AtomicBool>,
|
done: Arc<AtomicBool>,
|
||||||
promote_cache_slot: Arc<Mutex<Option<PreloadedTrack>>>,
|
promote_cache_slot: Arc<Mutex<Option<PreloadedTrack>>>,
|
||||||
|
normalization_engine: Arc<AtomicU32>,
|
||||||
normalization_target_lufs: Arc<AtomicU32>,
|
normalization_target_lufs: Arc<AtomicU32>,
|
||||||
loudness_pre_analysis_attenuation_db: Arc<AtomicU32>,
|
loudness_pre_analysis_attenuation_db: Arc<AtomicU32>,
|
||||||
cache_track_id: Option<String>,
|
cache_track_id: Option<String>,
|
||||||
@@ -1296,23 +1305,25 @@ async fn ranged_download_task(
|
|||||||
&& total_size > 0
|
&& total_size > 0
|
||||||
&& last_partial_loudness_emit.elapsed() >= Duration::from_millis(PARTIAL_LOUDNESS_EMIT_INTERVAL_MS)
|
&& last_partial_loudness_emit.elapsed() >= Duration::from_millis(PARTIAL_LOUDNESS_EMIT_INTERVAL_MS)
|
||||||
{
|
{
|
||||||
let target_lufs = f32::from_bits(normalization_target_lufs.load(Ordering::Relaxed));
|
|
||||||
let start_db = f32::from_bits(loudness_pre_analysis_attenuation_db.load(Ordering::Relaxed))
|
|
||||||
.clamp(-24.0, 0.0);
|
|
||||||
if let Some(provisional_db) =
|
|
||||||
provisional_loudness_gain_from_progress(downloaded, total_size, target_lufs, start_db)
|
|
||||||
{
|
|
||||||
let _ = app.emit(
|
|
||||||
"analysis:loudness-partial",
|
|
||||||
PartialLoudnessPayload {
|
|
||||||
track_id: playback_identity(&url),
|
|
||||||
gain_db: provisional_db,
|
|
||||||
target_lufs,
|
|
||||||
is_partial: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
last_partial_loudness_emit = Instant::now();
|
last_partial_loudness_emit = Instant::now();
|
||||||
|
if normalization_engine.load(Ordering::Relaxed) == 2 {
|
||||||
|
let target_lufs = f32::from_bits(normalization_target_lufs.load(Ordering::Relaxed));
|
||||||
|
let start_db = f32::from_bits(loudness_pre_analysis_attenuation_db.load(Ordering::Relaxed))
|
||||||
|
.clamp(-24.0, 0.0);
|
||||||
|
if let Some(provisional_db) =
|
||||||
|
provisional_loudness_gain_from_progress(downloaded, total_size, target_lufs, start_db)
|
||||||
|
{
|
||||||
|
let _ = app.emit(
|
||||||
|
"analysis:loudness-partial",
|
||||||
|
PartialLoudnessPayload {
|
||||||
|
track_id: playback_identity(&url),
|
||||||
|
gain_db: provisional_db,
|
||||||
|
target_lufs,
|
||||||
|
is_partial: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let mb = downloaded / (1024 * 1024);
|
let mb = downloaded / (1024 * 1024);
|
||||||
if mb >= next_progress_mb {
|
if mb >= next_progress_mb {
|
||||||
@@ -1344,7 +1355,22 @@ async fn ranged_download_task(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if downloaded == total_size && total_size > 0 && total_size <= TRACK_STREAM_PROMOTE_MAX_BYTES {
|
if downloaded == total_size && total_size > 0 && total_size <= TRACK_STREAM_PROMOTE_MAX_BYTES {
|
||||||
|
if let Some(ref tid) = cache_track_id {
|
||||||
|
crate::app_deprintln!(
|
||||||
|
"[stream] ranged: HTTP buffer full track_id={} size_mib={:.2} — cloning {} bytes then full-track analysis (blocks this download task until complete)",
|
||||||
|
tid,
|
||||||
|
total_size as f64 / (1024.0 * 1024.0),
|
||||||
|
total_size
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let t_clone = Instant::now();
|
||||||
let data = buf.lock().unwrap().clone();
|
let data = buf.lock().unwrap().clone();
|
||||||
|
if total_size > 32 * 1024 * 1024 {
|
||||||
|
crate::app_deprintln!(
|
||||||
|
"[stream] ranged: buffer cloned in_ms={}",
|
||||||
|
t_clone.elapsed().as_millis()
|
||||||
|
);
|
||||||
|
}
|
||||||
if let Some(track_id) = cache_track_id {
|
if let Some(track_id) = cache_track_id {
|
||||||
match crate::analysis_cache::seed_from_bytes(&app, &track_id, &data) {
|
match crate::analysis_cache::seed_from_bytes(&app, &track_id, &data) {
|
||||||
Err(e) => crate::app_eprintln!("[analysis] ranged seed failed for {}: {}", track_id, e),
|
Err(e) => crate::app_eprintln!("[analysis] ranged seed failed for {}: {}", track_id, e),
|
||||||
@@ -2735,6 +2761,56 @@ async fn fetch_data(
|
|||||||
Ok(Some(data))
|
Ok(Some(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// When playback uses full track bytes already in RAM (gapless `reuse_chained_bytes`,
|
||||||
|
/// `preloaded`, or `stream_completed_cache` via `fetch_data`), the `psysonic-local`
|
||||||
|
/// disk-read seed path never runs. Spawn the same `seed_from_bytes` work so waveform /
|
||||||
|
/// loudness SQLite can fill **offline** without `analysis_enqueue_seed_from_url` HTTP.
|
||||||
|
fn spawn_analysis_seed_from_in_memory_bytes(
|
||||||
|
app: &AppHandle,
|
||||||
|
cache_track_id: Option<&str>,
|
||||||
|
gen: u64,
|
||||||
|
gen_arc: &Arc<AtomicU64>,
|
||||||
|
bytes: &[u8],
|
||||||
|
) {
|
||||||
|
let Some(track_id) = cache_track_id.map(str::trim).filter(|s| !s.is_empty()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if bytes.is_empty() || bytes.len() > TRACK_STREAM_PROMOTE_MAX_BYTES {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let track_id = track_id.to_string();
|
||||||
|
let bytes = bytes.to_vec();
|
||||||
|
let app = app.clone();
|
||||||
|
let gen_arc = gen_arc.clone();
|
||||||
|
crate::app_deprintln!(
|
||||||
|
"[stream] in-memory play path: scheduling full-track analysis track_id={} size_mib={:.2}",
|
||||||
|
track_id,
|
||||||
|
bytes.len() as f64 / (1024.0 * 1024.0)
|
||||||
|
);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if gen_arc.load(Ordering::SeqCst) != gen {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match crate::analysis_cache::seed_from_bytes(&app, &track_id, &bytes) {
|
||||||
|
Err(e) => crate::app_eprintln!(
|
||||||
|
"[analysis] in-memory play path seed failed for {}: {}",
|
||||||
|
track_id,
|
||||||
|
e
|
||||||
|
),
|
||||||
|
Ok(crate::analysis_cache::SeedFromBytesOutcome::Upserted) => {
|
||||||
|
let _ = app.emit(
|
||||||
|
"analysis:waveform-updated",
|
||||||
|
WaveformUpdatedPayload {
|
||||||
|
track_id: track_id.clone(),
|
||||||
|
is_partial: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(_) => {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// -1 dB headroom applied at full scale to prevent inter-sample clipping.
|
/// -1 dB headroom applied at full scale to prevent inter-sample clipping.
|
||||||
/// Modern masters are often at 0 dBFS; the EQ biquad chain and resampler
|
/// Modern masters are often at 0 dBFS; the EQ biquad chain and resampler
|
||||||
/// can produce inter-sample peaks slightly above ±1.0 → audible distortion.
|
/// can produce inter-sample peaks slightly above ±1.0 → audible distortion.
|
||||||
@@ -2980,6 +3056,13 @@ pub async fn audio_play(
|
|||||||
// b) Server does not → legacy non-seekable AudioStreamReader fallback
|
// b) Server does not → legacy non-seekable AudioStreamReader fallback
|
||||||
// 4) Preloaded/streamed-cache hit → in-memory bytes via fetch_data
|
// 4) Preloaded/streamed-cache hit → in-memory bytes via fetch_data
|
||||||
let play_input = if let Some(d) = reuse_chained_bytes {
|
let play_input = if let Some(d) = reuse_chained_bytes {
|
||||||
|
spawn_analysis_seed_from_in_memory_bytes(
|
||||||
|
&app,
|
||||||
|
cache_id_for_tasks.as_deref(),
|
||||||
|
gen,
|
||||||
|
&state.generation,
|
||||||
|
&d,
|
||||||
|
);
|
||||||
PlayInput::Bytes(d)
|
PlayInput::Bytes(d)
|
||||||
} else {
|
} else {
|
||||||
let stream_cache_hit = {
|
let stream_cache_hit = {
|
||||||
@@ -3029,6 +3112,11 @@ pub async fn audio_play(
|
|||||||
if data.is_empty() || data.len() > TRACK_STREAM_PROMOTE_MAX_BYTES {
|
if data.is_empty() || data.len() > TRACK_STREAM_PROMOTE_MAX_BYTES {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
crate::app_deprintln!(
|
||||||
|
"[stream] psysonic-local: file read complete track_id={} size_mib={:.2} — invoking full-track analysis (async)",
|
||||||
|
seed_id,
|
||||||
|
data.len() as f64 / (1024.0 * 1024.0)
|
||||||
|
);
|
||||||
match crate::analysis_cache::seed_from_bytes(&app_seed, &seed_id, &data) {
|
match crate::analysis_cache::seed_from_bytes(&app_seed, &seed_id, &data) {
|
||||||
Err(e) => crate::app_eprintln!(
|
Err(e) => crate::app_eprintln!(
|
||||||
"[analysis] local-file seed failed for {}: {}",
|
"[analysis] local-file seed failed for {}: {}",
|
||||||
@@ -3106,6 +3194,7 @@ pub async fn audio_play(
|
|||||||
downloaded_to.clone(),
|
downloaded_to.clone(),
|
||||||
done.clone(),
|
done.clone(),
|
||||||
state.stream_completed_cache.clone(),
|
state.stream_completed_cache.clone(),
|
||||||
|
state.normalization_engine.clone(),
|
||||||
state.normalization_target_lufs.clone(),
|
state.normalization_target_lufs.clone(),
|
||||||
state.loudness_pre_analysis_attenuation_db.clone(),
|
state.loudness_pre_analysis_attenuation_db.clone(),
|
||||||
cache_id_for_tasks.clone(),
|
cache_id_for_tasks.clone(),
|
||||||
@@ -3146,6 +3235,7 @@ pub async fn audio_play(
|
|||||||
prod,
|
prod,
|
||||||
done.clone(),
|
done.clone(),
|
||||||
state.stream_completed_cache.clone(),
|
state.stream_completed_cache.clone(),
|
||||||
|
state.normalization_engine.clone(),
|
||||||
state.normalization_target_lufs.clone(),
|
state.normalization_target_lufs.clone(),
|
||||||
state.loudness_pre_analysis_attenuation_db.clone(),
|
state.loudness_pre_analysis_attenuation_db.clone(),
|
||||||
cache_id_for_tasks.clone(),
|
cache_id_for_tasks.clone(),
|
||||||
@@ -3174,6 +3264,13 @@ pub async fn audio_play(
|
|||||||
Some(d) => d,
|
Some(d) => d,
|
||||||
None => return Ok(()), // superseded while downloading
|
None => return Ok(()), // superseded while downloading
|
||||||
};
|
};
|
||||||
|
spawn_analysis_seed_from_in_memory_bytes(
|
||||||
|
&app,
|
||||||
|
cache_id_for_tasks.as_deref(),
|
||||||
|
gen,
|
||||||
|
&state.generation,
|
||||||
|
&data,
|
||||||
|
);
|
||||||
PlayInput::Bytes(data)
|
PlayInput::Bytes(data)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -4279,6 +4376,11 @@ pub async fn audio_preload(
|
|||||||
.map(|s| s.trim().to_string())
|
.map(|s| s.trim().to_string())
|
||||||
.filter(|s| !s.is_empty());
|
.filter(|s| !s.is_empty());
|
||||||
if let Some(track_id) = analysis_cache_track_id(logical_trim.as_deref(), &url) {
|
if let Some(track_id) = analysis_cache_track_id(logical_trim.as_deref(), &url) {
|
||||||
|
crate::app_deprintln!(
|
||||||
|
"[stream] audio_preload: bytes ready track_id={} size_mib={:.2} — invoking full-track analysis",
|
||||||
|
track_id,
|
||||||
|
data.len() as f64 / (1024.0 * 1024.0)
|
||||||
|
);
|
||||||
match crate::analysis_cache::seed_from_bytes(&app, &track_id, &data) {
|
match crate::analysis_cache::seed_from_bytes(&app, &track_id, &data) {
|
||||||
Err(e) => crate::app_eprintln!("[analysis] preload seed failed for {}: {}", track_id, e),
|
Err(e) => crate::app_eprintln!("[analysis] preload seed failed for {}: {}", track_id, e),
|
||||||
Ok(crate::analysis_cache::SeedFromBytesOutcome::Upserted) => {
|
Ok(crate::analysis_cache::SeedFromBytesOutcome::Upserted) => {
|
||||||
|
|||||||
+51
-19
@@ -608,6 +608,28 @@ function touchHotCacheOnPlayback(trackId: string, serverId: string) {
|
|||||||
useHotCacheStore.getState().touchPlayed(trackId, serverId);
|
useHotCacheStore.getState().touchPlayed(trackId, serverId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Coalesce concurrent `analysis_get_waveform_for_track` for one id (StrictMode double mount, init + queue restore). */
|
||||||
|
const waveformRefreshInflight = new Map<string, Promise<void>>();
|
||||||
|
|
||||||
|
/** Skip redundant `audio_set_normalization` IPC when the same payload is sent twice within a short window (e.g. StrictMode). */
|
||||||
|
let lastNormAudioInvokeKey = '';
|
||||||
|
let lastNormAudioInvokeAtMs = 0;
|
||||||
|
|
||||||
|
function invokeAudioSetNormalizationDeduped(payload: {
|
||||||
|
engine: string;
|
||||||
|
targetLufs: number;
|
||||||
|
preAnalysisAttenuationDb: number;
|
||||||
|
}) {
|
||||||
|
const key = `${payload.engine}|${payload.targetLufs}|${payload.preAnalysisAttenuationDb}`;
|
||||||
|
const now = Date.now();
|
||||||
|
if (key === lastNormAudioInvokeKey && now - lastNormAudioInvokeAtMs < 450) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastNormAudioInvokeKey = key;
|
||||||
|
lastNormAudioInvokeAtMs = now;
|
||||||
|
void invoke('audio_set_normalization', payload).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
function isReplayGainActive() {
|
function isReplayGainActive() {
|
||||||
const a = useAuthStore.getState();
|
const a = useAuthStore.getState();
|
||||||
return a.normalizationEngine === 'replaygain' && a.replayGainEnabled;
|
return a.normalizationEngine === 'replaygain' && a.replayGainEnabled;
|
||||||
@@ -676,23 +698,33 @@ async function reseedLoudnessForTrackId(trackId: string) {
|
|||||||
|
|
||||||
async function refreshWaveformForTrack(trackId: string) {
|
async function refreshWaveformForTrack(trackId: string) {
|
||||||
if (!trackId) return;
|
if (!trackId) return;
|
||||||
try {
|
let job = waveformRefreshInflight.get(trackId);
|
||||||
const row = await invoke<WaveformCachePayload | null>('analysis_get_waveform_for_track', { trackId });
|
if (!job) {
|
||||||
// Never apply bins for a non-current track (e.g. gapless byte-preload fetches the neighbour).
|
const p = (async () => {
|
||||||
if (usePlayerStore.getState().currentTrack?.id !== trackId) return;
|
try {
|
||||||
const bins = row ? coerceWaveformBins(row.bins) : null;
|
const row = await invoke<WaveformCachePayload | null>('analysis_get_waveform_for_track', { trackId });
|
||||||
if (!bins || bins.length === 0) {
|
// Never apply bins for a non-current track (e.g. gapless byte-preload fetches the neighbour).
|
||||||
usePlayerStore.setState({
|
if (usePlayerStore.getState().currentTrack?.id !== trackId) return;
|
||||||
waveformBins: null,
|
const bins = row ? coerceWaveformBins(row.bins) : null;
|
||||||
});
|
if (!bins || bins.length === 0) {
|
||||||
return;
|
usePlayerStore.setState({
|
||||||
}
|
waveformBins: null,
|
||||||
usePlayerStore.setState({
|
});
|
||||||
waveformBins: bins,
|
return;
|
||||||
|
}
|
||||||
|
usePlayerStore.setState({
|
||||||
|
waveformBins: bins,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// best-effort; seekbar falls back to placeholder waveform
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
job = p.finally(() => {
|
||||||
|
waveformRefreshInflight.delete(trackId);
|
||||||
});
|
});
|
||||||
} catch {
|
waveformRefreshInflight.set(trackId, job);
|
||||||
// best-effort; seekbar falls back to placeholder waveform
|
|
||||||
}
|
}
|
||||||
|
await job;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** When `syncPlayingEngine` is false, only update `cachedLoudnessGainByTrackId` (e.g. queue neighbour) — do not call `audio_update_replay_gain` for the already-playing track. */
|
/** When `syncPlayingEngine` is false, only update `cachedLoudnessGainByTrackId` (e.g. queue neighbour) — do not call `audio_update_replay_gain` for the already-playing track. */
|
||||||
@@ -1300,11 +1332,11 @@ export function initAudioListeners(): () => void {
|
|||||||
targetLufs: normCfg.loudnessTargetLufs,
|
targetLufs: normCfg.loudnessTargetLufs,
|
||||||
currentTrackId: usePlayerStore.getState().currentTrack?.id ?? null,
|
currentTrackId: usePlayerStore.getState().currentTrack?.id ?? null,
|
||||||
});
|
});
|
||||||
invoke('audio_set_normalization', {
|
invokeAudioSetNormalizationDeduped({
|
||||||
engine: normCfg.normalizationEngine,
|
engine: normCfg.normalizationEngine,
|
||||||
targetLufs: normCfg.loudnessTargetLufs,
|
targetLufs: normCfg.loudnessTargetLufs,
|
||||||
preAnalysisAttenuationDb: normCfg.loudnessPreAnalysisAttenuationDb,
|
preAnalysisAttenuationDb: normCfg.loudnessPreAnalysisAttenuationDb,
|
||||||
}).catch(() => {});
|
});
|
||||||
const bootTrackId = usePlayerStore.getState().currentTrack?.id;
|
const bootTrackId = usePlayerStore.getState().currentTrack?.id;
|
||||||
if (bootTrackId) {
|
if (bootTrackId) {
|
||||||
void refreshWaveformForTrack(bootTrackId);
|
void refreshWaveformForTrack(bootTrackId);
|
||||||
@@ -1356,11 +1388,11 @@ export function initAudioListeners(): () => void {
|
|||||||
targetLufs: state.loudnessTargetLufs,
|
targetLufs: state.loudnessTargetLufs,
|
||||||
currentTrackId: usePlayerStore.getState().currentTrack?.id ?? null,
|
currentTrackId: usePlayerStore.getState().currentTrack?.id ?? null,
|
||||||
});
|
});
|
||||||
invoke('audio_set_normalization', {
|
invokeAudioSetNormalizationDeduped({
|
||||||
engine: state.normalizationEngine,
|
engine: state.normalizationEngine,
|
||||||
targetLufs: state.loudnessTargetLufs,
|
targetLufs: state.loudnessTargetLufs,
|
||||||
preAnalysisAttenuationDb: state.loudnessPreAnalysisAttenuationDb,
|
preAnalysisAttenuationDb: state.loudnessPreAnalysisAttenuationDb,
|
||||||
}).catch(() => {});
|
});
|
||||||
if (state.normalizationEngine === 'loudness') {
|
if (state.normalizationEngine === 'loudness') {
|
||||||
const currentId = usePlayerStore.getState().currentTrack?.id;
|
const currentId = usePlayerStore.getState().currentTrack?.id;
|
||||||
if (onlyPreAnalysisChanged) {
|
if (onlyPreAnalysisChanged) {
|
||||||
|
|||||||
Reference in New Issue
Block a user