mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
refactor(waveform): move waveform data pipeline back to audio-core
The M0 pilot mis-membered the waveform data pipeline into features/waveform, creating a core->feature inversion: ~12 audio-core files in store/ imported refreshWaveformForTrack / fetchWaveformBins / bumpWaveformRefreshGen from the feature barrel. These are playback coordination (write waveformBins into playerStore on track change, last-write-wins gen guard), not seekbar rendering. Move waveformRefresh + waveformRefreshGen (+ tests) to src/store/ and waveformParse (+ test) to src/utils/waveform/ (beside waveformSilence). The feature now holds only the seekbar UI (WaveformSeek/SeekbarPreview + their hooks/render utils); its barrel exports UI only. Core consumers import the pipeline directly from @/store/*. Restores the no-core->feature-inversion rule.
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
/**
|
||||
* Waveform seekbar feature — the canvas seekbar, its waveform-bin loading, and
|
||||
* the per-track refresh generation guard. Co-located mirror of `cover/` /
|
||||
* `music-network/`. `waveformSilence` stays in `utils/waveform/` (crossfade /
|
||||
* auto-dj concern, not seekbar rendering).
|
||||
* Waveform seekbar feature — the canvas seekbar (rendering only). Co-located
|
||||
* mirror of `cover/` / `music-network/`.
|
||||
*
|
||||
* The waveform DATA pipeline is audio-core, NOT this feature: the audio engine
|
||||
* calls it on track change to write `waveformBins` into the player store. It
|
||||
* therefore lives in core, consumed directly (never via this barrel):
|
||||
* - `@/store/waveformRefresh` — `refreshWaveformForTrack` / `fetchWaveformBins`
|
||||
* - `@/store/waveformRefreshGen` — `bumpWaveformRefreshGen` / `getWaveformRefreshGen`
|
||||
* - `@/utils/waveform/waveformParse` — bin coercion (alongside `waveformSilence`,
|
||||
* the crossfade / auto-dj silence helper)
|
||||
* Keeping these in core preserves the iron rule (no core→feature inversion).
|
||||
*/
|
||||
export { default as WaveformSeek } from './components/WaveformSeek';
|
||||
export { SeekbarPreview } from './components/WaveformSeekPreview';
|
||||
export { fetchWaveformBins, refreshWaveformForTrack } from './store/waveformRefresh';
|
||||
export type { WaveformCachePayload } from './store/waveformRefresh';
|
||||
export { bumpWaveformRefreshGen, getWaveformRefreshGen } from './store/waveformRefreshGen';
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
type QueueUndoSnapshot,
|
||||
} from './queueUndo';
|
||||
import { refreshLoudnessForTrack } from './loudnessRefresh';
|
||||
import { refreshWaveformForTrack } from '@/features/waveform';
|
||||
import { refreshWaveformForTrack } from '@/store/waveformRefresh';
|
||||
import { stopRadio } from './radioPlayer';
|
||||
import { clearAllPlaybackScheduleTimers } from './scheduleTimers';
|
||||
import { syncUserQueueMutationToServer } from './queueSync';
|
||||
|
||||
@@ -27,7 +27,7 @@ vi.mock('./pausedRestorePrepare', () => ({
|
||||
preparePausedRestoreOnStartup: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/waveform', () => ({
|
||||
vi.mock('@/store/waveformRefresh', () => ({
|
||||
refreshWaveformForTrack: vi.fn(),
|
||||
}));
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useAuthStore } from './authStore';
|
||||
import { usePlayerStore } from './playerStore';
|
||||
import { preparePausedRestoreOnStartup } from './pausedRestorePrepare';
|
||||
import { pushQueueUndoFromGetter } from './queueUndo';
|
||||
import { refreshWaveformForTrack } from '@/features/waveform';
|
||||
import { refreshWaveformForTrack } from '@/store/waveformRefresh';
|
||||
import {
|
||||
getIdlePullGeneration,
|
||||
isIdleQueuePullSuspended,
|
||||
|
||||
@@ -87,7 +87,7 @@ import {
|
||||
getSeekTarget,
|
||||
getSeekTargetSetAt,
|
||||
} from './seekTargetState';
|
||||
import { refreshWaveformForTrack } from '@/features/waveform';
|
||||
import { refreshWaveformForTrack } from '@/store/waveformRefresh';
|
||||
import { analyzeBoundary, computeWaveformSilence } from '../utils/waveform/waveformSilence';
|
||||
import { autodjMaxOverlapCapSec } from '../utils/playback/autodjOverlapCap';
|
||||
import {
|
||||
|
||||
@@ -24,8 +24,8 @@ import {
|
||||
markNormalizationUiUpdate,
|
||||
} from '../playbackThrottles';
|
||||
import { usePlayerStore } from '../playerStore';
|
||||
import { refreshWaveformForTrack } from '@/features/waveform';
|
||||
import { bumpWaveformRefreshGen } from '@/features/waveform';
|
||||
import { refreshWaveformForTrack } from '@/store/waveformRefresh';
|
||||
import { bumpWaveformRefreshGen } from '@/store/waveformRefreshGen';
|
||||
import { setBytePreloadingId } from '../gaplessPreloadState';
|
||||
|
||||
type PreloadEventPayload = {
|
||||
|
||||
@@ -7,8 +7,8 @@ import { invokeAudioSetNormalizationDeduped } from '../normalizationIpcDedupe';
|
||||
import { usePlayerStore } from '../playerStore';
|
||||
import { clearLoudnessCacheStateForTrackId } from '../loudnessGainCache';
|
||||
import { refreshLoudnessForTrack } from '../loudnessRefresh';
|
||||
import { refreshWaveformForTrack } from '@/features/waveform';
|
||||
import { bumpWaveformRefreshGen } from '@/features/waveform';
|
||||
import { refreshWaveformForTrack } from '@/store/waveformRefresh';
|
||||
import { bumpWaveformRefreshGen } from '@/store/waveformRefreshGen';
|
||||
|
||||
/**
|
||||
* Keeps the Rust audio engine in sync whenever the auth store changes
|
||||
|
||||
@@ -5,7 +5,7 @@ import { emitNormalizationDebug } from '../normalizationDebug';
|
||||
import { invokeAudioSetNormalizationDeduped } from '../normalizationIpcDedupe';
|
||||
import { usePlayerStore } from '../playerStore';
|
||||
import { refreshLoudnessForTrack } from '../loudnessRefresh';
|
||||
import { refreshWaveformForTrack } from '@/features/waveform';
|
||||
import { refreshWaveformForTrack } from '@/store/waveformRefresh';
|
||||
|
||||
/**
|
||||
* One-shot startup sync: pushes the persisted audio settings to the Rust engine
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { getBytePreloadingId, setBytePreloadingId } from './gaplessPreloadState';
|
||||
import { refreshLoudnessForTrack } from './loudnessRefresh';
|
||||
import { usePlayerStore } from './playerStore';
|
||||
import { fetchWaveformBins } from '@/features/waveform';
|
||||
import { fetchWaveformBins } from '@/store/waveformRefresh';
|
||||
|
||||
// Crossfade pre-buffer budget: begin downloading the next track this many
|
||||
// seconds before it needs to play (the crossfade start), so a large lossless
|
||||
|
||||
@@ -39,7 +39,7 @@ vi.mock('./playerStore', () => ({
|
||||
setState: hoisted.playerSetStateMock,
|
||||
},
|
||||
}));
|
||||
vi.mock('@/features/waveform', () => ({
|
||||
vi.mock('@/store/waveformRefreshGen', () => ({
|
||||
bumpWaveformRefreshGen: hoisted.bumpWaveformRefreshGenMock,
|
||||
}));
|
||||
vi.mock('./loudnessGainCache', () => ({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { invoke } from '@tauri-apps/api/core';
|
||||
import { getPlaybackIndexKey } from '../utils/playback/playbackServer';
|
||||
import { useAuthStore } from './authStore';
|
||||
import { usePlayerStore } from './playerStore';
|
||||
import { bumpWaveformRefreshGen } from '@/features/waveform';
|
||||
import { bumpWaveformRefreshGen } from '@/store/waveformRefreshGen';
|
||||
import { clearLoudnessCacheStateForTrackId } from './loudnessGainCache';
|
||||
import { resetLoudnessBackfillStateForTrackId } from './loudnessBackfillState';
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ import {
|
||||
loudnessGainDbForEngineBind,
|
||||
} from './loudnessGainCache';
|
||||
import { refreshLoudnessForTrack } from './loudnessRefresh';
|
||||
import { fetchWaveformBins, refreshWaveformForTrack } from '@/features/waveform';
|
||||
import { fetchWaveformBins, refreshWaveformForTrack } from '@/store/waveformRefresh';
|
||||
import { deriveNormalizationSnapshot } from './normalizationSnapshot';
|
||||
import { useOrbitStore } from './orbitStore';
|
||||
import {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { clearSeekDebounce } from './seekDebounce';
|
||||
import { clearSeekFallbackRetry } from './seekFallbackState';
|
||||
import { clearSeekTarget } from './seekTargetState';
|
||||
import { tryAcquireTogglePlayLock } from './togglePlayLock';
|
||||
import { refreshWaveformForTrack } from '@/features/waveform';
|
||||
import { refreshWaveformForTrack } from '@/store/waveformRefresh';
|
||||
import { clearAutodjTransitionUi } from './autodjTransitionUi';
|
||||
|
||||
type SetState = (
|
||||
|
||||
+3
-3
@@ -22,18 +22,18 @@ const hoisted = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock('@tauri-apps/api/core', () => ({ invoke: hoisted.invokeMock }));
|
||||
vi.mock('@/features/waveform/utils/waveformParse', () => ({ coerceWaveformBins: hoisted.coerceWaveformBinsMock }));
|
||||
vi.mock('@/utils/waveform/waveformParse', () => ({ coerceWaveformBins: hoisted.coerceWaveformBinsMock }));
|
||||
vi.mock('@/store/playerStore', () => ({
|
||||
usePlayerStore: {
|
||||
getState: () => hoisted.playerSnapshot,
|
||||
setState: hoisted.playerSetStateMock,
|
||||
},
|
||||
}));
|
||||
vi.mock('@/features/waveform/store/waveformRefreshGen', () => ({
|
||||
vi.mock('@/store/waveformRefreshGen', () => ({
|
||||
getWaveformRefreshGen: hoisted.getGenMock,
|
||||
}));
|
||||
|
||||
import { refreshWaveformForTrack } from '@/features/waveform/store/waveformRefresh';
|
||||
import { refreshWaveformForTrack } from '@/store/waveformRefresh';
|
||||
|
||||
beforeEach(() => {
|
||||
hoisted.invokeMock.mockReset();
|
||||
@@ -1,8 +1,8 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { coerceWaveformBins } from '@/features/waveform/utils/waveformParse';
|
||||
import { coerceWaveformBins } from '@/utils/waveform/waveformParse';
|
||||
import { getPlaybackIndexKey } from '@/utils/playback/playbackServer';
|
||||
import { usePlayerStore } from '@/store/playerStore';
|
||||
import { getWaveformRefreshGen } from '@/features/waveform/store/waveformRefreshGen';
|
||||
import { getWaveformRefreshGen } from '@/store/waveformRefreshGen';
|
||||
|
||||
/** Subsonic-server waveform-cache row as Rust hands it back. */
|
||||
export type WaveformCachePayload = {
|
||||
+1
-1
@@ -3,7 +3,7 @@ import {
|
||||
_resetWaveformRefreshGenForTest,
|
||||
bumpWaveformRefreshGen,
|
||||
getWaveformRefreshGen,
|
||||
} from '@/features/waveform/store/waveformRefreshGen';
|
||||
} from '@/store/waveformRefreshGen';
|
||||
|
||||
afterEach(() => {
|
||||
_resetWaveformRefreshGenForTest();
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { coerceWaveformBins, waveformBlobLenOk } from '@/features/waveform/utils/waveformParse';
|
||||
import { coerceWaveformBins, waveformBlobLenOk } from '@/utils/waveform/waveformParse';
|
||||
|
||||
describe('waveformBlobLenOk', () => {
|
||||
it('accepts the legacy single-curve length (500)', () => {
|
||||
Reference in New Issue
Block a user