mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
feat(waveform): store peak+mean bins and blend at 70/30
Persist waveform v4 as paired curves per bin (peak and mean absolute value), invalidate cache rows with insufficient data shape, and render the seekbar as 0.7 * mean + 0.3 * peak for a denser, more stable contour. Thanks to @peri4ko for proposing this waveform improvement.
This commit is contained in:
@@ -7,6 +7,11 @@ function fmt(s: number): string {
|
||||
}
|
||||
|
||||
const BAR_COUNT = 500;
|
||||
/** Stored waveform bins per track (matches backend `bin_count` / PCM bins). */
|
||||
const WAVE_BIN_COUNT = 500;
|
||||
/** `0.7 * mean + 0.3 * max` in normalized 0..1 space (v4 cache: first half = peak, second = mean-abs). */
|
||||
const WAVE_MIX_MEAN = 0.7;
|
||||
const WAVE_MIX_MAX = 0.3;
|
||||
const SEG_COUNT = 60;
|
||||
const FLAT_WAVE_NORM = 0.06;
|
||||
const WAVE_MORPH_MS = 1000;
|
||||
@@ -111,8 +116,27 @@ function easeOutCubic(t: number): number {
|
||||
|
||||
function binsToHeights(src: number[]): Float32Array {
|
||||
const h = new Float32Array(BAR_COUNT);
|
||||
const n = src.length;
|
||||
if (n === WAVE_BIN_COUNT * 2) {
|
||||
for (let i = 0; i < BAR_COUNT; i++) {
|
||||
const idx = Math.min(WAVE_BIN_COUNT - 1, Math.floor((i / BAR_COUNT) * WAVE_BIN_COUNT));
|
||||
const maxNorm = Number(src[idx]) / 255;
|
||||
const meanNorm = Number(src[WAVE_BIN_COUNT + idx]) / 255;
|
||||
const v = WAVE_MIX_MEAN * meanNorm + WAVE_MIX_MAX * maxNorm;
|
||||
h[i] = Math.max(0.08, Math.min(1, v));
|
||||
}
|
||||
return h;
|
||||
}
|
||||
if (n === WAVE_BIN_COUNT) {
|
||||
for (let i = 0; i < BAR_COUNT; i++) {
|
||||
const idx = Math.min(WAVE_BIN_COUNT - 1, Math.floor((i / BAR_COUNT) * WAVE_BIN_COUNT));
|
||||
const v = src[idx];
|
||||
h[i] = Math.max(0.08, Math.min(1, (Number(v) / 255)));
|
||||
}
|
||||
return h;
|
||||
}
|
||||
for (let i = 0; i < BAR_COUNT; i++) {
|
||||
const idx = Math.min(src.length - 1, Math.floor((i / BAR_COUNT) * src.length));
|
||||
const idx = Math.min(n - 1, Math.floor((i / BAR_COUNT) * n));
|
||||
const v = src[idx];
|
||||
h[i] = Math.max(0.08, Math.min(1, (Number(v) / 255)));
|
||||
}
|
||||
|
||||
@@ -271,25 +271,34 @@ type WaveformCachePayload = {
|
||||
updatedAt: number;
|
||||
};
|
||||
|
||||
/** v4: `500` peak + `500` mean-abs = `1000` bytes. Legacy single curve: `500` (treated as mean=max). */
|
||||
function waveformBlobLenOk(len: number): boolean {
|
||||
return len === 500 || len === 1000;
|
||||
}
|
||||
|
||||
/** `Vec<u8>` from Rust often arrives as `Uint8Array`, not `Array.isArray`. */
|
||||
function coerceWaveformBins(bins: unknown): number[] | null {
|
||||
if (bins == null) return null;
|
||||
let raw: number[] | null = null;
|
||||
if (Array.isArray(bins)) {
|
||||
return bins.length > 0 ? bins.map(x => Number(x) & 255) : null;
|
||||
}
|
||||
if (bins instanceof Uint8Array) {
|
||||
return bins.length > 0 ? Array.from(bins) : null;
|
||||
}
|
||||
if (typeof bins === 'object' && 'length' in bins && typeof (bins as { length: unknown }).length === 'number') {
|
||||
if (bins.length === 0) return null;
|
||||
raw = bins.map(x => Number(x) & 255);
|
||||
} else if (bins instanceof Uint8Array) {
|
||||
if (bins.length === 0) return null;
|
||||
raw = Array.from(bins);
|
||||
} else if (typeof bins === 'object' && 'length' in bins && typeof (bins as { length: unknown }).length === 'number') {
|
||||
const len = (bins as { length: number }).length;
|
||||
if (len === 0) return null;
|
||||
try {
|
||||
return Array.from(bins as ArrayLike<number>).map(x => Number(x) & 255);
|
||||
raw = Array.from(bins as ArrayLike<number>).map(x => Number(x) & 255);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
if (!waveformBlobLenOk(raw.length)) return null;
|
||||
return raw;
|
||||
}
|
||||
|
||||
type LoudnessCachePayload = {
|
||||
|
||||
Reference in New Issue
Block a user