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