mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-19 04:06:43 +02:00
128 lines
4.3 KiB
TypeScript
128 lines
4.3 KiB
TypeScript
// Waveform peaks for the seek bar: cache-first, preview-on-miss, accurate
|
|
// decode-on-miss, store. The heavy native decode (extractWaveform) still runs
|
|
// once per track and persists; extractWaveformPreview gives uncached local
|
|
// tracks a fast first paint.
|
|
|
|
import { AstraLibraryScanner } from '../../modules/astra-library-scanner';
|
|
import { openLibraryDb } from '@/db/database';
|
|
import { getWaveformPeaks, putWaveformPeaks } from '@/db/waveformQueries';
|
|
|
|
export const WAVEFORM_BINS = 512;
|
|
export const WAVEFORM_PREVIEW_BINS = 96;
|
|
|
|
export interface WaveformLoadOptions {
|
|
onPreview?: (peaks: Float32Array) => void;
|
|
}
|
|
|
|
// Dedupe concurrent requests for the same track (e.g. mini-player + now-playing).
|
|
const inflight = new Map<string, Promise<Float32Array | null>>();
|
|
const previewInflight = new Map<string, Promise<Float32Array | null>>();
|
|
|
|
export function getWaveform(
|
|
trackPath: string,
|
|
options: WaveformLoadOptions = {}
|
|
): Promise<Float32Array | null> {
|
|
if (!isLocalWaveformPath(trackPath)) return Promise.resolve(null);
|
|
return loadWaveform(trackPath, options);
|
|
}
|
|
|
|
async function loadWaveform(
|
|
trackPath: string,
|
|
options: WaveformLoadOptions
|
|
): Promise<Float32Array | null> {
|
|
const db = await openLibraryDb();
|
|
const cached = await getWaveformPeaks(db, trackPath);
|
|
if (cached && cached.length > 0) return cached;
|
|
|
|
if (options.onPreview) {
|
|
void getWaveformPreview(trackPath).then((preview) => {
|
|
if (preview && preview.length > 0) options.onPreview?.(preview);
|
|
});
|
|
}
|
|
|
|
const existing = inflight.get(trackPath);
|
|
if (existing) return existing;
|
|
const task = decodeAccurateWaveform(trackPath).finally(() => inflight.delete(trackPath));
|
|
inflight.set(trackPath, task);
|
|
return task;
|
|
}
|
|
|
|
async function decodeAccurateWaveform(trackPath: string): Promise<Float32Array | null> {
|
|
let raw: number[];
|
|
try {
|
|
raw = await AstraLibraryScanner.extractWaveform(trackPath, WAVEFORM_BINS);
|
|
} catch {
|
|
return null;
|
|
}
|
|
if (!raw || raw.length === 0) return null;
|
|
|
|
const peaks = Float32Array.from(raw);
|
|
const db = await openLibraryDb();
|
|
await putWaveformPeaks(db, trackPath, peaks).catch(() => {
|
|
/* cache write failure is non-fatal */
|
|
});
|
|
return peaks;
|
|
}
|
|
|
|
function getWaveformPreview(trackPath: string): Promise<Float32Array | null> {
|
|
const existing = previewInflight.get(trackPath);
|
|
if (existing) return existing;
|
|
const task = decodePreviewWaveform(trackPath).finally(() => previewInflight.delete(trackPath));
|
|
previewInflight.set(trackPath, task);
|
|
return task;
|
|
}
|
|
|
|
async function decodePreviewWaveform(trackPath: string): Promise<Float32Array | null> {
|
|
let raw: number[];
|
|
try {
|
|
raw = await AstraLibraryScanner.extractWaveformPreview(trackPath, WAVEFORM_PREVIEW_BINS);
|
|
} catch {
|
|
return null;
|
|
}
|
|
if (!raw || raw.length === 0) return null;
|
|
return Float32Array.from(raw);
|
|
}
|
|
|
|
function isLocalWaveformPath(trackPath: string): boolean {
|
|
return trackPath.startsWith('content://') || trackPath.startsWith('file://');
|
|
}
|
|
|
|
/**
|
|
* Downsample high-res peaks to `barCount` bars with a power curve and two
|
|
* smoothing passes. Ported verbatim from desktop waveformExtractor.ts so the
|
|
* mobile seek bar matches the desktop look.
|
|
*/
|
|
export function downsampleWaveform(source: Float32Array, barCount: number): Float32Array {
|
|
if (source.length === 0 || barCount <= 0) return new Float32Array(0);
|
|
const binsPerBar = source.length / barCount;
|
|
const peaks = new Float32Array(barCount);
|
|
|
|
for (let i = 0; i < barCount; i++) {
|
|
const start = Math.floor(i * binsPerBar);
|
|
const end = Math.max(start + 1, Math.floor((i + 1) * binsPerBar));
|
|
let sum = 0;
|
|
for (let j = start; j < end; j++) sum += source[j];
|
|
peaks[i] = sum / (end - start);
|
|
}
|
|
|
|
let max = 0;
|
|
for (let i = 0; i < barCount; i++) if (peaks[i] > max) max = peaks[i];
|
|
if (max > 0) for (let i = 0; i < barCount; i++) peaks[i] /= max;
|
|
|
|
// Power curve — exaggerate dynamic range.
|
|
for (let i = 0; i < barCount; i++) peaks[i] = peaks[i] ** 2;
|
|
|
|
// Two smoothing passes.
|
|
let current = peaks;
|
|
for (let p = 0; p < 2; p++) {
|
|
const smoothed = new Float32Array(current.length);
|
|
smoothed[0] = current[0];
|
|
smoothed[current.length - 1] = current[current.length - 1];
|
|
for (let i = 1; i < current.length - 1; i++) {
|
|
smoothed[i] = current[i - 1] * 0.25 + current[i] * 0.5 + current[i + 1] * 0.25;
|
|
}
|
|
current = smoothed;
|
|
}
|
|
return current;
|
|
}
|