mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-17 11:11:18 +02:00
improve scope speed and efficiency
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user