significantly improved and more accurate vectorscope

This commit is contained in:
Boof2015
2026-08-16 20:52:50 -04:00
parent 3e95cbfc28
commit 04f254512c
21 changed files with 682 additions and 335 deletions
+13
View File
@@ -52,6 +52,7 @@ function createProfile(name: string): Profile {
profile.scopeSettings.spectrum.heatmapSmoothing = 0.67
profile.scopeSettings.spectrum.scaleMode = 'mel'
profile.scopeSettings.spectrum.frequencyRangeMode = 'audible'
profile.scopeSettings.vectorscope.zoomDb = 6
profile.scopeSettings.spectrogram.colorScheme = 'mono'
profile.scopeSettings.spectrogram.clarityMode = 'focused'
profile.scopeSettings.spectrogram.scaleMode = 'linear'
@@ -76,6 +77,7 @@ test('profile file serialization excludes geometry and round-trips with local me
assert.equal(file.scopeSettings.spectrum.heatmapSmoothing, 0.67)
assert.equal(file.scopeSettings.spectrum.scaleMode, 'mel')
assert.equal(file.scopeSettings.spectrum.frequencyRangeMode, 'audible')
assert.equal(file.scopeSettings.vectorscope.zoomDb, 6)
assert.equal(file.scopeSettings.spectrogram.scaleMode, 'linear')
assert.equal(file.scopeSettings.spectrogram.clarityMode, 'focused')
assert.equal(file.scopeSettings.spectrogram.frequencyRangeMode, 'extended')
@@ -94,6 +96,7 @@ test('profile file serialization excludes geometry and round-trips with local me
assert.equal(restored.scopeSettings.spectrum.heatmapSmoothing, 0.67)
assert.equal(restored.scopeSettings.spectrum.scaleMode, 'mel')
assert.equal(restored.scopeSettings.spectrum.frequencyRangeMode, 'audible')
assert.equal(restored.scopeSettings.vectorscope.zoomDb, 6)
assert.equal(restored.scopeSettings.spectrogram.colorScheme, 'mono')
assert.equal(restored.scopeSettings.spectrogram.clarityMode, 'focused')
assert.equal(restored.scopeSettings.spectrogram.scaleMode, 'linear')
@@ -202,6 +205,16 @@ test('mergeScopeSettings defaults missing or invalid VU needle channel settings
assert.equal(missing.vumeter.needleChannels, 'stereo')
})
test('mergeScopeSettings normalizes vectorscope zoom without a profile migration', () => {
assert.equal(mergeScopeSettings({ vectorscope: { zoomDb: 6.49 } }).vectorscope.zoomDb, 6)
assert.equal(mergeScopeSettings({ vectorscope: { zoomDb: 6.5 } }).vectorscope.zoomDb, 7)
assert.equal(mergeScopeSettings({ vectorscope: { zoomDb: -99 } }).vectorscope.zoomDb, -12)
assert.equal(mergeScopeSettings({ vectorscope: { zoomDb: 99 } }).vectorscope.zoomDb, 24)
assert.equal(mergeScopeSettings({ vectorscope: { zoomDb: 'loud' } }).vectorscope.zoomDb, 0)
assert.equal(mergeScopeSettings({ vectorscope: {} }).vectorscope.zoomDb, 0)
assert.equal(mergeScopeSettings({}).vectorscope.zoomDb, 0)
})
test('mergeScopeSettings normalizes frequency scales, ranges, clarity, and supported spectrogram speeds', () => {
const valid = mergeScopeSettings({
spectrum: { scaleMode: 'mel', frequencyRangeMode: 'extended' },
+237 -24
View File
@@ -119,6 +119,7 @@ import { decodeSpectrumFrame } from '../src/plugin-ui/juceBridge'
import { formatSpectrumPeakDbfs } from '../src/plugin-ui/peakOverlay'
import { spectrogramSettingsToOptions } from '../src/plugin-ui/spectrogramOptions'
import { spectrumSettingsToOptions } from '../src/plugin-ui/spectrumOptions'
import { vectorscopeSettingsToOptions } from '../src/plugin-ui/vectorscopeOptions'
import { Spectrogram, type SpectrogramOptions } from '../src/renderer/visualizers/Spectrogram'
import { Vectorscope } from '../src/renderer/visualizers/Vectorscope'
import { Waveform } from '../src/renderer/visualizers/Waveform'
@@ -135,6 +136,8 @@ import {
import {
drawVectorscopeGridForMode,
getVectorscopeLayout,
isVectorscopePhaseRisk,
transformPoint,
} from '../src/renderer/visualizers/vectorscopeGrids'
import {
MultibandBuffer,
@@ -155,6 +158,11 @@ import {
normalizedPositionAtFrequency,
type FrequencyScaleMode,
} from '../src/types/frequencyScale'
import {
formatVectorscopeReferenceDbfs,
normalizeVectorscopeZoomDb,
vectorscopeZoomDbToGain,
} from '../src/types/vectorscope'
type WindowWithRaf = typeof globalThis & Pick<Window, 'requestAnimationFrame' | 'cancelAnimationFrame'>
type FakeElectronAPI = {
@@ -613,6 +621,10 @@ interface FakeCanvasRecorder {
lineDash: number[]
}>
lineDashes: number[][]
fills: Array<{
commands: Array<{ kind: 'moveTo' | 'lineTo'; x: number; y: number }>
fillStyle: string
}>
imageDataWrites: Array<{ x: number; y: number; width: number; height: number; data: number[] }>
drawImageCalls: Array<{ compositeOperation: GlobalCompositeOperation; args: unknown[] }>
}
@@ -625,6 +637,7 @@ function createFakeCanvasRecorder(): FakeCanvasRecorder {
strokeRects: [],
arcs: [],
lineDashes: [],
fills: [],
imageDataWrites: [],
drawImageCalls: [],
}
@@ -650,7 +663,9 @@ function createFakeCanvasContext(recorder: FakeCanvasRecorder | null = null): Ca
currentPath = []
},
closePath() {},
fill() {},
fill() {
recorder?.fills.push({ commands: [...currentPath], fillStyle: currentFillStyle })
},
moveTo(x: number, y: number) {
currentPath.push({ kind: 'moveTo', x, y })
},
@@ -3321,9 +3336,11 @@ test('scopeSettingsToOptions wires waveform stereo mode into analyzer options',
test('scopeSettingsToOptions forwards shared scope background and guides to oscilloscope and vectorscope', () => {
const profile = createDefaultProfile('Default')
profile.scopeSettings.vectorscope.zoomDb = 6
const authoredTheme = createDefaultTheme()
authoredTheme.scopes.background = 'rgb(3, 4, 5)'
authoredTheme.scopes.guides = 'rgba(120, 130, 140, 0.2)'
authoredTheme.vectorscope.phaseRisk = 'rgb(200, 100, 50)'
const theme = resolveTheme(authoredTheme)
const oscilloscope = scopeSettingsToOptions('oscilloscope', profile.scopeSettings.oscilloscope, theme.oscilloscope)
@@ -3336,6 +3353,12 @@ test('scopeSettingsToOptions forwards shared scope background and guides to osci
assert.equal(vectorscope.gridMajorColor, theme.vectorscope.guides)
assert.equal(vectorscope.gridMinorColor, theme.vectorscope.guidesSecondary)
assert.equal(vectorscope.labelColor, theme.vectorscope.labels)
assert.equal(vectorscope.phaseRiskColor, 'rgb(200, 100, 50)')
assert.equal(vectorscope.zoomDb, 6)
const pluginVectorscope = vectorscopeSettingsToOptions(profile.scopeSettings.vectorscope, theme.vectorscope)
assert.equal(pluginVectorscope.phaseRiskColor, vectorscope.phaseRiskColor)
assert.equal(pluginVectorscope.zoomDb, vectorscope.zoomDb)
})
test('Oscilloscope projects raw sample amplitude without renderer gain', () => {
@@ -3412,7 +3435,7 @@ test('Vectorscope preserves height-limited and bipolar layout behavior', () => {
}
})
test('vectorscope adds a subtle dashed outer boundary without changing the base graph', () => {
test('vectorscope grids use calibrated boundaries, phase-risk shading, and honest labels', () => {
const lissajousRecorder = createFakeCanvasRecorder()
drawVectorscopeGridForMode(
createFakeCanvasContext(lissajousRecorder),
@@ -3422,12 +3445,24 @@ test('vectorscope adds a subtle dashed outer boundary without changing the base
'rgba(255, 255, 255, 0.05)',
'rgba(255, 255, 255, 0.2)',
'lissajous',
'rgb(255, 191, 0)',
6,
)
assert.equal(
lissajousRecorder.lineDashes.some((segments) => segments.length > 0),
false,
'lissajous should not render an outer headroom boundary',
'XY should not render an arbitrary overflow boundary',
)
assert.equal(lissajousRecorder.strokeRects.length, 1)
assert.equal(lissajousRecorder.strokeRects[0]?.width, getVectorscopeLayout(320, 180, 'lissajous').radius * 2)
assert.equal(
lissajousRecorder.fillRects.filter(({ fillStyle }) => fillStyle === 'rgba(255, 191, 0, 0.08)').length,
2,
'XY should shade its two opposite-sign quadrants',
)
assert.equal(lissajousRecorder.fillTexts.some(({ text }) => text === '-6 dBFS'), true)
assert.equal(lissajousRecorder.fillTexts.some(({ text }) => text === 'M'), true)
assert.equal(lissajousRecorder.fillTexts.some(({ text }) => text === 'S'), true)
const linearRecorder = createFakeCanvasRecorder()
drawVectorscopeGridForMode(
@@ -3438,12 +3473,18 @@ test('vectorscope adds a subtle dashed outer boundary without changing the base
'rgba(255, 255, 255, 0.05)',
'rgba(255, 255, 255, 0.2)',
'linear-bipolar',
'rgb(255, 191, 0)',
)
assert.equal(
linearRecorder.lineDashes.some((segments) => segments.length > 0),
true,
'linear mode should render a dashed outer max boundary',
false,
'linear mode should only use its exact outer diamond',
)
assert.equal(linearRecorder.fills.filter(({ fillStyle }) => fillStyle === 'rgba(255, 191, 0, 0.08)').length, 2)
assert.equal(linearRecorder.fillTexts.some(({ text }) => text === 'M+'), true)
assert.equal(linearRecorder.fillTexts.some(({ text }) => text === 'M'), true)
assert.equal(linearRecorder.fillTexts.some(({ text }) => text === 'S'), true)
assert.equal(linearRecorder.fillTexts.some(({ text }) => text === 'S+'), true)
const polarRecorder = createFakeCanvasRecorder()
drawVectorscopeGridForMode(
@@ -3454,31 +3495,115 @@ test('vectorscope adds a subtle dashed outer boundary without changing the base
'rgba(255, 255, 255, 0.05)',
'rgba(255, 255, 255, 0.2)',
'polar-bipolar',
'rgb(255, 191, 0)',
)
const polarLayout = getVectorscopeLayout(320, 180, 'polar-bipolar')
const dashedPolarArc = polarRecorder.arcs.find((arc) => arc.lineDash.length > 0)
const expectedPolarOverflowRadius = Math.min(
Math.min(polarLayout.centerX, 320 - polarLayout.centerX, polarLayout.centerY, 180 - polarLayout.centerY) * 0.98,
polarLayout.radius * 1.25,
)
assert.equal(
polarRecorder.lineDashes.some((segments) => segments.length > 0),
true,
'polar mode should render a dashed outer boundary',
)
assert.ok(
dashedPolarArc && dashedPolarArc.radius > polarLayout.radius,
'polar dashed boundary should sit outside the existing graph',
false,
'polar mode should not render an arbitrary overflow circle',
)
assertAlmostEqual(
dashedPolarArc?.radius ?? 0,
expectedPolarOverflowRadius,
Math.max(...polarRecorder.arcs.map(({ radius }) => radius)),
polarLayout.radius,
1e-6,
'polar dashed boundary should follow the next relative grid step, clamped to the canvas',
'the outer radial-reference circle should equal the layout radius',
)
assert.equal(polarRecorder.fills.filter(({ fillStyle }) => fillStyle === 'rgba(255, 191, 0, 0.08)').length, 2)
assert.equal(polarRecorder.fillTexts.some(({ text }) => text === '0 dB radial'), true)
})
test('Vectorscope keeps the original linear projection behavior', () => {
test('vectorscope projections calibrate XY and M/S Linear while preserving classic Polar shaping', () => {
const assertPoint = (
actual: { dx: number; dy: number },
expected: { dx: number; dy: number },
message: string,
): void => {
assertAlmostEqual(actual.dx, expected.dx, 1e-9, `${message} x`)
assertAlmostEqual(actual.dy, expected.dy, 1e-9, `${message} y`)
}
assertPoint(transformPoint(0.5, -0.25, 'lissajous'), { dx: -0.25, dy: 0.5 }, 'XY')
assertPoint(transformPoint(1, 1, 'linear-bipolar'), { dx: 0, dy: 1 }, 'linear mono')
assertPoint(transformPoint(1, 0, 'linear-bipolar'), { dx: -0.5, dy: 0.5 }, 'linear left-only')
assertPoint(transformPoint(0, 1, 'linear-bipolar'), { dx: 0.5, dy: 0.5 }, 'linear right-only')
assertPoint(transformPoint(1, -1, 'linear-bipolar'), { dx: -1, dy: 0 }, 'linear anti-phase')
assertPoint(transformPoint(-1, -1, 'linear-bipolar'), { dx: 0, dy: -1 }, 'linear inverted mono')
assertPoint(transformPoint(0.75, -0.25, 'linear-bipolar'), { dx: -0.5, dy: 0.25 }, 'linear unequal')
const polarLeft = transformPoint(1, 0, 'polar-bipolar')
assertAlmostEqual(polarLeft.dx, -Math.SQRT1_2, 1e-9, 'polar left-only x')
assertAlmostEqual(polarLeft.dy, Math.SQRT1_2, 1e-9, 'polar left-only y')
assertAlmostEqual(Math.hypot(polarLeft.dx, polarLeft.dy), 1, 1e-9, 'polar left-only radius')
const polarUnequal = transformPoint(0.75, -0.25, 'polar-bipolar')
assertAlmostEqual(
Math.hypot(polarUnequal.dx, polarUnequal.dy),
Math.pow(Math.hypot(0.75, -0.25), 0.35),
1e-9,
'polar unequal radius retains classic shaping',
)
for (const amplitude of [0.25, 0.5, 1]) {
const xy = transformPoint(amplitude, amplitude, 'lissajous')
const linear = transformPoint(amplitude, amplitude, 'linear-bipolar')
const polar = transformPoint(amplitude, amplitude, 'polar-bipolar')
assertAlmostEqual(Math.max(Math.abs(xy.dx), Math.abs(xy.dy)), amplitude, 1e-9, `XY amplitude ${amplitude}`)
assertAlmostEqual(Math.abs(linear.dx) + Math.abs(linear.dy), amplitude, 1e-9, `linear amplitude ${amplitude}`)
assertAlmostEqual(
Math.hypot(polar.dx, polar.dy),
Math.pow(Math.hypot(amplitude, amplitude), 0.35),
1e-9,
`classic polar amplitude ${amplitude}`,
)
}
for (const mode of ['polar-unipolar', 'linear-unipolar'] as const) {
assertPoint(
transformPoint(-0.8, -0.2, mode),
transformPoint(0.8, 0.2, mode),
`${mode} antipodal fold`,
)
}
})
test('vectorscope zoom maps the labeled per-channel and Polar radial references', () => {
for (const zoomDb of [-12, 0, 24]) {
const inputReference = 10 ** (-zoomDb / 20)
assertAlmostEqual(vectorscopeZoomDbToGain(zoomDb) * inputReference, 1, 1e-12, `zoom ${zoomDb}`)
for (const mode of ['lissajous', 'linear-unipolar', 'linear-bipolar'] as const) {
const point = transformPoint(inputReference, inputReference, mode, zoomDb)
const boundaryValue = mode === 'lissajous'
? Math.max(Math.abs(point.dx), Math.abs(point.dy))
: Math.abs(point.dx) + Math.abs(point.dy)
assertAlmostEqual(boundaryValue, 1, 1e-9, `${mode} at zoom ${zoomDb}`)
}
for (const mode of ['polar-unipolar', 'polar-bipolar'] as const) {
const point = transformPoint(inputReference, 0, mode, zoomDb)
assertAlmostEqual(Math.hypot(point.dx, point.dy), 1, 1e-9, `${mode} radial reference at zoom ${zoomDb}`)
}
}
assert.equal(normalizeVectorscopeZoomDb(6.49), 6)
assert.equal(normalizeVectorscopeZoomDb(6.5), 7)
assert.equal(normalizeVectorscopeZoomDb(-99), -12)
assert.equal(normalizeVectorscopeZoomDb(99), 24)
assert.equal(normalizeVectorscopeZoomDb('invalid'), 0)
assert.equal(formatVectorscopeReferenceDbfs(6), '-6 dBFS')
assert.equal(formatVectorscopeReferenceDbfs(-6), '+6 dBFS')
})
test('vectorscope phase-risk classification treats channel guides as safe boundaries', () => {
assert.equal(isVectorscopePhaseRisk(1, 1), false)
assert.equal(isVectorscopePhaseRisk(-1, -1), false)
assert.equal(isVectorscopePhaseRisk(1, -1), true)
assert.equal(isVectorscopePhaseRisk(-0.25, 0.75), true)
assert.equal(isVectorscopePhaseRisk(1, 0), false)
assert.equal(isVectorscopePhaseRisk(0, -1), false)
})
test('native and JavaScript vectorscope paths project identical channel samples', () => {
const dom = installFakeCanvasDom()
const dataSource = {
getPendingVectorscopeSamples: () => [],
@@ -3486,7 +3611,71 @@ test('Vectorscope keeps the original linear projection behavior', () => {
isPlaying: () => false,
subscribeToSessionChanges: () => () => {},
}
const vectorscope = new Vectorscope(createFakeCanvas(), { dataSource })
const vectorscope = new Vectorscope(createFakeCanvas(), { dataSource, nativeAnalyzer: null, zoomDb: 3 })
try {
const state = vectorscope as unknown as {
drawPoints: (
ctx: CanvasRenderingContext2D,
x: Float32Array,
y: Float32Array,
count: number,
centerX: number,
centerY: number,
scale: number,
) => void
drawFallbackPoints: (
ctx: CanvasRenderingContext2D,
samples: Array<{ left: Float32Array; right: Float32Array }>,
centerX: number,
centerY: number,
scale: number,
) => void
options: { mode: 'polar-unipolar' }
}
state.options.mode = 'polar-unipolar'
const left = -0.8
const right = -0.2
const nativeRecorder = createFakeCanvasRecorder()
const fallbackRecorder = createFakeCanvasRecorder()
state.drawPoints(
createFakeCanvasContext(nativeRecorder),
new Float32Array([right]),
new Float32Array([left]),
1,
100,
100,
80,
)
state.drawFallbackPoints(
createFakeCanvasContext(fallbackRecorder),
[{ left: new Float32Array([left]), right: new Float32Array([right]) }],
100,
100,
80,
)
assert.equal(nativeRecorder.fillRects.length, 1)
assert.equal(fallbackRecorder.fillRects.length, 1)
assertAlmostEqual(nativeRecorder.fillRects[0]?.x ?? 0, fallbackRecorder.fillRects[0]?.x ?? 0, 1e-6, 'path parity x')
assertAlmostEqual(nativeRecorder.fillRects[0]?.y ?? 0, fallbackRecorder.fillRects[0]?.y ?? 0, 1e-6, 'path parity y')
} finally {
vectorscope.dispose()
dom.restore()
}
})
test('Vectorscope applies live calibrated zoom and clears the previous projection', () => {
const dom = installFakeCanvasDom()
const nativeAnalyzer = createFakeVectorscopeNativeAnalyzer()
const dataSource = {
getPendingVectorscopeSamples: () => [],
getSampleRate: () => 48000,
isPlaying: () => false,
subscribeToSessionChanges: () => () => {},
}
const vectorscope = new Vectorscope(createFakeCanvas(), { dataSource, nativeAnalyzer })
try {
const state = vectorscope as unknown as {
@@ -3501,12 +3690,16 @@ test('Vectorscope keeps the original linear projection behavior', () => {
dotSize: number,
) => void
getProjectionScale: (radius: number) => number
options: { zoomDb: number }
}
const layout = getVectorscopeLayout(320, 180, 'linear-bipolar')
const recorder = createFakeCanvasRecorder()
const ctx = createFakeCanvasContext(recorder)
const scale = state.getProjectionScale(layout.radius)
vectorscope.setOptions({ zoomDb: 6.4 })
assert.equal(state.options.zoomDb, 6)
assert.equal(nativeAnalyzer.resetCount, 1)
state.drawProjectedDot(ctx, -1, 1, 'linear-bipolar', layout.centerX, layout.centerY, scale, 2)
assert.equal(recorder.fillRects.length, 1)
@@ -3515,15 +3708,15 @@ test('Vectorscope keeps the original linear projection behavior', () => {
const projectedCenterY = rect.y + rect.height / 2
assertAlmostEqual(
projectedCenterX,
layout.centerX + layout.radius * Math.SQRT2,
layout.centerX + layout.radius * vectorscopeZoomDbToGain(6),
1e-6,
'linear projection should preserve the original unscaled mapping',
'linear projection should apply the normalized zoom gain',
)
assertAlmostEqual(
projectedCenterY,
layout.centerY,
1e-6,
'linear overs peak should stay on the side axis',
'anti-phase projection should stay on the Side axis',
)
} finally {
vectorscope.dispose()
@@ -3677,6 +3870,26 @@ test('scopeSummary includes only waveform display modes', () => {
assert.equal(scopeSummary('waveform', profile.scopeSettings.waveform), 'Stereo · RGB')
})
test('scopeSummary exposes explicit vectorscope geometry and zoom names', () => {
const profile = createDefaultProfile('Default')
assert.equal(scopeSummary('vectorscope', profile.scopeSettings.vectorscope), 'XY (L/R)')
profile.scopeSettings.vectorscope.mode = 'polar-unipolar'
profile.scopeSettings.vectorscope.zoomDb = 6
profile.scopeSettings.vectorscope.multiband = true
assert.equal(
scopeSummary('vectorscope', profile.scopeSettings.vectorscope),
'Polar (Folded) · Zoom +6 dB · RGB',
)
profile.scopeSettings.vectorscope.mode = 'linear-bipolar'
assert.equal(
scopeSummary('vectorscope', profile.scopeSettings.vectorscope),
'M/S Linear (Bipolar) · Zoom +6 dB · RGB',
)
})
test('scopeSummary includes loudness readout source', () => {
const profile = createDefaultProfile('Default')
+5
View File
@@ -63,6 +63,7 @@ test('theme files round-trip and keep grouped sections intact', () => {
theme.controls.flatControls = 'true'
theme.scopes.background = '#030712'
theme.spectrum.heatMid = 'rgb(200, 50, 120)'
theme.vectorscope.phaseRisk = 'rgb(240, 120, 40)'
theme.vumeter.track = '#111827'
theme.vumeter.needleLeft = 'rgb(70, 80, 90)'
@@ -76,6 +77,7 @@ test('theme files round-trip and keep grouped sections intact', () => {
assert.match(serialized, /\[Scopes\]/)
assert.match(serialized, /flat_controls = true/)
assert.match(serialized, /needle_left = 70, 80, 90/)
assert.match(serialized, /phase_risk = 240, 120, 40/)
assert.doesNotMatch(serialized, /^id = /m)
assert.doesNotMatch(serialized, /^name = /m)
@@ -85,6 +87,7 @@ test('theme files round-trip and keep grouped sections intact', () => {
assert.equal(parsed.controls.flatControls, 'true')
assert.equal(parsed.scopes.background, 'rgb(3, 7, 18)')
assert.equal(parsed.spectrum.heatMid, 'rgb(200, 50, 120)')
assert.equal(parsed.vectorscope.phaseRisk, 'rgb(240, 120, 40)')
assert.equal(parsed.vumeter.track, 'rgb(17, 24, 39)')
assert.equal(parsed.vumeter.needleLeft, 'rgb(70, 80, 90)')
assert.equal(parsed.nowPlaying.background, theme.nowPlaying.background)
@@ -316,6 +319,7 @@ test('createTemplateThemeFile presents a simplified recommended theme layout', (
assert.match(template, /^\[Controls\]$/m)
assert.match(template, /^\[Scopes\]$/m)
assert.match(template, /^\[Spectrum\]$/m)
assert.match(template, /^# phase_risk = 255, 191, 0$/m)
assert.match(template, /^# flat_controls = false$/m)
assert.match(template, /^# toolbar_bg = 4, 8, 12, 199$/m)
assert.equal(parsed.app.accent, 'rgb(56, 189, 248)')
@@ -335,6 +339,7 @@ test('parsed template keeps module colors and backgrounds derived from starter a
assert.equal(resolved.spectrum.line, 'rgb(74, 222, 128)')
assert.equal(resolved.oscilloscope.line, 'rgb(74, 222, 128)')
assert.equal(resolved.vectorscope.trace, 'rgb(74, 222, 128)')
assert.equal(resolved.vectorscope.phaseRisk, resolved.interface.warning)
assert.equal(resolved.waveform.line, 'rgb(74, 222, 128)')
assert.equal(resolved.interface.scopeBackground, 'rgb(3, 7, 18)')
assert.equal(resolved.spectrum.background, 'rgb(3, 7, 18)')
+55
View File
@@ -0,0 +1,55 @@
import assert from 'node:assert/strict'
import { createRequire } from 'node:module'
import test from 'node:test'
const require = createRequire(import.meta.url)
const { vectorscope } = require('../native/build/Release/visualizer_dsp.node')
function createHighFrequencyStereo(frequencyHz, sampleRate, length) {
const left = new Float32Array(length)
const right = new Float32Array(length)
for (let index = 0; index < length; index += 1) {
const phase = 2 * Math.PI * frequencyHz * index / sampleRate
left[index] = 0.75 * Math.sin(phase)
right[index] = -0.4 * Math.cos(phase)
}
return { left, right }
}
function assertArrayAlmostEqual(actual, expected, tolerance, message) {
assert.equal(actual.length, expected.length, `${message} length`)
for (let index = 0; index < expected.length; index += 1) {
assert.ok(
Math.abs(actual[index] - expected[index]) <= tolerance,
`${message} sample ${index}: expected ${expected[index]} +/- ${tolerance}, got ${actual[index]}`,
)
}
}
test('native vectorscope preserves full-band channel samples above the former 8 kHz cutoff', () => {
const length = 512
for (const sampleRate of [44100, 48000, 96000]) {
const frequencyHz = 12000
const { left, right } = createHighFrequencyStereo(frequencyHz, sampleRate, length)
vectorscope.setSampleRate(sampleRate)
vectorscope.reset()
vectorscope.pushSamples(left, right)
const result = vectorscope.getPoints(length)
assert.equal(result.count, length)
assertArrayAlmostEqual(result.x, right, 1e-7, `${sampleRate}Hz right/X`)
assertArrayAlmostEqual(result.y, left, 1e-7, `${sampleRate}Hz left/Y`)
}
})
test('native vectorscope legacy process keeps the renderer-facing X/Y channel shape', () => {
const { left, right } = createHighFrequencyStereo(15000, 48000, 256)
vectorscope.setSampleRate(48000)
vectorscope.reset()
const result = vectorscope.process(left, right)
assertArrayAlmostEqual(result.x, right, 1e-7, 'legacy right/X')
assertArrayAlmostEqual(result.y, left, 1e-7, 'legacy left/Y')
})