fixes mixdown
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
component
|
||||
display: grid
|
||||
grid-template-rows: 1fr auto
|
||||
grid-template-rows: 1fr auto auto
|
||||
gap: 4px
|
||||
padding: 4px
|
||||
width: 320px
|
||||
@@ -16,7 +16,7 @@ component
|
||||
border: 1px solid rgba(255, 255, 255, 0.1)
|
||||
border-radius: 2px
|
||||
font-family: monospace
|
||||
font-size: 10px
|
||||
font-size: 9px
|
||||
line-height: 1.4
|
||||
padding: 4px
|
||||
outline: none
|
||||
@@ -25,6 +25,21 @@ component
|
||||
&:focus
|
||||
border-color: rgba(255, 255, 255, 0.3)
|
||||
|
||||
> .error
|
||||
display: none
|
||||
font-family: monospace
|
||||
font-size: 9px
|
||||
line-height: 1.3
|
||||
color: #ff6060
|
||||
background: rgba(255, 0, 0, 0.1)
|
||||
border: 1px solid rgba(255, 0, 0, 0.3)
|
||||
border-radius: 2px
|
||||
padding: 3px 4px
|
||||
white-space: pre-wrap
|
||||
word-break: break-word
|
||||
&.visible
|
||||
display: block
|
||||
|
||||
> button
|
||||
background: rgba(255, 255, 255, 0.1)
|
||||
color: #c0c0c0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import css from "./WerkstattDeviceEditor.sass?inline"
|
||||
import defaultCode from "./werkstatt-default.txt?raw"
|
||||
import {DeviceHost, WerkstattDeviceBoxAdapter} from "@opendaw/studio-adapters"
|
||||
import {Lifecycle} from "@opendaw/lib-std"
|
||||
import {Lifecycle, UUID} from "@opendaw/lib-std"
|
||||
import {createElement} from "@opendaw/lib-jsx"
|
||||
import {DeviceEditor} from "@/ui/devices/DeviceEditor.tsx"
|
||||
import {MenuItems} from "@/ui/devices/menu-items.ts"
|
||||
@@ -26,9 +26,18 @@ export const WerkstattDeviceEditor = ({lifecycle, service, adapter, deviceHost}:
|
||||
const storedCode = box.code.getValue()
|
||||
const userCode = storedCode.length > 0 ? WerkstattCompiler.stripHeader(storedCode) : defaultCode
|
||||
const textarea = <textarea spellcheck={false}>{userCode}</textarea> as HTMLTextAreaElement
|
||||
const errorDisplay = <div className="error"/> as HTMLDivElement
|
||||
const runButton = <button onclick={async () => {
|
||||
errorDisplay.textContent = ""
|
||||
errorDisplay.classList.remove("visible")
|
||||
await WerkstattCompiler.compile(service.audioContext, box, textarea.value)
|
||||
}}>Run</button>
|
||||
lifecycle.ownAll(
|
||||
service.engine.subscribeDeviceMessage(UUID.toString(adapter.uuid), message => {
|
||||
errorDisplay.textContent = message
|
||||
errorDisplay.classList.add("visible")
|
||||
})
|
||||
)
|
||||
return (
|
||||
<DeviceEditor lifecycle={lifecycle}
|
||||
project={project}
|
||||
@@ -37,6 +46,7 @@ export const WerkstattDeviceEditor = ({lifecycle, service, adapter, deviceHost}:
|
||||
populateControls={() => (
|
||||
<div className={className}>
|
||||
{textarea}
|
||||
{errorDisplay}
|
||||
{runButton}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,11 +2,12 @@ class Processor {
|
||||
bufferL = new Float32Array(sampleRate * 2)
|
||||
bufferR = new Float32Array(sampleRate * 2)
|
||||
writeHead = 0
|
||||
delaySamples = Math.round(sampleRate * 60 / 120 / 4 * 3)
|
||||
delaySamples = 0
|
||||
feedback = 0.5
|
||||
process({src, out}, {s0, s1}) {
|
||||
process({src, out}, {s0, s1, bpm}) {
|
||||
const [srcL, srcR] = src
|
||||
const [outL, outR] = out
|
||||
this.delaySamples = Math.round(sampleRate * 60 / bpm / 4 * 3)
|
||||
for (let i = s0; i < s1; i++) {
|
||||
const readHead = (this.writeHead - this.delaySamples + this.bufferL.length) % this.bufferL.length
|
||||
const delayedL = this.bufferL[readHead]
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface OfflineEngineRenderConfig {
|
||||
|
||||
export interface OfflineEngineProtocol {
|
||||
initialize(enginePort: MessagePort, config: OfflineEngineInitializeConfig): Promise<void>
|
||||
addModule(code: string): Promise<void>
|
||||
render(config: OfflineEngineRenderConfig): Promise<Float32Array[]>
|
||||
step(samples: number): Promise<Float32Array[]>
|
||||
stop(): void
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface EngineCommands extends Terminable {
|
||||
export interface EngineToClient {
|
||||
log(message: string): void
|
||||
error(reason: unknown): void
|
||||
deviceMessage(uuid: string, message: string): void
|
||||
fetchAudio(uuid: UUID.Bytes): Promise<AudioData>
|
||||
fetchSoundfont(uuid: UUID.Bytes): Promise<SoundFont2>
|
||||
fetchNamWasm(): Promise<ArrayBuffer>
|
||||
|
||||
@@ -139,6 +139,7 @@ export class EngineProcessor extends AudioWorkletProcessor implements EngineCont
|
||||
dispatcher => new class implements EngineToClient {
|
||||
log(message: string): void {dispatcher.dispatchAndForget(this.log, message)}
|
||||
error(error: unknown): void {dispatcher.dispatchAndForget(this.error, error)}
|
||||
deviceMessage(uuid: string, message: string): void {dispatcher.dispatchAndForget(this.deviceMessage, uuid, message)}
|
||||
fetchAudio(uuid: UUID.Bytes): Promise<AudioData> {
|
||||
return dispatcher.dispatchAndReturn(this.fetchAudio, uuid)
|
||||
}
|
||||
|
||||
+42
-11
@@ -1,5 +1,5 @@
|
||||
import {int, isDefined, Option, Terminable, UUID} from "@opendaw/lib-std"
|
||||
import {AudioEffectDeviceAdapter, WerkstattDeviceBoxAdapter} from "@opendaw/studio-adapters"
|
||||
import {int, isDefined, Nullable, Option, Terminable, UUID} from "@opendaw/lib-std"
|
||||
import {AudioEffectDeviceAdapter, EngineToClient, WerkstattDeviceBoxAdapter} from "@opendaw/studio-adapters"
|
||||
import {EngineContext} from "../../EngineContext"
|
||||
import {Block, Processor} from "../../processing"
|
||||
import {PeakBroadcaster} from "../../PeakBroadcaster"
|
||||
@@ -8,12 +8,27 @@ import {AudioBuffer} from "@opendaw/lib-dsp"
|
||||
import {AudioProcessor} from "../../AudioProcessor"
|
||||
|
||||
const HEADER_PATTERN = /^\/\/ @werkstatt (\w+) (\d+) (\d+)\n/
|
||||
const MAX_AMPLITUDE = 1000.0 // ~60dB
|
||||
|
||||
const parseUpdate = (code: string): int => {
|
||||
const match = code.match(HEADER_PATTERN)
|
||||
return match !== null ? parseInt(match[3]) : -1
|
||||
}
|
||||
|
||||
const validateOutput = (channels: ReadonlyArray<Float32Array>, s0: int, s1: int): Nullable<string> => {
|
||||
for (let ch = 0; ch < channels.length; ch++) {
|
||||
const channel = channels[ch]
|
||||
for (let i = s0; i < s1; i++) {
|
||||
const sample = channel[i]
|
||||
if (sample !== sample) {return `NaN detected in output channel ${ch} at sample ${i}`}
|
||||
if (sample > MAX_AMPLITUDE || sample < -MAX_AMPLITUDE) {
|
||||
return `Signal overflow in channel ${ch} at sample ${i} (amplitude: ${sample.toFixed(1)})`
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
interface UserIO {
|
||||
src: ReadonlyArray<Float32Array>
|
||||
out: ReadonlyArray<Float32Array>
|
||||
@@ -29,8 +44,10 @@ export class WerkstattDeviceProcessor extends AudioProcessor implements AudioEff
|
||||
readonly #id: int = WerkstattDeviceProcessor.ID++
|
||||
|
||||
readonly #adapter: WerkstattDeviceBoxAdapter
|
||||
readonly #engineToClient: EngineToClient
|
||||
readonly #output: AudioBuffer
|
||||
readonly #peaks: PeakBroadcaster
|
||||
readonly #uuid: string
|
||||
|
||||
#source: Option<AudioBuffer> = Option.None
|
||||
#userProcessor: Option<UserProcessor> = Option.None
|
||||
@@ -40,8 +57,10 @@ export class WerkstattDeviceProcessor extends AudioProcessor implements AudioEff
|
||||
constructor(context: EngineContext, adapter: WerkstattDeviceBoxAdapter) {
|
||||
super(context)
|
||||
this.#adapter = adapter
|
||||
this.#engineToClient = context.engineToClient
|
||||
this.#output = new AudioBuffer()
|
||||
this.#peaks = this.own(new PeakBroadcaster(context.broadcaster, adapter.address))
|
||||
this.#uuid = UUID.toString(adapter.uuid)
|
||||
this.ownAll(
|
||||
adapter.box.code.catchupAndSubscribe(owner => {
|
||||
const newUpdate = parseUpdate(owner.getValue())
|
||||
@@ -57,8 +76,7 @@ export class WerkstattDeviceProcessor extends AudioProcessor implements AudioEff
|
||||
}
|
||||
|
||||
#tryLoad(update: int): void {
|
||||
const uuid = UUID.toString(this.#adapter.uuid)
|
||||
const registry = (globalThis as any).openDAW?.werkstattProcessors?.[uuid]
|
||||
const registry = (globalThis as any).openDAW?.werkstattProcessors?.[this.#uuid]
|
||||
if (isDefined(registry) && registry.update === update) {
|
||||
this.#swapProcessor(registry.create, update)
|
||||
}
|
||||
@@ -70,11 +88,21 @@ export class WerkstattDeviceProcessor extends AudioProcessor implements AudioEff
|
||||
this.#currentUpdate = update
|
||||
this.#silenced = false
|
||||
} catch (error) {
|
||||
console.error("Werkstatt: failed to instantiate Processor", error)
|
||||
this.#reportError(`Failed to instantiate Processor: ${error}`)
|
||||
this.#silenced = true
|
||||
}
|
||||
}
|
||||
|
||||
#reportError(message: string): void {
|
||||
this.#engineToClient.deviceMessage(this.#uuid, message)
|
||||
}
|
||||
|
||||
#silence(message: string): void {
|
||||
this.#silenced = true
|
||||
this.#output.clear()
|
||||
this.#reportError(message)
|
||||
}
|
||||
|
||||
get incoming(): Processor {return this}
|
||||
get outgoing(): Processor {return this}
|
||||
|
||||
@@ -97,11 +125,9 @@ export class WerkstattDeviceProcessor extends AudioProcessor implements AudioEff
|
||||
|
||||
processAudio(block: Block): void {
|
||||
if (this.#silenced) {
|
||||
const uuid = UUID.toString(this.#adapter.uuid)
|
||||
const registry = (globalThis as any).openDAW?.werkstattProcessors?.[uuid]
|
||||
const expectedUpdate = parseUpdate(this.#adapter.box.code.getValue())
|
||||
if (isDefined(registry) && registry.update === expectedUpdate) {
|
||||
this.#swapProcessor(registry.create, expectedUpdate)
|
||||
if (expectedUpdate > 0 && expectedUpdate !== this.#currentUpdate) {
|
||||
this.#tryLoad(expectedUpdate)
|
||||
}
|
||||
if (this.#silenced) {return}
|
||||
}
|
||||
@@ -115,8 +141,13 @@ export class WerkstattDeviceProcessor extends AudioProcessor implements AudioEff
|
||||
try {
|
||||
proc.process(io, block)
|
||||
} catch (error) {
|
||||
console.error("Werkstatt: runtime error in process()", error)
|
||||
this.#silenced = true
|
||||
this.#silence(`Runtime error: ${error}`)
|
||||
return
|
||||
}
|
||||
const validationError = validateOutput(io.out, block.s0, block.s1)
|
||||
if (validationError !== null) {
|
||||
this.#silence(validationError)
|
||||
return
|
||||
}
|
||||
this.#peaks.process(io.out[0], io.out[1], block.s0, block.s1)
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ Communicator.executor<OfflineEngineProtocol>(
|
||||
running: false
|
||||
})
|
||||
},
|
||||
async addModule(code: string): Promise<void> {
|
||||
new Function(code)()
|
||||
},
|
||||
async step(numSamples: int): Promise<Float32Array[]> {
|
||||
const engine = state.unwrap()
|
||||
const result: Float32Array[] = Arrays.create(() => new Float32Array(numSamples), engine.numberOfChannels)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {int, Nullable, ObservableValue, Observer, Subscription, Terminable, UUID} from "@opendaw/lib-std"
|
||||
import {int, Nullable, ObservableValue, Observer, Procedure, Subscription, Terminable, UUID} from "@opendaw/lib-std"
|
||||
import {AudioData, bpm, ppqn} from "@opendaw/lib-dsp"
|
||||
import {ClipNotification, EnginePreferences, NoteSignal} from "@opendaw/studio-adapters"
|
||||
import {Project} from "./project"
|
||||
@@ -25,6 +25,7 @@ export interface Engine extends Terminable {
|
||||
scheduleClipPlay(clipIds: ReadonlyArray<UUID.Bytes>): void
|
||||
scheduleClipStop(trackIds: ReadonlyArray<UUID.Bytes>): void
|
||||
subscribeClipNotification(observer: Observer<ClipNotification>): Subscription
|
||||
subscribeDeviceMessage(uuid: string, listener: Procedure<string>): Subscription
|
||||
registerMonitoringSource(uuid: UUID.Bytes, node: AudioNode, numChannels: 1 | 2): void
|
||||
unregisterMonitoringSource(uuid: UUID.Bytes): void
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ObservableValue,
|
||||
Observer,
|
||||
Option,
|
||||
Procedure,
|
||||
Subscription,
|
||||
Terminator,
|
||||
UUID
|
||||
@@ -131,6 +132,9 @@ export class EngineFacade implements Engine {
|
||||
scheduleClipStop(trackIds: ReadonlyArray<UUID.Bytes>): void {
|
||||
this.#worklet.unwrap("No worklet to scheduleClipStop").scheduleClipStop(trackIds)
|
||||
}
|
||||
subscribeDeviceMessage(uuid: string, listener: Procedure<string>): Subscription {
|
||||
return this.#worklet.unwrap("No worklet to subscribeDeviceMessage").subscribeDeviceMessage(uuid, listener)
|
||||
}
|
||||
registerMonitoringSource(uuid: UUID.Bytes, node: AudioNode, numChannels: 1 | 2): void {
|
||||
this.#worklet.ifSome(worklet => worklet.registerMonitoringSource(uuid, node, numChannels))
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
ObservableValue,
|
||||
Observer,
|
||||
Option,
|
||||
Procedure,
|
||||
SetMultimap,
|
||||
Subscription,
|
||||
SyncStream,
|
||||
Terminator,
|
||||
@@ -65,6 +67,7 @@ export class EngineWorklet extends AudioWorkletNode implements Engine {
|
||||
readonly #notifyClipNotification: Notifier<ClipNotification>
|
||||
readonly #notifyNoteSignals: Notifier<NoteSignal>
|
||||
readonly #playingClips: Array<UUID.Bytes>
|
||||
readonly #deviceMessageListeners: SetMultimap<string, Procedure<string>> = new SetMultimap()
|
||||
readonly #commands: EngineCommands
|
||||
readonly #isReady: Promise<void>
|
||||
|
||||
@@ -171,6 +174,11 @@ export class EngineWorklet extends AudioWorkletNode implements Engine {
|
||||
Communicator.executor<EngineToClient>(messenger.channel("engine-to-client"), {
|
||||
log: (message: string): void => console.log("WORKLET", message),
|
||||
error: (reason: unknown) => this.dispatchEvent(new ErrorEvent("error", {error: reason})),
|
||||
deviceMessage: (uuid: string, message: string): void => {
|
||||
for (const listener of this.#deviceMessageListeners.get(uuid)) {
|
||||
listener(message)
|
||||
}
|
||||
},
|
||||
ready: (): void => resolve(),
|
||||
fetchAudio: (uuid: UUID.Bytes): Promise<AudioData> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -284,6 +292,11 @@ export class EngineWorklet extends AudioWorkletNode implements Engine {
|
||||
return this.#notifyClipNotification.subscribe(observer)
|
||||
}
|
||||
|
||||
subscribeDeviceMessage(uuid: string, listener: Procedure<string>): Subscription {
|
||||
this.#deviceMessageListeners.add(uuid, listener)
|
||||
return {terminate: () => this.#deviceMessageListeners.remove(uuid, listener)}
|
||||
}
|
||||
|
||||
registerMonitoringSource(uuid: UUID.Bytes, node: AudioNode, numChannels: 1 | 2): void {
|
||||
this.#monitoringSources.set(UUID.toString(uuid), {node, numChannels})
|
||||
this.#rebuildMonitoringMerger()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {DefaultObservableValue, Errors, int, isDefined, Nullable, Option, panic, SyncStream, Terminable, Terminator, TimeSpan, UUID} from "@opendaw/lib-std"
|
||||
import {AudioData, ppqn} from "@opendaw/lib-dsp"
|
||||
import {WerkstattDeviceBox} from "@opendaw/studio-boxes"
|
||||
import {Communicator, Messenger, Wait} from "@opendaw/lib-runtime"
|
||||
import {AnimationFrame} from "@opendaw/lib-dom"
|
||||
import {
|
||||
@@ -46,6 +47,9 @@ export class OfflineEngineRenderer {
|
||||
initialize(enginePort: MessagePort, config: OfflineEngineInitializeConfig): Promise<void> {
|
||||
return dispatcher.dispatchAndReturn(this.initialize, enginePort, config)
|
||||
}
|
||||
addModule(code: string): Promise<void> {
|
||||
return dispatcher.dispatchAndReturn(this.addModule, code)
|
||||
}
|
||||
render(config: OfflineEngineRenderConfig): Promise<Float32Array[]> {
|
||||
return dispatcher.dispatchAndReturn(this.render, config)
|
||||
}
|
||||
@@ -95,7 +99,10 @@ export class OfflineEngineRenderer {
|
||||
return response.arrayBuffer()
|
||||
},
|
||||
notifyClipSequenceChanges: (): void => {},
|
||||
switchMarkerState: (): void => {}
|
||||
switchMarkerState: (): void => {},
|
||||
deviceMessage: (uuid: string, message: string): void => {
|
||||
console.warn(`OFFLINE-ENGINE device(${uuid}): ${message}`)
|
||||
}
|
||||
})
|
||||
|
||||
const engineCommands = Communicator.sender<EngineCommands>(
|
||||
@@ -137,6 +144,30 @@ export class OfflineEngineRenderer {
|
||||
exportConfiguration: optExportConfiguration.unwrapOrUndefined()
|
||||
})
|
||||
|
||||
const HEADER_PATTERN = /^\/\/ @werkstatt (\w+) (\d+) (\d+)\n/
|
||||
for (const box of source.boxGraph.boxes()) {
|
||||
if (box instanceof WerkstattDeviceBox) {
|
||||
const code = box.code.getValue()
|
||||
const match = code.match(HEADER_PATTERN)
|
||||
if (match !== null) {
|
||||
const userCode = code.slice(match[0].length)
|
||||
const update = parseInt(match[3])
|
||||
const uuid = UUID.toString(box.address.uuid)
|
||||
await protocol.addModule(`
|
||||
if (typeof globalThis.openDAW === "undefined") { globalThis.openDAW = {} }
|
||||
if (typeof globalThis.openDAW.werkstattProcessors === "undefined") { globalThis.openDAW.werkstattProcessors = {} }
|
||||
globalThis.openDAW.werkstattProcessors["${uuid}"] = {
|
||||
update: ${update},
|
||||
create: (function werkstatt() {
|
||||
${userCode}
|
||||
return Processor
|
||||
})()
|
||||
}
|
||||
`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
engineCommands.setupMIDI(port, sab)
|
||||
|
||||
return new OfflineEngineRenderer(
|
||||
@@ -163,8 +194,9 @@ export class OfflineEngineRenderer {
|
||||
enabled.setValue(false)
|
||||
boxGraph.endTransaction()
|
||||
const endPosition = source.lastRegionAction()
|
||||
const maxDurationSeconds = source.tempoMap.ppqnToSeconds(endPosition) + 30
|
||||
const renderer = await this.create(source, optExportConfiguration, sampleRate)
|
||||
const result = await renderer.render({}, endPosition, progress, abortSignal)
|
||||
const result = await renderer.render({maxDurationSeconds}, endPosition, progress, abortSignal)
|
||||
boxGraph.beginTransaction()
|
||||
enabled.setValue(wasEnabled)
|
||||
boxGraph.endTransaction()
|
||||
|
||||
Reference in New Issue
Block a user