improve scope speed and efficiency

This commit is contained in:
Boof2015
2026-04-05 15:22:19 -04:00
parent 8b39ff5732
commit 07837949d7
9 changed files with 199 additions and 164 deletions
+1 -4
View File
@@ -64,6 +64,7 @@ function resolveNativeCapturePollDelay(chunkCount: number): number {
return chunkCount > 0 ? 0 : 2 return chunkCount > 0 ? 0 : 2
} }
export interface CaptureManagerStatus { export interface CaptureManagerStatus {
captureMode: CaptureMode captureMode: CaptureMode
backendPolicy: CaptureBackendPolicy backendPolicy: CaptureBackendPolicy
@@ -860,10 +861,6 @@ class AudioCapture {
capturedAt: chunk.capturedAt, capturedAt: chunk.capturedAt,
sequence: chunk.sequence, sequence: chunk.sequence,
}) })
nativeVisualizerTransport.handleChunk(chunk.left, chunk.right, {
sessionId: this.sessionId,
channelCount: chunk.channelCount,
})
} }
setInputGain(db: number): void { setInputGain(db: number): void {
@@ -1,11 +1,6 @@
import { oscilloscope, spectrum, vectorscope, isNativeAvailable } from './native' import { oscilloscope, spectrum, vectorscope, isNativeAvailable } from './native'
import type { VisualizerConsumerDemand } from './AudioRouter' import type { VisualizerConsumerDemand } from './AudioRouter'
export interface NativeVisualizerTransportChunkMeta {
sessionId?: number
channelCount?: number
}
export interface NativeVisualizerTransportSessionState { export interface NativeVisualizerTransportSessionState {
sessionId: number sessionId: number
sampleRate: number sampleRate: number
@@ -78,23 +73,12 @@ function normalizeDemand(demand: VisualizerConsumerDemand): Required<VisualizerC
export class NativeVisualizerTransport { export class NativeVisualizerTransport {
private readonly bridge: NativeVisualizerTransportBridge private readonly bridge: NativeVisualizerTransportBridge
private demand: Required<VisualizerConsumerDemand> = { ...EMPTY_DEMAND } private demand: Required<VisualizerConsumerDemand> = { ...EMPTY_DEMAND }
private sessionId = 0
private sampleRate = 48000 private sampleRate = 48000
private capturing = false
private hasSpectrumData = false
private spectrumMonoScratch = new Float32Array(0)
constructor(bridge: NativeVisualizerTransportBridge = defaultBridge) { constructor(bridge: NativeVisualizerTransportBridge = defaultBridge) {
this.bridge = bridge this.bridge = bridge
} }
private ensureSpectrumMonoScratch(length: number): Float32Array {
if (this.spectrumMonoScratch.length !== length) {
this.spectrumMonoScratch = new Float32Array(length)
}
return this.spectrumMonoScratch
}
setDemand(demand: VisualizerConsumerDemand): void { setDemand(demand: VisualizerConsumerDemand): void {
const nextDemand = normalizeDemand(demand) const nextDemand = normalizeDemand(demand)
if ( if (
@@ -115,7 +99,6 @@ export class NativeVisualizerTransport {
} }
if (this.demand.spectrum && !nextDemand.spectrum) { if (this.demand.spectrum && !nextDemand.spectrum) {
this.bridge.spectrum.reset() this.bridge.spectrum.reset()
this.hasSpectrumData = false
} }
if (this.demand.vectorscope && !nextDemand.vectorscope) { if (this.demand.vectorscope && !nextDemand.vectorscope) {
this.bridge.vectorscope.reset() this.bridge.vectorscope.reset()
@@ -125,7 +108,6 @@ export class NativeVisualizerTransport {
} }
if (!this.demand.spectrum && nextDemand.spectrum) { if (!this.demand.spectrum && nextDemand.spectrum) {
this.bridge.spectrum.setSampleRate(this.sampleRate) this.bridge.spectrum.setSampleRate(this.sampleRate)
this.hasSpectrumData = false
} }
if (!this.demand.vectorscope && nextDemand.vectorscope) { if (!this.demand.vectorscope && nextDemand.vectorscope) {
this.bridge.vectorscope.setSampleRate(this.sampleRate) this.bridge.vectorscope.setSampleRate(this.sampleRate)
@@ -135,52 +117,8 @@ export class NativeVisualizerTransport {
this.demand = nextDemand this.demand = nextDemand
} }
handleChunk(left: Float32Array, right: Float32Array, meta: NativeVisualizerTransportChunkMeta = {}): void {
if (!this.capturing || !this.bridge.isAvailable()) {
return
}
if (meta.sessionId !== undefined && meta.sessionId !== this.sessionId) {
return
}
if (!this.demand.oscilloscope && !this.demand.spectrum && !this.demand.vectorscope) {
return
}
const effectiveChannelCount = Math.max(1, Math.floor(meta.channelCount ?? 2) || 1)
const resolvedRight = effectiveChannelCount > 1 && right.length > 0 ? right : left
const length = Math.min(left.length, resolvedRight.length)
if (length === 0) {
return
}
const leftSamples = left.length === length ? left : left.subarray(0, length)
const rightSamples = resolvedRight.length === length ? resolvedRight : resolvedRight.subarray(0, length)
if (this.demand.oscilloscope) {
this.bridge.oscilloscope.pushSamples(leftSamples)
}
if (this.demand.vectorscope) {
this.bridge.vectorscope.pushSamples(leftSamples, rightSamples)
}
if (this.demand.spectrum) {
const mono = this.ensureSpectrumMonoScratch(length)
for (let index = 0; index < length; index += 1) {
mono[index] = (leftSamples[index] + rightSamples[index]) * 0.5
}
this.bridge.spectrum.pushSamples(mono)
this.hasSpectrumData = true
}
}
reset(sessionState: NativeVisualizerTransportSessionState): void { reset(sessionState: NativeVisualizerTransportSessionState): void {
this.sessionId = sessionState.sessionId
this.capturing = sessionState.capturing
this.sampleRate = Math.max(1, Math.floor(sessionState.sampleRate) || 1) this.sampleRate = Math.max(1, Math.floor(sessionState.sampleRate) || 1)
this.hasSpectrumData = false
if (!this.bridge.isAvailable()) { if (!this.bridge.isAvailable()) {
return return
@@ -204,14 +142,6 @@ export class NativeVisualizerTransport {
this.bridge.vectorscope.setSampleRate(this.sampleRate) this.bridge.vectorscope.setSampleRate(this.sampleRate)
} }
} }
fillLatestSpectrumMagnitudes(output: Float32Array): number {
if (!this.bridge.isAvailable() || !this.demand.spectrum || !this.hasSpectrumData) {
return 0
}
return this.bridge.spectrum.fillMagnitudes(output)
}
} }
export const nativeVisualizerTransport = new NativeVisualizerTransport() export const nativeVisualizerTransport = new NativeVisualizerTransport()
@@ -58,12 +58,6 @@ export class ScopePopoutDataSource implements AnyScopeDataSource {
if (isStereoScope(this.scopeKind)) { if (isStereoScope(this.scopeKind)) {
if (!isStereoBatch(batch)) return if (!isStereoBatch(batch)) return
this.stereoQueue.push(...batch) this.stereoQueue.push(...batch)
for (const chunk of batch) {
this.nativeVisualizerTransport.handleChunk(chunk.left, chunk.right, {
sessionId: this.sessionState.sessionId,
channelCount: this.sessionState.channelCount,
})
}
return return
} }
@@ -71,38 +65,16 @@ export class ScopePopoutDataSource implements AnyScopeDataSource {
if (isStereoBatch(batch)) { if (isStereoBatch(batch)) {
this.monoQueue = [] this.monoQueue = []
this.stereoQueue.push(...batch) this.stereoQueue.push(...batch)
if (this.scopeKind === 'spectrum') {
for (const chunk of batch) {
this.nativeVisualizerTransport.handleChunk(chunk.left, chunk.right, {
sessionId: this.sessionState.sessionId,
channelCount: this.sessionState.channelCount,
})
}
}
return return
} }
this.stereoQueue = [] this.stereoQueue = []
this.monoQueue.push(...batch) this.monoQueue.push(...batch)
if (this.scopeKind === 'spectrum') {
for (const chunk of batch) {
this.nativeVisualizerTransport.handleChunk(chunk, chunk, {
sessionId: this.sessionState.sessionId,
channelCount: 1,
})
}
}
return return
} }
if (isStereoBatch(batch)) return if (isStereoBatch(batch)) return
this.monoQueue.push(...batch) this.monoQueue.push(...batch)
for (const chunk of batch) {
this.nativeVisualizerTransport.handleChunk(chunk, chunk, {
sessionId: this.sessionState.sessionId,
channelCount: 1,
})
}
} }
setSessionState(nextState: ScopePopoutSessionState): void { setSessionState(nextState: ScopePopoutSessionState): void {
@@ -134,9 +106,6 @@ export class ScopePopoutDataSource implements AnyScopeDataSource {
} }
} }
getNativeVisualizerTransport(): NativeVisualizerTransport {
return this.nativeVisualizerTransport
}
getPendingSpectrumSamples(): Float32Array[] { getPendingSpectrumSamples(): Float32Array[] {
const batch = this.monoQueue const batch = this.monoQueue
+30 -6
View File
@@ -86,6 +86,7 @@ export class Oscilloscope {
private staticLayerCtx: CanvasRenderingContext2D private staticLayerCtx: CanvasRenderingContext2D
private staticLayerKey = '' private staticLayerKey = ''
private renderBuffer = new Float32Array(0) private renderBuffer = new Float32Array(0)
private pushScratch = new Float32Array(0)
private static readonly WARMUP_SAMPLES = 4096 private static readonly WARMUP_SAMPLES = 4096
constructor(canvas: HTMLCanvasElement, options: OscilloscopeOptions = {}) { constructor(canvas: HTMLCanvasElement, options: OscilloscopeOptions = {}) {
@@ -186,6 +187,31 @@ export class Oscilloscope {
return this.renderBuffer return this.renderBuffer
} }
private concatMonoChunks(chunks: Float32Array[]): Float32Array {
if (chunks.length === 1) return chunks[0]
let totalLength = 0
for (const chunk of chunks) {
totalLength += chunk.length
}
if (this.pushScratch.length < totalLength) {
this.pushScratch = new Float32Array(totalLength)
}
const out = this.pushScratch.length === totalLength
? this.pushScratch
: this.pushScratch.subarray(0, totalLength)
let offset = 0
for (const chunk of chunks) {
out.set(chunk, offset)
offset += chunk.length
}
return out
}
private drawFrame = (): void => { private drawFrame = (): void => {
const { canvas, ctx, options } = this const { canvas, ctx, options } = this
const width = canvas.width const width = canvas.width
@@ -207,13 +233,11 @@ export class Oscilloscope {
return return
} }
const nativeTransport = this.dataSource.getNativeVisualizerTransport?.() ?? null
const pendingSamples = this.dataSource.getPendingOscilloscopeSamples() const pendingSamples = this.dataSource.getPendingOscilloscopeSamples()
for (const chunk of pendingSamples) { if (pendingSamples.length > 0) {
if (!nativeTransport) { const merged = this.concatMonoChunks(pendingSamples)
nativeOscilloscope.pushSamples(chunk) nativeOscilloscope.pushSamples(merged)
} this.samplesReceived += merged.length
this.samplesReceived += chunk.length
} }
if (options.pitchLock && this.samplesReceived < Oscilloscope.WARMUP_SAMPLES) { if (options.pitchLock && this.samplesReceived < Oscilloscope.WARMUP_SAMPLES) {
+31 -8
View File
@@ -246,6 +246,7 @@ export class SpectrumAnalyzer {
private jsBufferedSamples = 0 private jsBufferedSamples = 0
private jsHasSpectrumData = false private jsHasSpectrumData = false
private nativeMagnitudeBuffer = new Float32Array(0) private nativeMagnitudeBuffer = new Float32Array(0)
private pushScratch = new Float32Array(0)
private primaryPointX = new Float32Array(0) private primaryPointX = new Float32Array(0)
private primaryPointY = new Float32Array(0) private primaryPointY = new Float32Array(0)
private primaryPointHeatmap = new Float32Array(0) private primaryPointHeatmap = new Float32Array(0)
@@ -497,11 +498,38 @@ export class SpectrumAnalyzer {
} }
private pushPendingSpectrumChunks(pendingSpectrum: Float32Array[]): void { private pushPendingSpectrumChunks(pendingSpectrum: Float32Array[]): void {
if (pendingSpectrum.length === 0) return
if (pendingSpectrum.length === 1) {
if (pendingSpectrum[0].length > 0) {
nativeSpectrum.pushSamples(pendingSpectrum[0])
}
return
}
let totalLength = 0
for (const chunk of pendingSpectrum) {
totalLength += chunk.length
}
if (totalLength === 0) return
if (this.pushScratch.length < totalLength) {
this.pushScratch = new Float32Array(totalLength)
}
const merged = this.pushScratch.length === totalLength
? this.pushScratch
: this.pushScratch.subarray(0, totalLength)
let offset = 0
for (const chunk of pendingSpectrum) { for (const chunk of pendingSpectrum) {
if (chunk.length > 0) { if (chunk.length > 0) {
nativeSpectrum.pushSamples(chunk) merged.set(chunk, offset)
offset += chunk.length
} }
} }
nativeSpectrum.pushSamples(merged)
} }
private clearPendingSpectrumQueues(): void { private clearPendingSpectrumQueues(): void {
@@ -747,17 +775,12 @@ export class SpectrumAnalyzer {
return return
} }
const nativeTransport = this.dataSource.getNativeVisualizerTransport?.() ?? null
const pendingSpectrum = this.dataSource.getPendingSpectrumSamples() const pendingSpectrum = this.dataSource.getPendingSpectrumSamples()
if (!nativeTransport) { this.pushPendingSpectrumChunks(pendingSpectrum)
this.pushPendingSpectrumChunks(pendingSpectrum)
}
const nativeMagnitudes = this.ensureNativeMagnitudeBuffer() const nativeMagnitudes = this.ensureNativeMagnitudeBuffer()
primaryData = nativeMagnitudes primaryData = nativeMagnitudes
primaryDataLength = nativeTransport primaryDataLength = nativeSpectrum.fillMagnitudes(nativeMagnitudes)
? nativeTransport.fillLatestSpectrumMagnitudes(nativeMagnitudes)
: nativeSpectrum.fillMagnitudes(nativeMagnitudes)
} }
if (!primaryData || primaryDataLength === 0) { if (!primaryData || primaryDataLength === 0) {
+36 -5
View File
@@ -81,6 +81,8 @@ export class Vectorscope {
private multibandPointScratch: MultibandChunk = createMultibandChunk(0) private multibandPointScratch: MultibandChunk = createMultibandChunk(0)
private nativePointX = new Float32Array(0) private nativePointX = new Float32Array(0)
private nativePointY = new Float32Array(0) private nativePointY = new Float32Array(0)
private pushScratchL = new Float32Array(0)
private pushScratchR = new Float32Array(0)
private staticLayerKey = '' private staticLayerKey = ''
constructor(canvas: HTMLCanvasElement, options: VectorscopeOptions = {}) { constructor(canvas: HTMLCanvasElement, options: VectorscopeOptions = {}) {
@@ -214,16 +216,14 @@ export class Vectorscope {
offscreenCtx.fillRect(0, 0, width, height) offscreenCtx.fillRect(0, 0, width, height)
offscreenCtx.globalCompositeOperation = 'source-over' offscreenCtx.globalCompositeOperation = 'source-over'
const nativeTransport = this.dataSource.getNativeVisualizerTransport?.() ?? null
const pendingSamples = this.dataSource.getPendingVectorscopeSamples() const pendingSamples = this.dataSource.getPendingVectorscopeSamples()
if (options.multiband) { if (options.multiband) {
this.drawMultibandPoints(offscreenCtx, pendingSamples, centerX, centerY, scale) this.drawMultibandPoints(offscreenCtx, pendingSamples, centerX, centerY, scale)
} else if (isNativeAvailable()) { } else if (isNativeAvailable()) {
if (!nativeTransport) { if (pendingSamples.length > 0) {
for (const chunk of pendingSamples) { const { left, right } = this.concatStereoChunks(pendingSamples)
nativeVectorscope.pushSamples(chunk.left, chunk.right) nativeVectorscope.pushSamples(left, right)
}
} }
const count = this.fillNativePoints(options.displayPoints) const count = this.fillNativePoints(options.displayPoints)
@@ -423,6 +423,37 @@ export class Vectorscope {
return this.multibandPointScratch return this.multibandPointScratch
} }
private concatStereoChunks(chunks: Array<{ left: Float32Array; right: Float32Array }>): { left: Float32Array; right: Float32Array } {
if (chunks.length === 1) return chunks[0]
let totalLength = 0
for (const chunk of chunks) {
totalLength += Math.min(chunk.left.length, chunk.right.length)
}
if (this.pushScratchL.length < totalLength) {
this.pushScratchL = new Float32Array(totalLength)
this.pushScratchR = new Float32Array(totalLength)
}
const left = this.pushScratchL.length === totalLength
? this.pushScratchL
: this.pushScratchL.subarray(0, totalLength)
const right = this.pushScratchR.length === totalLength
? this.pushScratchR
: this.pushScratchR.subarray(0, totalLength)
let offset = 0
for (const chunk of chunks) {
const len = Math.min(chunk.left.length, chunk.right.length)
left.set(chunk.left.subarray(0, len), offset)
right.set(chunk.right.subarray(0, len), offset)
offset += len
}
return { left, right }
}
private drawProjectedDot( private drawProjectedDot(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
left: number, left: number,
-4
View File
@@ -1,18 +1,14 @@
import { audioRouter } from '../audio/AudioRouter' import { audioRouter } from '../audio/AudioRouter'
import { nativeVisualizerTransport } from '../audio/NativeVisualizerTransport'
import type { ScopePopoutSessionState } from '../../types/popout' import type { ScopePopoutSessionState } from '../../types/popout'
import type { NativeVisualizerTransport } from '../audio/NativeVisualizerTransport'
export interface VisualizerSessionSource { export interface VisualizerSessionSource {
getSampleRate: () => number getSampleRate: () => number
isPlaying: () => boolean isPlaying: () => boolean
subscribeToSessionChanges: (listener: (state: ScopePopoutSessionState) => void) => () => void subscribeToSessionChanges: (listener: (state: ScopePopoutSessionState) => void) => () => void
getNativeVisualizerTransport?: () => NativeVisualizerTransport | null
} }
export const defaultVisualizerSessionSource: VisualizerSessionSource = { export const defaultVisualizerSessionSource: VisualizerSessionSource = {
getSampleRate: () => audioRouter.getSampleRate(), getSampleRate: () => audioRouter.getSampleRate(),
isPlaying: () => audioRouter.isCapturing(), isPlaying: () => audioRouter.isCapturing(),
subscribeToSessionChanges: (listener) => audioRouter.subscribeToSessionChanges((state) => listener(state)), subscribeToSessionChanges: (listener) => audioRouter.subscribeToSessionChanges((state) => listener(state)),
getNativeVisualizerTransport: () => nativeVisualizerTransport,
} }
+90 -36
View File
@@ -94,6 +94,7 @@ const SCOPES_SCHEMA = {
} as const satisfies SectionSchema<ThemeScopesTokens> } as const satisfies SectionSchema<ThemeScopesTokens>
const SPECTRUM_SCHEMA = { const SPECTRUM_SCHEMA = {
background: 'background',
line: 'line', line: 'line',
side_line: 'sideLine', side_line: 'sideLine',
fill: 'fill', fill: 'fill',
@@ -102,23 +103,28 @@ const SPECTRUM_SCHEMA = {
heat_high: 'heatHigh', heat_high: 'heatHigh',
heat_base: 'heatBase', heat_base: 'heatBase',
guides: 'guides', guides: 'guides',
labels: 'labels',
} as const satisfies SectionSchema<ThemeSpectrumTokens> } as const satisfies SectionSchema<ThemeSpectrumTokens>
const OSCILLOSCOPE_SCHEMA = { const OSCILLOSCOPE_SCHEMA = {
background: 'background',
line: 'line', line: 'line',
fill: 'fill', fill: 'fill',
guides: 'guides', guides: 'guides',
} as const satisfies SectionSchema<ThemeOscilloscopeTokens> } as const satisfies SectionSchema<ThemeOscilloscopeTokens>
const VECTORSCOPE_SCHEMA = { const VECTORSCOPE_SCHEMA = {
background: 'background',
trace: 'trace', trace: 'trace',
band_low: 'bandLow', band_low: 'bandLow',
band_mid: 'bandMid', band_mid: 'bandMid',
band_high: 'bandHigh', band_high: 'bandHigh',
guides: 'guides', guides: 'guides',
labels: 'labels',
} as const satisfies SectionSchema<ThemeVectorscopeTokens> } as const satisfies SectionSchema<ThemeVectorscopeTokens>
const SPECTROGRAM_SCHEMA = { const SPECTROGRAM_SCHEMA = {
background: 'background',
mono: 'mono', mono: 'mono',
heat_low: 'heatLow', heat_low: 'heatLow',
heat_mid: 'heatMid', heat_mid: 'heatMid',
@@ -126,21 +132,26 @@ const SPECTROGRAM_SCHEMA = {
} as const satisfies SectionSchema<ThemeSpectrogramTokens> } as const satisfies SectionSchema<ThemeSpectrogramTokens>
const VUMETER_SCHEMA = { const VUMETER_SCHEMA = {
background: 'background',
level: 'level', level: 'level',
track: 'track', track: 'track',
peak: 'peak', peak: 'peak',
clip: 'clip', clip: 'clip',
scale: 'scale', scale: 'scale',
labels: 'labels',
} as const satisfies SectionSchema<ThemeVUMeterTokens> } as const satisfies SectionSchema<ThemeVUMeterTokens>
const LUFSMETER_SCHEMA = { const LUFSMETER_SCHEMA = {
background: 'background',
level: 'level', level: 'level',
track: 'track', track: 'track',
target: 'target', target: 'target',
scale: 'scale', scale: 'scale',
labels: 'labels',
} as const satisfies SectionSchema<ThemeLUFSMeterTokens> } as const satisfies SectionSchema<ThemeLUFSMeterTokens>
const WAVEFORM_SCHEMA = { const WAVEFORM_SCHEMA = {
background: 'background',
line: 'line', line: 'line',
band_low: 'bandLow', band_low: 'bandLow',
band_mid: 'bandMid', band_mid: 'bandMid',
@@ -817,40 +828,82 @@ export function serializeThemeFile(theme: PrismTheme): string {
} }
export function createTemplateThemeFile(): string { export function createTemplateThemeFile(): string {
const template = createDefaultTheme() const base = createDefaultTheme()
template.id = 'theme_template' const resolved = resolveTheme(base)
template.name = 'Template Theme'
template.credit = 'Your Name' const template: PrismTheme = {
template.website = 'https://example.com' id: 'theme_template',
name: 'Template Theme',
credit: 'Your Name',
website: 'https://example.com',
description: 'Custom Prism theme',
app: {
...base.app,
toolbarBg: resolved.interface.toolbarBg,
settingsBgTop: resolved.interface.settingsBgTop,
settingsBgBottom: resolved.interface.settingsBgBottom,
bottomBarBg: resolved.interface.bottomBarBg,
},
controls: { ...base.controls },
scopes: { ...base.scopes },
spectrum: {
...base.spectrum,
background: resolved.spectrum.background,
guides: resolved.spectrum.guides,
labels: resolved.spectrum.labels,
heatBase: resolved.spectrum.heatBase,
},
oscilloscope: {
...base.oscilloscope,
background: resolved.oscilloscope.background,
guides: resolved.oscilloscope.guides,
},
vectorscope: {
...base.vectorscope,
background: resolved.vectorscope.background,
guides: resolved.vectorscope.guides,
labels: resolved.vectorscope.labels,
},
spectrogram: {
...base.spectrogram,
background: resolved.spectrogram.background,
},
vumeter: {
...base.vumeter,
background: resolved.vumeter.background,
scale: resolved.vumeter.scale,
labels: resolved.vumeter.labels,
},
lufsmeter: {
...base.lufsmeter,
background: resolved.lufsmeter.background,
scale: resolved.lufsmeter.scale,
labels: resolved.lufsmeter.labels,
},
waveform: {
...base.waveform,
background: resolved.waveform.background,
guides: resolved.waveform.guides,
},
astra: { ...base.astra },
}
return `# Prism theme template return `# Prism theme template
# #
# Authoring rules: # Colors use R, G, B or R, G, B, A (0-255)
# - Colors use R, G, B or R, G, B, A (0-255) # CSS colors like #hex, rgb(), and rgba() also work
# - CSS colors like #hex, rgb(), and rgba() also work
# #
# ── UI ── # ── UI ──
# [App] Overall window chrome, background, and text # [App] Window background, text, and accent color
# toolbar_bg, settings_bg_top, settings_bg_bottom, bottom_bar_bg
# can be set to override the default derived values
# [Controls] Buttons, inputs, menus, and sliders # [Controls] Buttons, inputs, menus, and sliders
# Set flat_controls = true to disable glass highlights/gradients # Set flat_controls = true to disable glass highlights/gradients
# #
# ── Scopes ── # ── Scopes ──
# [Scopes] Shared background and guide color for all scopes # [Scopes] Shared defaults for all scopes (background, guides, overlays)
# background defaults to [App] background if not set
# Set it explicitly here to decouple scope bg from window bg
# #
# Per-scope sections override colors unique to that module. # Each scope section below has FULL control over its own colors.
# Each scope can set its own guides to override the shared [Scopes] guides. # Per-scope values override the shared [Scopes] defaults.
# # Every token shown below can be changed independently.
# [Spectrum] line, fill, heat gradient (heat_low/mid/high/base), guides
# [Oscilloscope] line, fill, guides
# [Vectorscope] trace, band_low/mid/high (RGB overlay colors), guides
# [Spectrogram] mono, heat gradient
# [VUMeter] level, track, peak, clip, scale
# [LUFSMeter] level, track, target, scale
# [Waveform] line, band_low/mid/high, guides
# #
${serializeThemeFile(template)}` ${serializeThemeFile(template)}`
} }
@@ -1038,13 +1091,14 @@ function resolveSpectrumTheme(
const sideLine = section.sideLine ?? withAlpha(line, 0.5) const sideLine = section.sideLine ?? withAlpha(line, 0.5)
const fill = section.fill ?? withAlpha(line, 0.34) const fill = section.fill ?? withAlpha(line, 0.34)
const guides = section.guides ?? scopes.guides const guides = section.guides ?? scopes.guides
const background = section.background ?? scopes.background
return { return {
line, line,
sideLine, sideLine,
guides, guides,
guidesSecondary: multiplyAlpha(guides, 0.5), guidesSecondary: multiplyAlpha(guides, 0.5),
labels: section.guides ? guides : scopes.labels, labels: section.labels ?? guides,
background: scopes.background, background,
fill, fill,
fillGradient: [ fillGradient: [
withAlpha(fill, 0), withAlpha(fill, 0),
@@ -1056,7 +1110,7 @@ function resolveSpectrumTheme(
section.heatMid ?? DEFAULT_HEAT_MID, section.heatMid ?? DEFAULT_HEAT_MID,
section.heatHigh ?? DEFAULT_HEAT_HIGH, section.heatHigh ?? DEFAULT_HEAT_HIGH,
], ],
heatBase: section.heatBase ?? scopes.background, heatBase: section.heatBase ?? background,
} }
} }
@@ -1070,7 +1124,7 @@ function resolveOscilloscopeTheme(
line: theme.oscilloscope.line ?? app.accent, line: theme.oscilloscope.line ?? app.accent,
guides, guides,
guidesSecondary: multiplyAlpha(guides, 0.5), guidesSecondary: multiplyAlpha(guides, 0.5),
background: scopes.background, background: theme.oscilloscope.background ?? scopes.background,
fill: theme.oscilloscope.fill ?? 'rgba(245, 248, 252, 0.18)', fill: theme.oscilloscope.fill ?? 'rgba(245, 248, 252, 0.18)',
} }
} }
@@ -1085,8 +1139,8 @@ function resolveVectorscopeTheme(
trace: theme.vectorscope.trace ?? app.accent, trace: theme.vectorscope.trace ?? app.accent,
guides, guides,
guidesSecondary: multiplyAlpha(guides, 0.5), guidesSecondary: multiplyAlpha(guides, 0.5),
labels: theme.vectorscope.guides ? guides : scopes.labels, labels: theme.vectorscope.labels ?? guides,
background: scopes.background, background: theme.vectorscope.background ?? scopes.background,
bandLow: theme.vectorscope.bandLow ?? DEFAULT_BAND_LOW, bandLow: theme.vectorscope.bandLow ?? DEFAULT_BAND_LOW,
bandMid: theme.vectorscope.bandMid ?? DEFAULT_BAND_MID, bandMid: theme.vectorscope.bandMid ?? DEFAULT_BAND_MID,
bandHigh: theme.vectorscope.bandHigh ?? DEFAULT_BAND_HIGH, bandHigh: theme.vectorscope.bandHigh ?? DEFAULT_BAND_HIGH,
@@ -1100,7 +1154,7 @@ function resolveSpectrogramTheme(
): ResolvedSpectrogramTheme { ): ResolvedSpectrogramTheme {
return { return {
mono: theme.spectrogram.mono ?? app.accent, mono: theme.spectrogram.mono ?? app.accent,
background: scopes.background, background: theme.spectrogram.background ?? scopes.background,
heatColors: [ heatColors: [
theme.spectrogram.heatLow ?? DEFAULT_HEAT_LOW, theme.spectrogram.heatLow ?? DEFAULT_HEAT_LOW,
theme.spectrogram.heatMid ?? DEFAULT_HEAT_MID, theme.spectrogram.heatMid ?? DEFAULT_HEAT_MID,
@@ -1121,8 +1175,8 @@ function resolveVUMeterTheme(
peak: theme.vumeter.peak ?? 'rgb(255, 127, 0)', peak: theme.vumeter.peak ?? 'rgb(255, 127, 0)',
clip: theme.vumeter.clip ?? 'rgba(255, 120, 80, 0.9)', clip: theme.vumeter.clip ?? 'rgba(255, 120, 80, 0.9)',
scale: theme.vumeter.scale ?? scopes.guides, scale: theme.vumeter.scale ?? scopes.guides,
labels: blendText(app.text, app.textMuted, 0.35), labels: theme.vumeter.labels ?? blendText(app.text, app.textMuted, 0.35),
background: scopes.background, background: theme.vumeter.background ?? scopes.background,
} }
} }
@@ -1137,8 +1191,8 @@ function resolveLUFSMeterTheme(
track: theme.lufsmeter.track ?? withAlpha(level, 0.08), track: theme.lufsmeter.track ?? withAlpha(level, 0.08),
target: theme.lufsmeter.target ?? withAlpha(level, 0.25), target: theme.lufsmeter.target ?? withAlpha(level, 0.25),
scale: theme.lufsmeter.scale ?? scopes.guides, scale: theme.lufsmeter.scale ?? scopes.guides,
labels: blendText(app.text, app.textMuted, 0.2), labels: theme.lufsmeter.labels ?? blendText(app.text, app.textMuted, 0.2),
background: scopes.background, background: theme.lufsmeter.background ?? scopes.background,
} }
} }
@@ -1152,7 +1206,7 @@ function resolveWaveformTheme(
line: theme.waveform.line ?? app.accent, line: theme.waveform.line ?? app.accent,
guides, guides,
guidesSecondary: multiplyAlpha(guides, 0.5), guidesSecondary: multiplyAlpha(guides, 0.5),
background: scopes.background, background: theme.waveform.background ?? scopes.background,
bandLow: theme.waveform.bandLow ?? DEFAULT_BAND_LOW, bandLow: theme.waveform.bandLow ?? DEFAULT_BAND_LOW,
bandMid: theme.waveform.bandMid ?? DEFAULT_BAND_MID, bandMid: theme.waveform.bandMid ?? DEFAULT_BAND_MID,
bandHigh: theme.waveform.bandHigh ?? DEFAULT_BAND_HIGH, bandHigh: theme.waveform.bandHigh ?? DEFAULT_BAND_HIGH,
+11
View File
@@ -56,6 +56,7 @@ export interface ThemeScopesTokens {
} }
export interface ThemeSpectrumTokens { export interface ThemeSpectrumTokens {
background?: string
line?: string line?: string
sideLine?: string sideLine?: string
fill?: string fill?: string
@@ -64,23 +65,28 @@ export interface ThemeSpectrumTokens {
heatHigh?: string heatHigh?: string
heatBase?: string heatBase?: string
guides?: string guides?: string
labels?: string
} }
export interface ThemeOscilloscopeTokens { export interface ThemeOscilloscopeTokens {
background?: string
line?: string line?: string
fill?: string fill?: string
guides?: string guides?: string
} }
export interface ThemeVectorscopeTokens { export interface ThemeVectorscopeTokens {
background?: string
trace?: string trace?: string
bandLow?: string bandLow?: string
bandMid?: string bandMid?: string
bandHigh?: string bandHigh?: string
guides?: string guides?: string
labels?: string
} }
export interface ThemeSpectrogramTokens { export interface ThemeSpectrogramTokens {
background?: string
mono?: string mono?: string
heatLow?: string heatLow?: string
heatMid?: string heatMid?: string
@@ -88,21 +94,26 @@ export interface ThemeSpectrogramTokens {
} }
export interface ThemeVUMeterTokens { export interface ThemeVUMeterTokens {
background?: string
level?: string level?: string
track?: string track?: string
peak?: string peak?: string
clip?: string clip?: string
scale?: string scale?: string
labels?: string
} }
export interface ThemeLUFSMeterTokens { export interface ThemeLUFSMeterTokens {
background?: string
level?: string level?: string
track?: string track?: string
target?: string target?: string
scale?: string scale?: string
labels?: string
} }
export interface ThemeWaveformTokens { export interface ThemeWaveformTokens {
background?: string
line?: string line?: string
bandLow?: string bandLow?: string
bandMid?: string bandMid?: string