mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-12 05:10:51 +02:00
Improve spectrogram
This commit is contained in:
@@ -248,6 +248,7 @@ export function scopeSettingsToOptions(
|
|||||||
backgroundColor: t.background,
|
backgroundColor: t.background,
|
||||||
fftSize: s.fftSize,
|
fftSize: s.fftSize,
|
||||||
scrollSpeed: s.scrollSpeed,
|
scrollSpeed: s.scrollSpeed,
|
||||||
|
contrast: s.contrast,
|
||||||
clarityMode: s.clarityMode,
|
clarityMode: s.clarityMode,
|
||||||
scaleMode: s.scaleMode,
|
scaleMode: s.scaleMode,
|
||||||
colorScheme: s.colorScheme,
|
colorScheme: s.colorScheme,
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { useState, type CSSProperties, type JSX, type ReactNode } from 'react'
|
|||||||
import type { ScopeKind } from '../../types/scope'
|
import type { ScopeKind } from '../../types/scope'
|
||||||
import { SCOPE_LABELS } from '../../types/scope'
|
import { SCOPE_LABELS } from '../../types/scope'
|
||||||
import type { ScopeSettings } from '../../types/settings'
|
import type { ScopeSettings } from '../../types/settings'
|
||||||
|
import {
|
||||||
|
MAX_SPECTROGRAM_CONTRAST,
|
||||||
|
MIN_SPECTROGRAM_CONTRAST,
|
||||||
|
SPECTROGRAM_CONTRAST_STEP,
|
||||||
|
} from '../../types/spectrogram'
|
||||||
import {
|
import {
|
||||||
DEFAULT_VU_REFERENCE_DBFS,
|
DEFAULT_VU_REFERENCE_DBFS,
|
||||||
VU_REFERENCE_MAX_DBFS,
|
VU_REFERENCE_MAX_DBFS,
|
||||||
@@ -531,6 +536,17 @@ export default function ScopeSettingsSection({
|
|||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
onChange={(value) => onUpdate('spectrogram', { scrollSpeed: value })}
|
onChange={(value) => onUpdate('spectrogram', { scrollSpeed: value })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<RangeControl
|
||||||
|
label="Contrast"
|
||||||
|
value={current.contrast}
|
||||||
|
valueLabel={`${current.contrast.toFixed(1)}x`}
|
||||||
|
min={MIN_SPECTROGRAM_CONTRAST}
|
||||||
|
max={MAX_SPECTROGRAM_CONTRAST}
|
||||||
|
step={SPECTROGRAM_CONTRAST_STEP}
|
||||||
|
fullWidth={false}
|
||||||
|
onChange={(value) => onUpdate('spectrogram', { contrast: value })}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import { FrameScheduler } from './frameScheduler'
|
|||||||
import { VisualizerFrameLoop } from './visualizerFrameLoop'
|
import { VisualizerFrameLoop } from './visualizerFrameLoop'
|
||||||
import {
|
import {
|
||||||
DEFAULT_SPECTROGRAM_CLARITY_MODE,
|
DEFAULT_SPECTROGRAM_CLARITY_MODE,
|
||||||
|
DEFAULT_SPECTROGRAM_CONTRAST,
|
||||||
DEFAULT_SPECTROGRAM_SCALE_MODE,
|
DEFAULT_SPECTROGRAM_SCALE_MODE,
|
||||||
DEFAULT_SPECTROGRAM_SCROLL_SPEED,
|
DEFAULT_SPECTROGRAM_SCROLL_SPEED,
|
||||||
|
clampSpectrogramContrast,
|
||||||
clampSpectrogramScrollSpeed,
|
clampSpectrogramScrollSpeed,
|
||||||
isSpectrogramClarityMode,
|
isSpectrogramClarityMode,
|
||||||
isSpectrogramScaleMode,
|
isSpectrogramScaleMode,
|
||||||
@@ -30,6 +32,7 @@ export interface SpectrogramOptions {
|
|||||||
minDecibels?: number
|
minDecibels?: number
|
||||||
maxDecibels?: number
|
maxDecibels?: number
|
||||||
scrollSpeed?: number
|
scrollSpeed?: number
|
||||||
|
contrast?: number
|
||||||
clarityMode?: SpectrogramClarityMode
|
clarityMode?: SpectrogramClarityMode
|
||||||
scaleMode?: SpectrogramScaleMode
|
scaleMode?: SpectrogramScaleMode
|
||||||
colorScheme?: 'heat' | 'mono'
|
colorScheme?: 'heat' | 'mono'
|
||||||
@@ -45,6 +48,7 @@ type ResolvedSpectrogramOptions = Required<Omit<SpectrogramOptions, 'dataSource'
|
|||||||
interface SpectrogramClarityProfile {
|
interface SpectrogramClarityProfile {
|
||||||
gamma: number // contrast curve exponent
|
gamma: number // contrast curve exponent
|
||||||
sharpness: number // local peak suppression exponent (0 = off, higher = thinner lines)
|
sharpness: number // local peak suppression exponent (0 = off, higher = thinner lines)
|
||||||
|
lineWidth: number // target visible line width in pixels (smaller = tighter peaks)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SPECTROGRAM_DISPLAY_GAIN_DB = 2
|
const SPECTROGRAM_DISPLAY_GAIN_DB = 2
|
||||||
@@ -59,6 +63,7 @@ const defaultOptions: ResolvedSpectrogramOptions = {
|
|||||||
minDecibels: -90,
|
minDecibels: -90,
|
||||||
maxDecibels: -12,
|
maxDecibels: -12,
|
||||||
scrollSpeed: DEFAULT_SPECTROGRAM_SCROLL_SPEED,
|
scrollSpeed: DEFAULT_SPECTROGRAM_SCROLL_SPEED,
|
||||||
|
contrast: DEFAULT_SPECTROGRAM_CONTRAST,
|
||||||
clarityMode: DEFAULT_SPECTROGRAM_CLARITY_MODE,
|
clarityMode: DEFAULT_SPECTROGRAM_CLARITY_MODE,
|
||||||
scaleMode: DEFAULT_SPECTROGRAM_SCALE_MODE,
|
scaleMode: DEFAULT_SPECTROGRAM_SCALE_MODE,
|
||||||
colorScheme: 'heat',
|
colorScheme: 'heat',
|
||||||
@@ -75,11 +80,11 @@ const defaultSpectrogramDataSource: SpectrogramDataSource = {
|
|||||||
function getClarityProfile(mode: SpectrogramClarityMode): SpectrogramClarityProfile {
|
function getClarityProfile(mode: SpectrogramClarityMode): SpectrogramClarityProfile {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'classic':
|
case 'classic':
|
||||||
return { gamma: 1.4, sharpness: 0 }
|
return { gamma: 1.4, sharpness: 0, lineWidth: 3 }
|
||||||
case 'sharp':
|
case 'sharp':
|
||||||
return { gamma: 1.5, sharpness: 2.5 }
|
return { gamma: 1.5, sharpness: 2.5, lineWidth: 3 }
|
||||||
case 'sharper':
|
case 'sharper':
|
||||||
return { gamma: 1.6, sharpness: 5.0 }
|
return { gamma: 2.0, sharpness: 5.0, lineWidth: 2 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +106,9 @@ function resolveOptions(base: ResolvedSpectrogramOptions, overrides: Partial<Spe
|
|||||||
scrollSpeed: overrides.scrollSpeed === undefined
|
scrollSpeed: overrides.scrollSpeed === undefined
|
||||||
? base.scrollSpeed
|
? base.scrollSpeed
|
||||||
: clampSpectrogramScrollSpeed(overrides.scrollSpeed),
|
: clampSpectrogramScrollSpeed(overrides.scrollSpeed),
|
||||||
|
contrast: overrides.contrast === undefined
|
||||||
|
? base.contrast
|
||||||
|
: clampSpectrogramContrast(overrides.contrast),
|
||||||
clarityMode: resolveClarityMode(overrides.clarityMode, base.clarityMode),
|
clarityMode: resolveClarityMode(overrides.clarityMode, base.clarityMode),
|
||||||
scaleMode: resolveScaleMode(overrides.scaleMode, base.scaleMode),
|
scaleMode: resolveScaleMode(overrides.scaleMode, base.scaleMode),
|
||||||
colorScheme: overrides.colorScheme ?? base.colorScheme,
|
colorScheme: overrides.colorScheme ?? base.colorScheme,
|
||||||
@@ -639,11 +647,26 @@ export class Spectrogram {
|
|||||||
for (let row = 0; row < height; row += 1) {
|
for (let row = 0; row < height; row += 1) {
|
||||||
const centerBin = this.rowCenterBins[row]
|
const centerBin = this.rowCenterBins[row]
|
||||||
|
|
||||||
// Sub-bin interpolation in dB domain
|
// 4-point Catmull–Rom cubic interpolation in dB — captures the Hann
|
||||||
const binLo = Math.floor(centerBin)
|
// mainlobe curvature so a tone between bins lands on a single row
|
||||||
const binHi = Math.min(binLo + 1, numBins - 1)
|
// instead of smearing linearly across two.
|
||||||
const frac = centerBin - binLo
|
const i1 = Math.floor(centerBin)
|
||||||
const db = magnitudes[binLo] * (1 - frac) + magnitudes[binHi] * frac
|
const frac = centerBin - i1
|
||||||
|
const i0 = Math.max(0, i1 - 1)
|
||||||
|
const i2 = Math.min(numBins - 1, i1 + 1)
|
||||||
|
const i3 = Math.min(numBins - 1, i1 + 2)
|
||||||
|
const m0 = magnitudes[i0]
|
||||||
|
const m1 = magnitudes[i1]
|
||||||
|
const m2 = magnitudes[i2]
|
||||||
|
const m3 = magnitudes[i3]
|
||||||
|
const f2 = frac * frac
|
||||||
|
const f3 = f2 * frac
|
||||||
|
const db = 0.5 * (
|
||||||
|
(2 * m1)
|
||||||
|
+ (-m0 + m2) * frac
|
||||||
|
+ (2 * m0 - 5 * m1 + 4 * m2 - m3) * f2
|
||||||
|
+ (-m0 + 3 * m1 - 3 * m2 + m3) * f3
|
||||||
|
)
|
||||||
|
|
||||||
// Frequency-based tilt — dB per octave from reference, scale-mode independent
|
// Frequency-based tilt — dB per octave from reference, scale-mode independent
|
||||||
const centerFreq = Math.max(1, centerBin * binWidth)
|
const centerFreq = Math.max(1, centerBin * binWidth)
|
||||||
@@ -659,7 +682,7 @@ export class Spectrogram {
|
|||||||
// Hann mainlobe = 4 original bins = 4 * FFT_PAD_FACTOR padded bins
|
// Hann mainlobe = 4 original bins = 4 * FFT_PAD_FACTOR padded bins
|
||||||
const mainlobePaddedBins = 4 * FFT_PAD_FACTOR
|
const mainlobePaddedBins = 4 * FFT_PAD_FACTOR
|
||||||
// Target visual line width in pixels — suppression scales to achieve this
|
// Target visual line width in pixels — suppression scales to achieve this
|
||||||
const TARGET_LINE_WIDTH = 3
|
const TARGET_LINE_WIDTH = clarity.lineWidth
|
||||||
|
|
||||||
for (let row = 0; row < height; row += 1) {
|
for (let row = 0; row < height; row += 1) {
|
||||||
// Adaptive window: mainlobe width in pixel rows at this frequency
|
// Adaptive window: mainlobe width in pixel rows at this frequency
|
||||||
@@ -690,9 +713,10 @@ export class Spectrogram {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 3: apply gamma
|
// Pass 3: apply gamma scaled by user contrast (1.0 = profile default)
|
||||||
|
const effectiveGamma = clarity.gamma * this.options.contrast
|
||||||
for (let row = 0; row < height; row += 1) {
|
for (let row = 0; row < height; row += 1) {
|
||||||
values[row] = Math.pow(raw[row], clarity.gamma)
|
values[row] = Math.pow(raw[row], effectiveGamma)
|
||||||
}
|
}
|
||||||
|
|
||||||
return values
|
return values
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { VectorscopeMode } from '../renderer/visualizers/Vectorscope'
|
import type { VectorscopeMode } from '../renderer/visualizers/Vectorscope'
|
||||||
import type { SpectrogramClarityMode, SpectrogramScaleMode } from './spectrogram'
|
import { DEFAULT_SPECTROGRAM_CONTRAST, type SpectrogramClarityMode, type SpectrogramScaleMode } from './spectrogram'
|
||||||
import { DEFAULT_VU_REFERENCE_DBFS, type VUMeterMode, type VUMeterNeedleChannels, type VUMeterOrientation } from './vumeter'
|
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_LUFS_METER_READOUT, type LUFSMeterMode, type LUFSMeterReadout } from './lufsmeter'
|
||||||
import { DEFAULT_WAVEFORM_MODE, type WaveformMode } from './waveform'
|
import { DEFAULT_WAVEFORM_MODE, type WaveformMode } from './waveform'
|
||||||
@@ -34,6 +34,7 @@ export interface ScopeSettings {
|
|||||||
spectrogram: {
|
spectrogram: {
|
||||||
fftSize: number
|
fftSize: number
|
||||||
scrollSpeed: number
|
scrollSpeed: number
|
||||||
|
contrast: number
|
||||||
clarityMode: SpectrogramClarityMode
|
clarityMode: SpectrogramClarityMode
|
||||||
scaleMode: SpectrogramScaleMode
|
scaleMode: SpectrogramScaleMode
|
||||||
colorScheme: 'heat' | 'mono'
|
colorScheme: 'heat' | 'mono'
|
||||||
@@ -67,7 +68,7 @@ export const DEFAULT_SCOPE_SETTINGS: ScopeSettings = {
|
|||||||
spectrum: { fftSize: 2048, tiltDbPerOctave: 2.0, heatmap: false, heatmapTiltDbPerOctave: 2.0, heatmapSmoothing: 0.5, showGrid: true, smoothing: 0.9, fillGradient: true, showSideLine: false, peakInfoMode: DEFAULT_SPECTRUM_PEAK_INFO_MODE },
|
spectrum: { fftSize: 2048, tiltDbPerOctave: 2.0, heatmap: false, heatmapTiltDbPerOctave: 2.0, heatmapSmoothing: 0.5, showGrid: true, smoothing: 0.9, fillGradient: true, showSideLine: false, peakInfoMode: DEFAULT_SPECTRUM_PEAK_INFO_MODE },
|
||||||
oscilloscope: { pitchLock: true, underfillEnabled: false, showGrid: true, lineWidth: 2 },
|
oscilloscope: { pitchLock: true, underfillEnabled: false, showGrid: true, lineWidth: 2 },
|
||||||
vectorscope: { mode: 'lissajous', multiband: false, showGrid: true, persistence: 0.10, lineWidth: 1.5 },
|
vectorscope: { mode: 'lissajous', multiband: false, showGrid: true, persistence: 0.10, lineWidth: 1.5 },
|
||||||
spectrogram: { fftSize: 2048, scrollSpeed: 2, clarityMode: 'sharper', scaleMode: 'log', colorScheme: 'heat' },
|
spectrogram: { fftSize: 2048, scrollSpeed: 2, contrast: DEFAULT_SPECTROGRAM_CONTRAST, clarityMode: 'sharper', scaleMode: 'log', colorScheme: 'heat' },
|
||||||
vumeter: { mode: 'bar', orientation: 'horizontal', needleChannels: 'stereo', referenceDb: DEFAULT_VU_REFERENCE_DBFS },
|
vumeter: { mode: 'bar', orientation: 'horizontal', needleChannels: 'stereo', referenceDb: DEFAULT_VU_REFERENCE_DBFS },
|
||||||
lufsmeter: { mode: 'bar', readout: DEFAULT_LUFS_METER_READOUT },
|
lufsmeter: { mode: 'bar', readout: DEFAULT_LUFS_METER_READOUT },
|
||||||
waveform: { mode: DEFAULT_WAVEFORM_MODE, scrollSpeed: 1, multiband: false },
|
waveform: { mode: DEFAULT_WAVEFORM_MODE, scrollSpeed: 1, multiband: false },
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ export const MAX_SPECTROGRAM_SCROLL_SPEED = 4
|
|||||||
export const SPECTROGRAM_SCROLL_SPEED_STEP = 0.5
|
export const SPECTROGRAM_SCROLL_SPEED_STEP = 0.5
|
||||||
export const DEFAULT_SPECTROGRAM_SCROLL_SPEED = 2
|
export const DEFAULT_SPECTROGRAM_SCROLL_SPEED = 2
|
||||||
|
|
||||||
|
export const MIN_SPECTROGRAM_CONTRAST = 0.5
|
||||||
|
export const MAX_SPECTROGRAM_CONTRAST = 2.0
|
||||||
|
export const SPECTROGRAM_CONTRAST_STEP = 0.1
|
||||||
|
export const DEFAULT_SPECTROGRAM_CONTRAST = 1.0
|
||||||
|
|
||||||
export function isSpectrogramClarityMode(value: unknown): value is SpectrogramClarityMode {
|
export function isSpectrogramClarityMode(value: unknown): value is SpectrogramClarityMode {
|
||||||
return typeof value === 'string' && SPECTROGRAM_CLARITY_MODES.includes(value as SpectrogramClarityMode)
|
return typeof value === 'string' && SPECTROGRAM_CLARITY_MODES.includes(value as SpectrogramClarityMode)
|
||||||
}
|
}
|
||||||
@@ -36,3 +41,13 @@ export function clampSpectrogramScrollSpeed(value: unknown): number {
|
|||||||
const snapped = Math.round(numeric / SPECTROGRAM_SCROLL_SPEED_STEP) * SPECTROGRAM_SCROLL_SPEED_STEP
|
const snapped = Math.round(numeric / SPECTROGRAM_SCROLL_SPEED_STEP) * SPECTROGRAM_SCROLL_SPEED_STEP
|
||||||
return Math.min(MAX_SPECTROGRAM_SCROLL_SPEED, Math.max(MIN_SPECTROGRAM_SCROLL_SPEED, snapped))
|
return Math.min(MAX_SPECTROGRAM_SCROLL_SPEED, Math.max(MIN_SPECTROGRAM_SCROLL_SPEED, snapped))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clampSpectrogramContrast(value: unknown): number {
|
||||||
|
const numeric = Number(value)
|
||||||
|
if (!Number.isFinite(numeric)) {
|
||||||
|
return DEFAULT_SPECTROGRAM_CONTRAST
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapped = Math.round(numeric / SPECTROGRAM_CONTRAST_STEP) * SPECTROGRAM_CONTRAST_STEP
|
||||||
|
return Math.min(MAX_SPECTROGRAM_CONTRAST, Math.max(MIN_SPECTROGRAM_CONTRAST, snapped))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user