improve speed scale for waveform + version bump + profile migration

This commit is contained in:
Boof2015
2026-05-19 19:36:27 -04:00
parent 056d46e95d
commit 9416eac19c
11 changed files with 122 additions and 23 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "prism",
"version": "0.1.0-beta",
"version": "0.2.0-beta",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "prism",
"version": "0.1.0-beta",
"version": "0.2.0-beta",
"hasInstallScript": true,
"license": "GPL-3.0-only",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "prism",
"version": "0.1.0-beta",
"version": "0.2.0-beta",
"description": "Open-source audio metering and visualization tool",
"main": "./out/main/index.js",
"scripts": {
+1 -1
View File
@@ -351,7 +351,7 @@ export class FileBackedProfileLibrary {
throw new Error(`Unsupported profile format in ${basename(filePath)}.`)
}
if (candidate.version !== 1 && candidate.version !== PROFILE_FILE_VERSION) {
if (candidate.version !== 1 && candidate.version !== 2 && candidate.version !== PROFILE_FILE_VERSION) {
throw new Error(`Unsupported profile version in ${basename(filePath)}.`)
}
@@ -18,6 +18,11 @@ import {
findVUReferencePreset,
sanitizeVUReferenceDbfs,
} from '../../types/vumeter'
import {
MAX_WAVEFORM_SCROLL_SPEED,
MIN_WAVEFORM_SCROLL_SPEED,
WAVEFORM_SCROLL_SPEED_STEP,
} from '../../types/waveform'
import ThemedSelect from './ThemedSelect'
function vectorscopeModeLabel(mode: ScopeSettings['vectorscope']['mode']): string {
@@ -669,9 +674,9 @@ export default function ScopeSettingsSection({
label="Speed"
value={current.scrollSpeed}
valueLabel={`x${current.scrollSpeed.toFixed(0)}`}
min={1}
max={8}
step={1}
min={MIN_WAVEFORM_SCROLL_SPEED}
max={MAX_WAVEFORM_SCROLL_SPEED}
step={WAVEFORM_SCROLL_SPEED_STEP}
fullWidth={false}
onChange={(value) => onUpdate('waveform', { scrollSpeed: value })}
/>
+1 -1
View File
@@ -62,7 +62,7 @@ const MULTIBAND_DOMINANCE_SENSITIVITY = 5
const MULTIBAND_FOCUSED_BLEND = 0.68
const MULTIBAND_FILL_ALPHA = 0.72
const MULTIBAND_EDGE_ALPHA = 1.0
const BASE_PIXELS_PER_SECOND = 64
const BASE_PIXELS_PER_SECOND = 128
const DISPLAY_MARGIN = 0.95
const defaultWaveformDataSource: WaveformDataSource = {
+27 -7
View File
@@ -9,7 +9,7 @@ import {
type ProfileLocalMetadata,
type PrismProfileFile,
type PrismProfileFileScopePopoutMap,
type PrismProfileFileV2,
type PrismProfileFileV3,
type PrismProfileLocalStateV1,
} from '../types/profile'
import { AUDIO_SCOPE_KINDS, SCOPE_KINDS, normalizeScopeKind, type ScopeKind } from '../types/scope'
@@ -45,6 +45,19 @@ export function cloneScopeSettings(settings: ScopeSettings): ScopeSettings {
return JSON.parse(JSON.stringify(settings)) as ScopeSettings
}
function isLegacyProfileFileVersion(version: unknown): boolean {
return version === 1 || version === 2
}
function normalizeWaveformScrollSpeed(value: unknown, legacyProfileFileScale: boolean): number {
if (legacyProfileFileScale && value !== undefined && value !== null) {
const numeric = Number(value)
return clampWaveformScrollSpeed(Number.isFinite(numeric) ? numeric / 2 : value)
}
return clampWaveformScrollSpeed(value ?? DEFAULT_SCOPE_SETTINGS.waveform.scrollSpeed)
}
export function createDefaultScopePopouts(): ScopePopoutStateMap {
return SCOPE_KINDS.reduce((acc, kind) => {
acc[kind] = { poppedOut: false }
@@ -131,7 +144,10 @@ export function normalizeWidthWeights(raw: unknown): Record<ScopeKind, number> {
}, {} as Record<ScopeKind, number>)
}
export function mergeScopeSettings(raw: unknown): ScopeSettings {
export function mergeScopeSettings(
raw: unknown,
options: { legacyProfileFileScale?: boolean } = {},
): ScopeSettings {
const parsed = typeof raw === 'object' && raw !== null
? raw as Partial<ScopeSettings>
: {}
@@ -195,7 +211,7 @@ export function mergeScopeSettings(raw: unknown): ScopeSettings {
mode: rawWaveform.mode === 'stereo' || rawWaveform.mode === 'mono'
? rawWaveform.mode
: DEFAULT_SCOPE_SETTINGS.waveform.mode,
scrollSpeed: clampWaveformScrollSpeed(rawWaveform.scrollSpeed ?? DEFAULT_SCOPE_SETTINGS.waveform.scrollSpeed),
scrollSpeed: normalizeWaveformScrollSpeed(rawWaveform.scrollSpeed, Boolean(options.legacyProfileFileScale)),
multiband: typeof rawWaveform.multiband === 'boolean'
? rawWaveform.multiband
: DEFAULT_SCOPE_SETTINGS.waveform.multiband,
@@ -272,7 +288,7 @@ export function normalizeProfileFile(
raw: unknown,
fallbackId: string,
fallbackName = DEFAULT_PROFILE_NAME,
) : PrismProfileFileV2 {
) : PrismProfileFileV3 {
const parsed = typeof raw === 'object' && raw !== null
? raw as Partial<PrismProfileFile>
: {}
@@ -291,12 +307,14 @@ export function normalizeProfileFile(
scopeOrder: normalizeScopeOrder(parsed.scopeOrder),
hiddenScopes: normalizeHiddenScopes(parsed.hiddenScopes),
widthWeights: normalizeWidthWeights(parsed.widthWeights),
scopeSettings: mergeScopeSettings(parsed.scopeSettings),
scopeSettings: mergeScopeSettings(parsed.scopeSettings, {
legacyProfileFileScale: isLegacyProfileFileVersion(parsed.version),
}),
scopePopouts: normalizeProfileFileScopePopouts(parsed.scopePopouts),
}
}
export function profileToFileData(id: string, profile: Profile): PrismProfileFileV2 {
export function profileToFileData(id: string, profile: Profile): PrismProfileFileV3 {
const normalized = normalizeProfile(profile, profile.name)
return {
@@ -402,7 +420,9 @@ export function profileFileToProfile(
scopeOrder: normalizeScopeOrder(file.scopeOrder),
hiddenScopes: normalizeHiddenScopes(file.hiddenScopes),
widthWeights: normalizeWidthWeights(file.widthWeights),
scopeSettings: mergeScopeSettings(file.scopeSettings),
scopeSettings: mergeScopeSettings(file.scopeSettings, {
legacyProfileFileScale: isLegacyProfileFileVersion(file.version),
}),
scopePopouts: SCOPE_KINDS.reduce((acc, kind) => {
acc[kind] = {
poppedOut: Boolean(file.scopePopouts[kind]?.poppedOut),
+15 -2
View File
@@ -3,7 +3,7 @@ import type { ScopeKind } from './scope'
import type { ScopeSettings } from './settings'
export const PROFILE_FILE_FORMAT = 'prism-profile'
export const PROFILE_FILE_VERSION = 2
export const PROFILE_FILE_VERSION = 3
export const PROFILE_LOCAL_STATE_FORMAT = 'prism-profile-local'
export const PROFILE_LOCAL_STATE_VERSION = 1
export const LEGACY_PROFILE_MIGRATION_VERSION = 1
@@ -39,6 +39,19 @@ export interface PrismProfileFileV1 {
}
export interface PrismProfileFileV2 {
format: typeof PROFILE_FILE_FORMAT
version: 2
id: string
name: string
themeId?: string | null
scopeOrder: ScopeKind[]
hiddenScopes: ScopeKind[]
widthWeights: Record<ScopeKind, number>
scopeSettings: ScopeSettings
scopePopouts: PrismProfileFileScopePopoutMap
}
export interface PrismProfileFileV3 {
format: typeof PROFILE_FILE_FORMAT
version: typeof PROFILE_FILE_VERSION
id: string
@@ -51,7 +64,7 @@ export interface PrismProfileFileV2 {
scopePopouts: PrismProfileFileScopePopoutMap
}
export type PrismProfileFile = PrismProfileFileV1 | PrismProfileFileV2
export type PrismProfileFile = PrismProfileFileV1 | PrismProfileFileV2 | PrismProfileFileV3
export interface ProfileLocalMetadata {
windowBounds?: WindowBounds
+2 -2
View File
@@ -9,7 +9,7 @@ import {
} from './spectrogram'
import { DEFAULT_VU_REFERENCE_DBFS, type VUMeterMode, type VUMeterNeedleChannels, type VUMeterOrientation } from './vumeter'
import { DEFAULT_LUFS_METER_READOUT, type LUFSMeterMode, type LUFSMeterReadout } from './lufsmeter'
import { DEFAULT_WAVEFORM_MODE, type WaveformMode } from './waveform'
import { DEFAULT_WAVEFORM_MODE, DEFAULT_WAVEFORM_SCROLL_SPEED, type WaveformMode } from './waveform'
import { DEFAULT_SPECTRUM_PEAK_INFO_MODE, type SpectrumPeakInfoMode } from './spectrum'
export interface ScopeSettings {
@@ -80,7 +80,7 @@ export const DEFAULT_SCOPE_SETTINGS: ScopeSettings = {
spectrogram: { fftSize: 4096, tiltDbPerOctave: DEFAULT_SPECTROGRAM_TILT_DB_PER_OCTAVE, scrollSpeed: 2, contrast: DEFAULT_SPECTROGRAM_CONTRAST, clarityMode: 'sharper', scaleMode: 'log', orientation: DEFAULT_SPECTROGRAM_ORIENTATION, colorScheme: 'heat' },
vumeter: { mode: 'bar', orientation: 'horizontal', needleChannels: 'stereo', referenceDb: DEFAULT_VU_REFERENCE_DBFS },
lufsmeter: { mode: 'bar', readout: DEFAULT_LUFS_METER_READOUT },
waveform: { mode: DEFAULT_WAVEFORM_MODE, scrollSpeed: 1, multiband: false },
waveform: { mode: DEFAULT_WAVEFORM_MODE, scrollSpeed: DEFAULT_WAVEFORM_SCROLL_SPEED, multiband: false },
nowPlaying: {
showCoverArt: true,
showTitle: true,
+2 -2
View File
@@ -1,8 +1,8 @@
export type WaveformMode = 'mono' | 'stereo'
export const MIN_WAVEFORM_SCROLL_SPEED = 0.5
export const MIN_WAVEFORM_SCROLL_SPEED = 1
export const MAX_WAVEFORM_SCROLL_SPEED = 8
export const WAVEFORM_SCROLL_SPEED_STEP = 0.5
export const WAVEFORM_SCROLL_SPEED_STEP = 1
export const DEFAULT_WAVEFORM_SCROLL_SPEED = 1
export const DEFAULT_WAVEFORM_MODE: WaveformMode = 'mono'
+35 -2
View File
@@ -8,6 +8,7 @@ import {
createDefaultProfile,
extractLocalProfileMetadata,
mergeScopeSettings,
normalizeProfileFile,
normalizeScopeOrder,
profileFileToProfile,
profileToFileData,
@@ -81,6 +82,38 @@ test('profile file serialization excludes geometry and round-trips with local me
assert.equal(restored.scopeSettings.nowPlaying.showControls, true)
})
test('profile file waveform speed migration preserves legacy scroll feel', () => {
const base = profileToFileData('profile_speed', createDefaultProfile('Speed'))
const withWaveformSpeed = (version: number, scrollSpeed: unknown) => normalizeProfileFile({
...base,
version,
scopeSettings: {
...base.scopeSettings,
waveform: {
...base.scopeSettings.waveform,
scrollSpeed,
},
},
}, 'profile_speed')
assert.equal(withWaveformSpeed(1, 8).scopeSettings.waveform.scrollSpeed, 4)
assert.equal(withWaveformSpeed(2, 8).scopeSettings.waveform.scrollSpeed, 4)
assert.equal(withWaveformSpeed(PROFILE_FILE_VERSION, 4).scopeSettings.waveform.scrollSpeed, 4)
assert.equal(withWaveformSpeed(1, 'fast').scopeSettings.waveform.scrollSpeed, 1)
const legacyMissingSpeed = normalizeProfileFile({
...base,
version: 1,
scopeSettings: {
...base.scopeSettings,
waveform: {
mode: 'mono',
},
},
}, 'profile_speed')
assert.equal(legacyMissingSpeed.scopeSettings.waveform.scrollSpeed, 1)
})
test('mergeScopeSettings defaults missing or invalid spectrogram orientation to horizontal', () => {
const vertical = mergeScopeSettings({
spectrogram: {
@@ -253,7 +286,7 @@ test('partial files normalize, unsupported versions fail, and import does not ch
assert.equal(partialSnapshot.profiles.profile_partial.scopeSettings.spectrogram.colorScheme, 'heat')
assert.equal(partialSnapshot.profiles.profile_partial.scopeSettings.lufsmeter.readout, 'shortTerm')
assert.equal(partialSnapshot.profiles.profile_partial.scopeSettings.waveform.mode, 'stereo')
assert.equal(partialSnapshot.profiles.profile_partial.scopeSettings.waveform.scrollSpeed, 2)
assert.equal(partialSnapshot.profiles.profile_partial.scopeSettings.waveform.scrollSpeed, 1)
assert.equal(partialSnapshot.profiles.profile_partial.scopeSettings.waveform.multiband, true)
assert.equal(Object.hasOwn(partialSnapshot.profiles.profile_partial.scopeSettings.waveform, 'gainDb'), false)
assert.equal(partialSnapshot.profiles.profile_partial.scopePopouts.spectrogram.poppedOut, true)
@@ -285,7 +318,7 @@ test('legacy profile files with themeId import successfully and ignore embedded
const legacyPath = join(harness.rootDir, 'legacy-theme.prsm')
await writeFile(legacyPath, `${JSON.stringify({
format: PROFILE_FILE_FORMAT,
version: PROFILE_FILE_VERSION,
version: 2,
id: 'profile_legacy_theme',
name: 'Legacy Theme',
themeId: 'theme_midnight',
+28
View File
@@ -4501,6 +4501,34 @@ test('Waveform reconfigures and resets native multiband state on option changes'
}
})
test('Waveform maps x4 to the old max scroll rate and x8 to faster headroom', () => {
const dom = installFakeCanvasDom()
const nativeAnalyzer = createFakeWaveformNativeAnalyzer()
const dataSource = {
getPendingWaveformSamples: () => [],
getPendingWaveformStereoSamples: () => [],
getSampleRate: () => 4096,
isPlaying: () => true,
subscribeToSessionChanges: () => () => {},
}
const waveform = new Waveform(createFakeCanvas(), {
dataSource,
nativeAnalyzer,
scrollSpeed: 4,
})
try {
assert.equal(nativeAnalyzer.configs.at(-1)?.samplesPerColumn, 8)
nativeAnalyzer.configs.length = 0
waveform.setOptions({ scrollSpeed: 8 })
assert.equal(nativeAnalyzer.configs.at(-1)?.samplesPerColumn, 4)
} finally {
waveform.dispose()
dom.restore()
}
})
test('Vectorscope multiband uses native interleaved band points when available', () => {
const recorder = createFakeCanvasRecorder()
const dom = installFakeCanvasDom(() => createFakeCanvas(recorder))