feat: 3 IA audio effects — Stutter, RingMod, Chorus

- IA Stutter: rhythmic buffer repeat/glitch (rate, depth, chance, mix)
- IA RingMod: ring modulation for metallic/robotic textures (freq, depth, mix)
- IA Chorus: chorus/flanger with modulated delay line (rate, depth, feedback, mix)

Each effect: schema + forge + box + adapter + processor + editor + manual
Registered in: BoxAdapters, DeviceProcessorFactory, DeviceEditorFactory

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kxkm
2026-03-22 14:25:13 +01:00
parent 949508c636
commit 301269e11d
23 changed files with 719 additions and 1 deletions
@@ -0,0 +1,33 @@
# IA Chorus
Chorus and flanger effect for thickening and movement.
---
## 0. Overview
_IA Chorus_ uses a modulated delay line to create chorus, flanger, and doubling effects. An LFO modulates the delay time, producing the characteristic shimmering sound.
---
## 1. Controls
### 1.1 Rate
LFO speed in Hz. Slow (0.1-1Hz) for chorus, fast (1-10Hz) for flanger/vibrato.
### 1.2 Depth
LFO modulation depth. Controls how much the delay time varies.
### 1.3 Feedback
Amount of output fed back into the delay. Higher values create flanger resonance.
### 1.4 Mix
Dry/wet balance.
---
_IA audio effect — openDIAW.be / KXKM_
@@ -0,0 +1,29 @@
# IA RingMod
Ring modulation effect for metallic and robotic textures.
---
## 0. Overview
_IA RingMod_ multiplies the input signal by a sine wave oscillator, producing sum and difference frequencies. Creates robotic, metallic, bell-like, or alien timbres depending on the modulation frequency.
---
## 1. Controls
### 1.1 Frequency
Modulation oscillator frequency in Hz. Low values (20-100Hz) create tremolo. Higher values (200-2000Hz) create metallic tones.
### 1.2 Depth
Amount of ring modulation applied.
### 1.3 Mix
Dry/wet balance.
---
_IA audio effect — openDIAW.be / KXKM_
@@ -0,0 +1,33 @@
# IA Stutter
Rhythmic buffer repeat and glitch effect.
---
## 0. Overview
_IA Stutter_ captures audio into a buffer and repeats it at a configurable rate. A probability control (Chance) determines whether each cycle stutters or passes through. Creates rhythmic glitch patterns and beat-repeat effects.
---
## 1. Controls
### 1.1 Rate
Repeat frequency in Hz. Higher = faster stutters.
### 1.2 Depth
Intensity of the stutter effect.
### 1.3 Chance
Probability (0-100%) that each cycle triggers a stutter. 100% = constant repeat, 50% = half the time.
### 1.4 Mix
Dry/wet balance.
---
_IA audio effect — openDIAW.be / KXKM_
@@ -40,6 +40,9 @@ import {
KokoroTtsDeviceBox,
PiperDeviceBox,
MagentaDeviceBox,
StutterDeviceBox,
RingModDeviceBox,
ChorusDeviceBox,
ZeitgeistDeviceBox
} from "@opendaw/studio-boxes"
import {ArpeggioDeviceEditor} from "@/ui/devices/midi-effects/ArpeggioDeviceEditor.tsx"
@@ -84,6 +87,9 @@ import {
KokoroTtsDeviceBoxAdapter,
PiperDeviceBoxAdapter,
MagentaDeviceBoxAdapter,
StutterDeviceBoxAdapter,
RingModDeviceBoxAdapter,
ChorusDeviceBoxAdapter,
ZeitgeistDeviceBoxAdapter
} from "@opendaw/studio-adapters"
import {DelayDeviceEditor} from "@/ui/devices/audio-effects/DelayDeviceEditor.tsx"
@@ -117,6 +123,9 @@ import {DattorroReverbDeviceEditor} from "@/ui/devices/audio-effects/DattorroRev
import {NeuralAmpDeviceEditor} from "@/ui/devices/audio-effects/NeuralAmpDeviceEditor"
import {WaveshaperDeviceEditor} from "@/ui/devices/audio-effects/WaveshaperDeviceEditor"
import {SpielwerkDeviceEditor} from "@/ui/devices/midi-effects/SpielwerkDeviceEditor"
import {StutterDeviceEditor} from "./audio-effects/StutterDeviceEditor"
import {RingModDeviceEditor} from "./audio-effects/RingModDeviceEditor"
import {ChorusDeviceEditor} from "./audio-effects/ChorusDeviceEditor"
import {WerkstattDeviceEditor} from "@/ui/devices/audio-effects/WerkstattDeviceEditor"
import {DroneDeviceEditor} from "./instruments/DroneDeviceEditor"
import {GrainDeviceEditor} from "./instruments/GrainDeviceEditor"
@@ -377,6 +386,21 @@ export namespace DeviceEditorFactory {
adapter={service.project.boxAdapters.adapterFor(box, WaveshaperDeviceBoxAdapter)}
deviceHost={deviceHost}/>
),
visitStutterDeviceBox: (box: StutterDeviceBox) => (
<StutterDeviceEditor lifecycle={lifecycle} service={service}
adapter={service.project.boxAdapters.adapterFor(box, StutterDeviceBoxAdapter)}
deviceHost={deviceHost}/>
),
visitRingModDeviceBox: (box: RingModDeviceBox) => (
<RingModDeviceEditor lifecycle={lifecycle} service={service}
adapter={service.project.boxAdapters.adapterFor(box, RingModDeviceBoxAdapter)}
deviceHost={deviceHost}/>
),
visitChorusDeviceBox: (box: ChorusDeviceBox) => (
<ChorusDeviceEditor lifecycle={lifecycle} service={service}
adapter={service.project.boxAdapters.adapterFor(box, ChorusDeviceBoxAdapter)}
deviceHost={deviceHost}/>
),
visitWerkstattDeviceBox: (box: WerkstattDeviceBox) => (
<WerkstattDeviceEditor lifecycle={lifecycle}
service={service}
@@ -0,0 +1,4 @@
@use "@/mixins"
component
@include mixins.ControlLayout(1)
@@ -0,0 +1,36 @@
import css from "./ChorusDeviceEditor.sass?inline"
import {Lifecycle} 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"
import {DeviceHost, ChorusDeviceBoxAdapter} from "@opendaw/studio-adapters"
import {ControlBuilder} from "@/ui/devices/ControlBuilder.tsx"
import {DevicePeakMeter} from "@/ui/devices/panel/DevicePeakMeter.tsx"
import {Html} from "@opendaw/lib-dom"
import {IconSymbol} from "@opendaw/studio-enums"
import {StudioService} from "@/service/StudioService"
const className = Html.adoptStyleSheet(css, "ChorusDeviceEditor")
type Construct = { lifecycle: Lifecycle; service: StudioService; adapter: ChorusDeviceBoxAdapter; deviceHost: DeviceHost }
export const ChorusDeviceEditor = ({lifecycle, service, adapter, deviceHost}: Construct) => {
const params = adapter.namedParameter
const {project} = service
const {editing, midiLearning} = project
const knob = (parameter: typeof params[keyof typeof params]) => ControlBuilder.createKnob({lifecycle, editing, midiLearning, adapter: adapter as any, parameter})
return (
<DeviceEditor lifecycle={lifecycle} project={project} adapter={adapter as any}
populateMenu={parent => MenuItems.forAudioUnitInput(parent, service, deviceHost)}
populateControls={() => (
<div className={className}>
{Object.values(params).map(p => knob(p))}
</div>
)}
icon={IconSymbol.Effects}
populateMeter={() => (
<DevicePeakMeter lifecycle={lifecycle} receiver={project.liveStreamReceiver} address={adapter.address}/>
)}
/>
)
}
@@ -0,0 +1,4 @@
@use "@/mixins"
component
@include mixins.ControlLayout(1)
@@ -0,0 +1,36 @@
import css from "./RingModDeviceEditor.sass?inline"
import {Lifecycle} 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"
import {DeviceHost, RingModDeviceBoxAdapter} from "@opendaw/studio-adapters"
import {ControlBuilder} from "@/ui/devices/ControlBuilder.tsx"
import {DevicePeakMeter} from "@/ui/devices/panel/DevicePeakMeter.tsx"
import {Html} from "@opendaw/lib-dom"
import {IconSymbol} from "@opendaw/studio-enums"
import {StudioService} from "@/service/StudioService"
const className = Html.adoptStyleSheet(css, "RingModDeviceEditor")
type Construct = { lifecycle: Lifecycle; service: StudioService; adapter: RingModDeviceBoxAdapter; deviceHost: DeviceHost }
export const RingModDeviceEditor = ({lifecycle, service, adapter, deviceHost}: Construct) => {
const params = adapter.namedParameter
const {project} = service
const {editing, midiLearning} = project
const knob = (parameter: typeof params[keyof typeof params]) => ControlBuilder.createKnob({lifecycle, editing, midiLearning, adapter: adapter as any, parameter})
return (
<DeviceEditor lifecycle={lifecycle} project={project} adapter={adapter as any}
populateMenu={parent => MenuItems.forAudioUnitInput(parent, service, deviceHost)}
populateControls={() => (
<div className={className}>
{Object.values(params).map(p => knob(p))}
</div>
)}
icon={IconSymbol.Effects}
populateMeter={() => (
<DevicePeakMeter lifecycle={lifecycle} receiver={project.liveStreamReceiver} address={adapter.address}/>
)}
/>
)
}
@@ -0,0 +1,4 @@
@use "@/mixins"
component
@include mixins.ControlLayout(1)
@@ -0,0 +1,36 @@
import css from "./StutterDeviceEditor.sass?inline"
import {Lifecycle} 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"
import {DeviceHost, StutterDeviceBoxAdapter} from "@opendaw/studio-adapters"
import {ControlBuilder} from "@/ui/devices/ControlBuilder.tsx"
import {DevicePeakMeter} from "@/ui/devices/panel/DevicePeakMeter.tsx"
import {Html} from "@opendaw/lib-dom"
import {IconSymbol} from "@opendaw/studio-enums"
import {StudioService} from "@/service/StudioService"
const className = Html.adoptStyleSheet(css, "StutterDeviceEditor")
type Construct = { lifecycle: Lifecycle; service: StudioService; adapter: StutterDeviceBoxAdapter; deviceHost: DeviceHost }
export const StutterDeviceEditor = ({lifecycle, service, adapter, deviceHost}: Construct) => {
const params = adapter.namedParameter
const {project} = service
const {editing, midiLearning} = project
const knob = (parameter: typeof params[keyof typeof params]) => ControlBuilder.createKnob({lifecycle, editing, midiLearning, adapter: adapter as any, parameter})
return (
<DeviceEditor lifecycle={lifecycle} project={project} adapter={adapter as any}
populateMenu={parent => MenuItems.forAudioUnitInput(parent, service, deviceHost)}
populateControls={() => (
<div className={className}>
{Object.values(params).map(p => knob(p))}
</div>
)}
icon={IconSymbol.Effects}
populateMeter={() => (
<DevicePeakMeter lifecycle={lifecycle} receiver={project.liveStreamReceiver} address={adapter.address}/>
)}
/>
)
}
@@ -86,6 +86,9 @@ import {
AceStepDeviceBox,
KokoroTtsDeviceBox,
PiperDeviceBox,
StutterDeviceBox,
RingModDeviceBox,
ChorusDeviceBox,
ZeitgeistDeviceBox
} from "@opendaw/studio-boxes"
import {AudioUnitBoxAdapter} from "./audio-unit/AudioUnitBoxAdapter"
@@ -139,6 +142,9 @@ import {MagentaDeviceBoxAdapter} from "./devices/instruments/MagentaDeviceBoxAda
import {AceStepDeviceBoxAdapter} from "./devices/instruments/AceStepDeviceBoxAdapter"
import {KokoroTtsDeviceBoxAdapter} from "./devices/instruments/KokoroTtsDeviceBoxAdapter"
import {PiperDeviceBoxAdapter} from "./devices/instruments/PiperDeviceBoxAdapter"
import {StutterDeviceBoxAdapter} from "./devices/audio-effects/StutterDeviceBoxAdapter"
import {RingModDeviceBoxAdapter} from "./devices/audio-effects/RingModDeviceBoxAdapter"
import {ChorusDeviceBoxAdapter} from "./devices/audio-effects/ChorusDeviceBoxAdapter"
import {ZeitgeistDeviceBoxAdapter} from "./devices/midi-effects/ZeitgeistDeviceBoxAdapter"
import {GrooveShuffleBoxAdapter} from "./grooves/GrooveShuffleBoxAdapter"
import {UnknownAudioEffectDeviceBoxAdapter} from "./devices/audio-effects/UnknownAudioEffectDeviceBoxAdapter"
@@ -289,6 +295,9 @@ export class BoxAdapters implements Terminable {
visitAceStepDeviceBox: (box: AceStepDeviceBox) => new AceStepDeviceBoxAdapter(this.#context, box),
visitKokoroTtsDeviceBox: (box: KokoroTtsDeviceBox) => new KokoroTtsDeviceBoxAdapter(this.#context, box),
visitPiperDeviceBox: (box: PiperDeviceBox) => new PiperDeviceBoxAdapter(this.#context, box),
visitStutterDeviceBox: (box: StutterDeviceBox) => new StutterDeviceBoxAdapter(this.#context, box),
visitRingModDeviceBox: (box: RingModDeviceBox) => new RingModDeviceBoxAdapter(this.#context, box),
visitChorusDeviceBox: (box: ChorusDeviceBox) => new ChorusDeviceBoxAdapter(this.#context, box),
visitZeitgeistDeviceBox: (box: ZeitgeistDeviceBox) => new ZeitgeistDeviceBoxAdapter(this.#context, box)
}), `Could not find factory for ${unknownBox}`)
}
@@ -25,6 +25,9 @@ export namespace DeviceManualUrls {
export const NeuralAmp = "manuals/devices/audio/neural-amp"
export const Waveshaper = "manuals/devices/audio/waveshaper"
export const Werkstatt = "manuals/devices/audio/werkstatt"
export const Stutter = "manuals/devices/audio/stutter"
export const RingMod = "manuals/devices/audio/ringmod"
export const Chorus = "manuals/devices/audio/chorus"
// Instruments
export const Apparat = "manuals/devices/instruments/apparat"
@@ -0,0 +1,50 @@
import {Option, StringMapping, UUID, ValueMapping} from "@opendaw/lib-std"
import {ChorusDeviceBox} from "@opendaw/studio-boxes"
import {Address, BooleanField, Int32Field, PointerField, StringField} from "@opendaw/lib-box"
import {Pointers} from "@opendaw/studio-enums"
import {AudioEffectDeviceAdapter, DeviceHost, Devices} from "../../DeviceAdapter"
import {LabeledAudioOutput} from "../../LabeledAudioOutputsOwner"
import {BoxAdaptersContext} from "../../BoxAdaptersContext"
import {DeviceManualUrls} from "../../DeviceManualUrls"
import {ParameterAdapterSet} from "../../ParameterAdapterSet"
import {AudioUnitBoxAdapter} from "../../audio-unit/AudioUnitBoxAdapter"
export class ChorusDeviceBoxAdapter implements AudioEffectDeviceAdapter {
readonly type = "audio-effect"
readonly accepts = "audio"
readonly manualUrl = DeviceManualUrls.Chorus
readonly #context: BoxAdaptersContext
readonly #box: ChorusDeviceBox
readonly #parametric: ParameterAdapterSet
readonly namedParameter
constructor(context: BoxAdaptersContext, box: ChorusDeviceBox) {
this.#context = context
this.#box = box
this.#parametric = new ParameterAdapterSet(this.#context)
this.namedParameter = this.#wrapParameters(box)
}
get box(): ChorusDeviceBox {return this.#box}
get uuid(): UUID.Bytes {return this.#box.address.uuid}
get address(): Address {return this.#box.address}
get indexField(): Int32Field {return this.#box.index}
get labelField(): StringField {return this.#box.label}
get enabledField(): BooleanField {return this.#box.enabled}
get minimizedField(): BooleanField {return this.#box.minimized}
get host(): PointerField<Pointers.AudioEffectHost> {return this.#box.host}
deviceHost(): DeviceHost {
return this.#context.boxAdapters.adapterFor(this.#box.host.targetVertex.unwrap("no device-host").box, Devices.isHost)
}
audioUnitBoxAdapter(): AudioUnitBoxAdapter {return this.deviceHost().audioUnitBoxAdapter()}
*labeledAudioOutputs(): Iterable<LabeledAudioOutput> {
yield {address: this.address, label: this.labelField.getValue(), children: () => Option.None}
}
terminate(): void {this.#parametric.terminate()}
#wrapParameters(box: import("@opendaw/studio-boxes").ChorusDeviceBox) {
return {
rate: this.#parametric.createParameter(box.rate, ValueMapping.exponential(0.1, 10.0), StringMapping.numeric({unit: "Hz", fractionDigits: 1}), "Rate"),
depth: this.#parametric.createParameter(box.depth, ValueMapping.unipolar(), StringMapping.percent({fractionDigits: 0}), "Depth"),
feedback: this.#parametric.createParameter(box.feedback, ValueMapping.unipolar(), StringMapping.percent({fractionDigits: 0}), "Feedback"),
mix: this.#parametric.createParameter(box.mix, ValueMapping.unipolar(), StringMapping.percent({fractionDigits: 0}), "Mix"),
} as const
}
}
@@ -0,0 +1,49 @@
import {Option, StringMapping, UUID, ValueMapping} from "@opendaw/lib-std"
import {RingModDeviceBox} from "@opendaw/studio-boxes"
import {Address, BooleanField, Int32Field, PointerField, StringField} from "@opendaw/lib-box"
import {Pointers} from "@opendaw/studio-enums"
import {AudioEffectDeviceAdapter, DeviceHost, Devices} from "../../DeviceAdapter"
import {LabeledAudioOutput} from "../../LabeledAudioOutputsOwner"
import {BoxAdaptersContext} from "../../BoxAdaptersContext"
import {DeviceManualUrls} from "../../DeviceManualUrls"
import {ParameterAdapterSet} from "../../ParameterAdapterSet"
import {AudioUnitBoxAdapter} from "../../audio-unit/AudioUnitBoxAdapter"
export class RingModDeviceBoxAdapter implements AudioEffectDeviceAdapter {
readonly type = "audio-effect"
readonly accepts = "audio"
readonly manualUrl = DeviceManualUrls.RingMod
readonly #context: BoxAdaptersContext
readonly #box: RingModDeviceBox
readonly #parametric: ParameterAdapterSet
readonly namedParameter
constructor(context: BoxAdaptersContext, box: RingModDeviceBox) {
this.#context = context
this.#box = box
this.#parametric = new ParameterAdapterSet(this.#context)
this.namedParameter = this.#wrapParameters(box)
}
get box(): RingModDeviceBox {return this.#box}
get uuid(): UUID.Bytes {return this.#box.address.uuid}
get address(): Address {return this.#box.address}
get indexField(): Int32Field {return this.#box.index}
get labelField(): StringField {return this.#box.label}
get enabledField(): BooleanField {return this.#box.enabled}
get minimizedField(): BooleanField {return this.#box.minimized}
get host(): PointerField<Pointers.AudioEffectHost> {return this.#box.host}
deviceHost(): DeviceHost {
return this.#context.boxAdapters.adapterFor(this.#box.host.targetVertex.unwrap("no device-host").box, Devices.isHost)
}
audioUnitBoxAdapter(): AudioUnitBoxAdapter {return this.deviceHost().audioUnitBoxAdapter()}
*labeledAudioOutputs(): Iterable<LabeledAudioOutput> {
yield {address: this.address, label: this.labelField.getValue(), children: () => Option.None}
}
terminate(): void {this.#parametric.terminate()}
#wrapParameters(box: import("@opendaw/studio-boxes").RingModDeviceBox) {
return {
frequency: this.#parametric.createParameter(box.frequency, ValueMapping.exponential(20.0, 5000.0), StringMapping.numeric({unit: "Hz", fractionDigits: 0, unitPrefix: true}), "Freq"),
depth: this.#parametric.createParameter(box.depth, ValueMapping.unipolar(), StringMapping.percent({fractionDigits: 0}), "Depth"),
mix: this.#parametric.createParameter(box.mix, ValueMapping.unipolar(), StringMapping.percent({fractionDigits: 0}), "Mix"),
} as const
}
}
@@ -0,0 +1,50 @@
import {Option, StringMapping, UUID, ValueMapping} from "@opendaw/lib-std"
import {StutterDeviceBox} from "@opendaw/studio-boxes"
import {Address, BooleanField, Int32Field, PointerField, StringField} from "@opendaw/lib-box"
import {Pointers} from "@opendaw/studio-enums"
import {AudioEffectDeviceAdapter, DeviceHost, Devices} from "../../DeviceAdapter"
import {LabeledAudioOutput} from "../../LabeledAudioOutputsOwner"
import {BoxAdaptersContext} from "../../BoxAdaptersContext"
import {DeviceManualUrls} from "../../DeviceManualUrls"
import {ParameterAdapterSet} from "../../ParameterAdapterSet"
import {AudioUnitBoxAdapter} from "../../audio-unit/AudioUnitBoxAdapter"
export class StutterDeviceBoxAdapter implements AudioEffectDeviceAdapter {
readonly type = "audio-effect"
readonly accepts = "audio"
readonly manualUrl = DeviceManualUrls.Stutter
readonly #context: BoxAdaptersContext
readonly #box: StutterDeviceBox
readonly #parametric: ParameterAdapterSet
readonly namedParameter
constructor(context: BoxAdaptersContext, box: StutterDeviceBox) {
this.#context = context
this.#box = box
this.#parametric = new ParameterAdapterSet(this.#context)
this.namedParameter = this.#wrapParameters(box)
}
get box(): StutterDeviceBox {return this.#box}
get uuid(): UUID.Bytes {return this.#box.address.uuid}
get address(): Address {return this.#box.address}
get indexField(): Int32Field {return this.#box.index}
get labelField(): StringField {return this.#box.label}
get enabledField(): BooleanField {return this.#box.enabled}
get minimizedField(): BooleanField {return this.#box.minimized}
get host(): PointerField<Pointers.AudioEffectHost> {return this.#box.host}
deviceHost(): DeviceHost {
return this.#context.boxAdapters.adapterFor(this.#box.host.targetVertex.unwrap("no device-host").box, Devices.isHost)
}
audioUnitBoxAdapter(): AudioUnitBoxAdapter {return this.deviceHost().audioUnitBoxAdapter()}
*labeledAudioOutputs(): Iterable<LabeledAudioOutput> {
yield {address: this.address, label: this.labelField.getValue(), children: () => Option.None}
}
terminate(): void {this.#parametric.terminate()}
#wrapParameters(box: import("@opendaw/studio-boxes").StutterDeviceBox) {
return {
rate: this.#parametric.createParameter(box.rate, ValueMapping.exponential(1.0, 64.0), StringMapping.numeric({unit: "Hz", fractionDigits: 1}), "Rate"),
depth: this.#parametric.createParameter(box.depth, ValueMapping.unipolar(), StringMapping.percent({fractionDigits: 0}), "Depth"),
chance: this.#parametric.createParameter(box.chance, ValueMapping.unipolar(), StringMapping.percent({fractionDigits: 0}), "Chance"),
mix: this.#parametric.createParameter(box.mix, ValueMapping.unipolar(), StringMapping.percent({fractionDigits: 0}), "Mix"),
} as const
}
}
@@ -38,6 +38,9 @@ import {
KokoroTtsDeviceBox,
PiperDeviceBox,
MagentaDeviceBox,
StutterDeviceBox,
RingModDeviceBox,
ChorusDeviceBox,
ZeitgeistDeviceBox
} from "@opendaw/studio-boxes"
import {DelayDeviceProcessor} from "./devices/audio-effects/DelayDeviceProcessor"
@@ -80,6 +83,9 @@ import {
KokoroTtsDeviceBoxAdapter,
PiperDeviceBoxAdapter,
MagentaDeviceBoxAdapter,
StutterDeviceBoxAdapter,
RingModDeviceBoxAdapter,
ChorusDeviceBoxAdapter,
ZeitgeistDeviceBoxAdapter
} from "@opendaw/studio-adapters"
import {NopDeviceProcessor} from "./devices/audio-effects/NopDeviceProcessor"
@@ -113,6 +119,9 @@ import {TidalDeviceProcessor} from "./devices/audio-effects/TidalDeviceProcessor
import {DattorroReverbDeviceProcessor} from "./devices/audio-effects/DattorroReverbDeviceProcessor"
import {NeuralAmpDeviceProcessor} from "./devices/audio-effects/NeuralAmpDeviceProcessor"
import {WaveshaperDeviceProcessor} from "./devices/audio-effects/WaveshaperDeviceProcessor"
import {StutterDeviceProcessor} from "./devices/audio-effects/StutterDeviceProcessor"
import {RingModDeviceProcessor} from "./devices/audio-effects/RingModDeviceProcessor"
import {ChorusDeviceProcessor} from "./devices/audio-effects/ChorusDeviceProcessor"
import {WerkstattDeviceProcessor} from "./devices/audio-effects/WerkstattDeviceProcessor"
import {SpielwerkDeviceProcessor} from "./devices/midi-effects/SpielwerkDeviceProcessor"
import {ApparatDeviceProcessor} from "./devices/instruments/ApparatDeviceProcessor"
@@ -220,6 +229,12 @@ export namespace AudioEffectDeviceProcessorFactory {
new NeuralAmpDeviceProcessor(context, context.boxAdapters.adapterFor(box, NeuralAmpDeviceBoxAdapter)),
visitWaveshaperDeviceBox: (box: WaveshaperDeviceBox): AudioEffectDeviceProcessor =>
new WaveshaperDeviceProcessor(context, context.boxAdapters.adapterFor(box, WaveshaperDeviceBoxAdapter)),
visitStutterDeviceBox: (box: StutterDeviceBox): AudioEffectDeviceProcessor =>
new StutterDeviceProcessor(context, context.boxAdapters.adapterFor(box, StutterDeviceBoxAdapter)),
visitRingModDeviceBox: (box: RingModDeviceBox): AudioEffectDeviceProcessor =>
new RingModDeviceProcessor(context, context.boxAdapters.adapterFor(box, RingModDeviceBoxAdapter)),
visitChorusDeviceBox: (box: ChorusDeviceBox): AudioEffectDeviceProcessor =>
new ChorusDeviceProcessor(context, context.boxAdapters.adapterFor(box, ChorusDeviceBoxAdapter)),
visitWerkstattDeviceBox: (box: WerkstattDeviceBox): AudioEffectDeviceProcessor =>
new WerkstattDeviceProcessor(context, context.boxAdapters.adapterFor(box, WerkstattDeviceBoxAdapter))
}), `Could not create audio-effect for'${box.name}'`)
@@ -0,0 +1,80 @@
import {int, Option, Terminable, UUID} from "@opendaw/lib-std"
import {AudioBuffer} from "@opendaw/lib-dsp"
import {AudioEffectDeviceAdapter, ChorusDeviceBoxAdapter} from "@opendaw/studio-adapters"
import {EngineContext} from "../../EngineContext"
import {Block, Processor} from "../../processing"
import {PeakBroadcaster} from "../../PeakBroadcaster"
import {AutomatableParameter} from "../../AutomatableParameter"
import {AudioEffectDeviceProcessor} from "../../AudioEffectDeviceProcessor"
import {AudioProcessor} from "../../AudioProcessor"
const TWO_PI = Math.PI * 2
const MAX_DELAY = 4410 // 100ms at 44100
export class ChorusDeviceProcessor extends AudioProcessor implements AudioEffectDeviceProcessor {
static ID: int = 0 | 0
readonly #id: int = ChorusDeviceProcessor.ID++
readonly #adapter: ChorusDeviceBoxAdapter
readonly #output: AudioBuffer
readonly #peaks: PeakBroadcaster
readonly #delayL = new Float32Array(MAX_DELAY)
readonly #delayR = new Float32Array(MAX_DELAY)
#writePos: int = 0
#lfoPhase = 0.0
#source: Option<AudioBuffer> = Option.None
rate = 1.5; depth = 0.5; feedback = 0.3; mix = 0.5
readonly paramRate: AutomatableParameter<number>
readonly paramDepth: AutomatableParameter<number>
readonly paramFeedback: AutomatableParameter<number>
readonly paramMix: AutomatableParameter<number>
constructor(context: EngineContext, adapter: ChorusDeviceBoxAdapter) {
super(context)
this.#adapter = adapter
this.#output = new AudioBuffer()
this.#peaks = this.own(new PeakBroadcaster(context.broadcaster, adapter.address))
this.paramRate = this.own(this.bindParameter(adapter.namedParameter.rate))
this.paramDepth = this.own(this.bindParameter(adapter.namedParameter.depth))
this.paramFeedback = this.own(this.bindParameter(adapter.namedParameter.feedback))
this.paramMix = this.own(this.bindParameter(adapter.namedParameter.mix))
this.ownAll(context.registerProcessor(this), context.audioOutputBufferRegistry.register(adapter.address, this.#output, this.outgoing))
this.readAllParameters()
}
get incoming(): Processor {return this}
get outgoing(): Processor {return this}
reset(): void {this.#peaks.clear(); this.#output.clear(); this.eventInput.clear(); this.#delayL.fill(0); this.#delayR.fill(0); this.#writePos = 0; this.#lfoPhase = 0}
get uuid(): UUID.Bytes {return this.#adapter.uuid}
get audioOutput(): AudioBuffer {return this.#output}
setAudioSource(source: AudioBuffer): Terminable {this.#source = Option.wrap(source); return {terminate: () => this.#source = Option.None}}
index(): int {return this.#adapter.indexField.getValue()}
adapter(): AudioEffectDeviceAdapter {return this.#adapter}
processAudio({s0, s1}: Block): void {
if (this.#source.isEmpty()) return
const [inL, inR] = this.#source.unwrap().channels()
const [outL, outR] = this.#output.channels()
const lfoInc = (this.rate * TWO_PI) / sampleRate
const maxDelay = MAX_DELAY - 1
for (let i = s0; i < s1; i++) {
const lfo = (0.5 + 0.5 * Math.sin(this.#lfoPhase)) * this.depth
const delaySamples = 1 + lfo * (maxDelay * 0.5)
const readPos = (this.#writePos - delaySamples + MAX_DELAY) % MAX_DELAY
const idx = Math.floor(readPos)
const frac = readPos - idx
const delL = this.#delayL[idx] * (1 - frac) + this.#delayL[(idx + 1) % MAX_DELAY] * frac
const delR = this.#delayR[idx] * (1 - frac) + this.#delayR[(idx + 1) % MAX_DELAY] * frac
this.#delayL[this.#writePos] = inL[i] + delL * this.feedback
this.#delayR[this.#writePos] = inR[i] + delR * this.feedback
this.#writePos = (this.#writePos + 1) % MAX_DELAY
outL[i] = inL[i] * (1 - this.mix) + delL * this.mix
outR[i] = inR[i] * (1 - this.mix) + delR * this.mix
this.#lfoPhase = (this.#lfoPhase + lfoInc) % TWO_PI
}
this.#peaks.process(outL, outR, s0, s1)
}
parameterChanged(p: AutomatableParameter): void {
if (p === this.paramRate) this.rate = this.paramRate.getValue()
else if (p === this.paramDepth) this.depth = this.paramDepth.getValue()
else if (p === this.paramFeedback) this.feedback = this.paramFeedback.getValue()
else if (p === this.paramMix) this.mix = this.paramMix.getValue()
}
toString(): string {return `{ChorusDevice (${this.#id})}`}
}
@@ -0,0 +1,65 @@
import {int, Option, Terminable, UUID} from "@opendaw/lib-std"
import {AudioBuffer} from "@opendaw/lib-dsp"
import {AudioEffectDeviceAdapter, RingModDeviceBoxAdapter} from "@opendaw/studio-adapters"
import {EngineContext} from "../../EngineContext"
import {Block, Processor} from "../../processing"
import {PeakBroadcaster} from "../../PeakBroadcaster"
import {AutomatableParameter} from "../../AutomatableParameter"
import {AudioEffectDeviceProcessor} from "../../AudioEffectDeviceProcessor"
import {AudioProcessor} from "../../AudioProcessor"
const TWO_PI = Math.PI * 2
export class RingModDeviceProcessor extends AudioProcessor implements AudioEffectDeviceProcessor {
static ID: int = 0 | 0
readonly #id: int = RingModDeviceProcessor.ID++
readonly #adapter: RingModDeviceBoxAdapter
readonly #output: AudioBuffer
readonly #peaks: PeakBroadcaster
#phase = 0.0
#source: Option<AudioBuffer> = Option.None
freq = 440.0; depth = 1.0; mix = 0.5
readonly paramFreq: AutomatableParameter<number>
readonly paramDepth: AutomatableParameter<number>
readonly paramMix: AutomatableParameter<number>
constructor(context: EngineContext, adapter: RingModDeviceBoxAdapter) {
super(context)
this.#adapter = adapter
this.#output = new AudioBuffer()
this.#peaks = this.own(new PeakBroadcaster(context.broadcaster, adapter.address))
this.paramFreq = this.own(this.bindParameter(adapter.namedParameter.frequency))
this.paramDepth = this.own(this.bindParameter(adapter.namedParameter.depth))
this.paramMix = this.own(this.bindParameter(adapter.namedParameter.mix))
this.ownAll(context.registerProcessor(this), context.audioOutputBufferRegistry.register(adapter.address, this.#output, this.outgoing))
this.readAllParameters()
}
get incoming(): Processor {return this}
get outgoing(): Processor {return this}
reset(): void {this.#peaks.clear(); this.#output.clear(); this.eventInput.clear(); this.#phase = 0}
get uuid(): UUID.Bytes {return this.#adapter.uuid}
get audioOutput(): AudioBuffer {return this.#output}
setAudioSource(source: AudioBuffer): Terminable {this.#source = Option.wrap(source); return {terminate: () => this.#source = Option.None}}
index(): int {return this.#adapter.indexField.getValue()}
adapter(): AudioEffectDeviceAdapter {return this.#adapter}
processAudio({s0, s1}: Block): void {
if (this.#source.isEmpty()) return
const [inL, inR] = this.#source.unwrap().channels()
const [outL, outR] = this.#output.channels()
const phaseInc = (this.freq * TWO_PI) / sampleRate
for (let i = s0; i < s1; i++) {
const mod = Math.sin(this.#phase) * this.depth
const wetL = inL[i] * mod
const wetR = inR[i] * mod
outL[i] = inL[i] * (1 - this.mix) + wetL * this.mix
outR[i] = inR[i] * (1 - this.mix) + wetR * this.mix
this.#phase = (this.#phase + phaseInc) % TWO_PI
}
this.#peaks.process(outL, outR, s0, s1)
}
parameterChanged(p: AutomatableParameter): void {
if (p === this.paramFreq) this.freq = this.paramFreq.getValue()
else if (p === this.paramDepth) this.depth = this.paramDepth.getValue()
else if (p === this.paramMix) this.mix = this.paramMix.getValue()
}
toString(): string {return `{RingModDevice (${this.#id})}`}
}
@@ -0,0 +1,87 @@
import {int, Option, Terminable, UUID} from "@opendaw/lib-std"
import {AudioBuffer} from "@opendaw/lib-dsp"
import {AudioEffectDeviceAdapter, StutterDeviceBoxAdapter} from "@opendaw/studio-adapters"
import {EngineContext} from "../../EngineContext"
import {Block, Processor} from "../../processing"
import {PeakBroadcaster} from "../../PeakBroadcaster"
import {AutomatableParameter} from "../../AutomatableParameter"
import {AudioEffectDeviceProcessor} from "../../AudioEffectDeviceProcessor"
import {AudioProcessor} from "../../AudioProcessor"
const MAX_BUF = 48000
export class StutterDeviceProcessor extends AudioProcessor implements AudioEffectDeviceProcessor {
static ID: int = 0 | 0
readonly #id: int = StutterDeviceProcessor.ID++
readonly #adapter: StutterDeviceBoxAdapter
readonly #output: AudioBuffer
readonly #peaks: PeakBroadcaster
readonly #bufL = new Float32Array(MAX_BUF)
readonly #bufR = new Float32Array(MAX_BUF)
#writePos: int = 0
#readPos: number = 0
#timer: number = 0
#stuttering = false
#source: Option<AudioBuffer> = Option.None
rate = 8.0; depth = 1.0; chance = 0.5; mix = 1.0
readonly paramRate: AutomatableParameter<number>
readonly paramDepth: AutomatableParameter<number>
readonly paramChance: AutomatableParameter<number>
readonly paramMix: AutomatableParameter<number>
constructor(context: EngineContext, adapter: StutterDeviceBoxAdapter) {
super(context)
this.#adapter = adapter
this.#output = new AudioBuffer()
this.#peaks = this.own(new PeakBroadcaster(context.broadcaster, adapter.address))
this.paramRate = this.own(this.bindParameter(adapter.namedParameter.rate))
this.paramDepth = this.own(this.bindParameter(adapter.namedParameter.depth))
this.paramChance = this.own(this.bindParameter(adapter.namedParameter.chance))
this.paramMix = this.own(this.bindParameter(adapter.namedParameter.mix))
this.ownAll(context.registerProcessor(this), context.audioOutputBufferRegistry.register(adapter.address, this.#output, this.outgoing))
this.readAllParameters()
}
get incoming(): Processor {return this}
get outgoing(): Processor {return this}
reset(): void {this.#peaks.clear(); this.#output.clear(); this.eventInput.clear(); this.#writePos = 0; this.#readPos = 0; this.#timer = 0; this.#stuttering = false}
get uuid(): UUID.Bytes {return this.#adapter.uuid}
get audioOutput(): AudioBuffer {return this.#output}
setAudioSource(source: AudioBuffer): Terminable {this.#source = Option.wrap(source); return {terminate: () => this.#source = Option.None}}
index(): int {return this.#adapter.indexField.getValue()}
adapter(): AudioEffectDeviceAdapter {return this.#adapter}
processAudio({s0, s1}: Block): void {
if (this.#source.isEmpty()) return
const [inL, inR] = this.#source.unwrap().channels()
const [outL, outR] = this.#output.channels()
const interval = sampleRate / this.rate
const bufSize = Math.min(MAX_BUF, Math.floor(interval))
for (let i = s0; i < s1; i++) {
this.#bufL[this.#writePos] = inL[i]
this.#bufR[this.#writePos] = inR[i]
this.#writePos = (this.#writePos + 1) % bufSize
this.#timer++
if (this.#timer >= interval) {
this.#timer = 0
this.#stuttering = Math.random() < this.chance
this.#readPos = 0
}
if (this.#stuttering) {
const idx = Math.floor(this.#readPos) % bufSize
const wet = this.depth
outL[i] = inL[i] * (1 - this.mix) + this.#bufL[idx] * wet * this.mix
outR[i] = inR[i] * (1 - this.mix) + this.#bufR[idx] * wet * this.mix
this.#readPos++
} else {
outL[i] = inL[i]
outR[i] = inR[i]
}
}
this.#peaks.process(outL, outR, s0, s1)
}
parameterChanged(p: AutomatableParameter): void {
if (p === this.paramRate) this.rate = this.paramRate.getValue()
else if (p === this.paramDepth) this.depth = this.paramDepth.getValue()
else if (p === this.paramChance) this.chance = this.paramChance.getValue()
else if (p === this.paramMix) this.mix = this.paramMix.getValue()
}
toString(): string {return `{StutterDevice (${this.#id})}`}
}
@@ -0,0 +1,23 @@
import {BoxSchema} from "@opendaw/lib-box-forge"
import {Pointers} from "@opendaw/studio-enums"
import {ParameterPointerRules} from "../../std/Defaults"
import {DeviceFactory} from "../../std/DeviceFactory"
export const ChorusDeviceBox: BoxSchema<Pointers> = DeviceFactory.createAudioEffect("ChorusDeviceBox", {
10: {
type: "float32", name: "rate", pointerRules: ParameterPointerRules,
value: 1.5, constraints: {"min": 0.1, "max": 10.0, "scaling": "exponential"}, unit: "Hz"
},
11: {
type: "float32", name: "depth", pointerRules: ParameterPointerRules,
value: 0.5, constraints: "unipolar", unit: "%"
},
12: {
type: "float32", name: "feedback", pointerRules: ParameterPointerRules,
value: 0.3, constraints: "unipolar", unit: "%"
},
13: {
type: "float32", name: "mix", pointerRules: ParameterPointerRules,
value: 0.5, constraints: "unipolar", unit: "%"
}
})
@@ -0,0 +1,19 @@
import {BoxSchema} from "@opendaw/lib-box-forge"
import {Pointers} from "@opendaw/studio-enums"
import {ParameterPointerRules} from "../../std/Defaults"
import {DeviceFactory} from "../../std/DeviceFactory"
export const RingModDeviceBox: BoxSchema<Pointers> = DeviceFactory.createAudioEffect("RingModDeviceBox", {
10: {
type: "float32", name: "frequency", pointerRules: ParameterPointerRules,
value: 440.0, constraints: {"min": 20.0, "max": 5000.0, "scaling": "exponential"}, unit: "Hz"
},
11: {
type: "float32", name: "depth", pointerRules: ParameterPointerRules,
value: 1.0, constraints: "unipolar", unit: "%"
},
12: {
type: "float32", name: "mix", pointerRules: ParameterPointerRules,
value: 0.5, constraints: "unipolar", unit: "%"
}
})
@@ -0,0 +1,23 @@
import {BoxSchema} from "@opendaw/lib-box-forge"
import {Pointers} from "@opendaw/studio-enums"
import {ParameterPointerRules} from "../../std/Defaults"
import {DeviceFactory} from "../../std/DeviceFactory"
export const StutterDeviceBox: BoxSchema<Pointers> = DeviceFactory.createAudioEffect("StutterDeviceBox", {
10: {
type: "float32", name: "rate", pointerRules: ParameterPointerRules,
value: 8.0, constraints: {"min": 1.0, "max": 64.0, "scaling": "exponential"}, unit: "Hz"
},
11: {
type: "float32", name: "depth", pointerRules: ParameterPointerRules,
value: 1.0, constraints: "unipolar", unit: "%"
},
12: {
type: "float32", name: "chance", pointerRules: ParameterPointerRules,
value: 0.5, constraints: "unipolar", unit: "%"
},
13: {
type: "float32", name: "mix", pointerRules: ParameterPointerRules,
value: 1.0, constraints: "unipolar", unit: "%"
}
})
@@ -30,6 +30,9 @@ import {WerkstattDeviceBox} from "./audio-effects/WerkstattDeviceBox"
import {WerkstattParameterBox} from "./audio-effects/WerkstattParameterBox"
import {WerkstattSampleBox} from "./audio-effects/WerkstattSampleBox"
import {SpielwerkDeviceBox} from "./midi-effects/SpielwerkDeviceBox"
import {StutterDeviceBox} from "./audio-effects/StutterDeviceBox"
import {RingModDeviceBox} from "./audio-effects/RingModDeviceBox"
import {ChorusDeviceBox} from "./audio-effects/ChorusDeviceBox"
import {ApparatDeviceBox} from "./instruments/ApparatDeviceBox"
import {MagentaDeviceBox} from "./instruments/MagentaDeviceBox"
import {AceStepDeviceBox} from "./instruments/AceStepDeviceBox"
@@ -85,5 +88,8 @@ export const DeviceDefinitions = [
GrainDeviceBox,
GlitchDeviceBox,
CircusDeviceBox,
HonkDeviceBox
HonkDeviceBox,
StutterDeviceBox,
RingModDeviceBox,
ChorusDeviceBox
]