diff --git a/packages/app/studio/public/manuals/devices/audio/chorus.md b/packages/app/studio/public/manuals/devices/audio/chorus.md new file mode 100644 index 00000000..b61130c0 --- /dev/null +++ b/packages/app/studio/public/manuals/devices/audio/chorus.md @@ -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_ diff --git a/packages/app/studio/public/manuals/devices/audio/ringmod.md b/packages/app/studio/public/manuals/devices/audio/ringmod.md new file mode 100644 index 00000000..1ec9753f --- /dev/null +++ b/packages/app/studio/public/manuals/devices/audio/ringmod.md @@ -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_ diff --git a/packages/app/studio/public/manuals/devices/audio/stutter.md b/packages/app/studio/public/manuals/devices/audio/stutter.md new file mode 100644 index 00000000..402280b8 --- /dev/null +++ b/packages/app/studio/public/manuals/devices/audio/stutter.md @@ -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_ diff --git a/packages/app/studio/src/ui/devices/DeviceEditorFactory.tsx b/packages/app/studio/src/ui/devices/DeviceEditorFactory.tsx index ea81b4d9..10317cce 100644 --- a/packages/app/studio/src/ui/devices/DeviceEditorFactory.tsx +++ b/packages/app/studio/src/ui/devices/DeviceEditorFactory.tsx @@ -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) => ( + + ), + visitRingModDeviceBox: (box: RingModDeviceBox) => ( + + ), + visitChorusDeviceBox: (box: ChorusDeviceBox) => ( + + ), visitWerkstattDeviceBox: (box: WerkstattDeviceBox) => ( { + 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 ( + MenuItems.forAudioUnitInput(parent, service, deviceHost)} + populateControls={() => ( +
+ {Object.values(params).map(p => knob(p))} +
+ )} + icon={IconSymbol.Effects} + populateMeter={() => ( + + )} + /> + ) +} diff --git a/packages/app/studio/src/ui/devices/audio-effects/RingModDeviceEditor.sass b/packages/app/studio/src/ui/devices/audio-effects/RingModDeviceEditor.sass new file mode 100644 index 00000000..0b162075 --- /dev/null +++ b/packages/app/studio/src/ui/devices/audio-effects/RingModDeviceEditor.sass @@ -0,0 +1,4 @@ +@use "@/mixins" + +component + @include mixins.ControlLayout(1) diff --git a/packages/app/studio/src/ui/devices/audio-effects/RingModDeviceEditor.tsx b/packages/app/studio/src/ui/devices/audio-effects/RingModDeviceEditor.tsx new file mode 100644 index 00000000..415f1b1c --- /dev/null +++ b/packages/app/studio/src/ui/devices/audio-effects/RingModDeviceEditor.tsx @@ -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 ( + MenuItems.forAudioUnitInput(parent, service, deviceHost)} + populateControls={() => ( +
+ {Object.values(params).map(p => knob(p))} +
+ )} + icon={IconSymbol.Effects} + populateMeter={() => ( + + )} + /> + ) +} diff --git a/packages/app/studio/src/ui/devices/audio-effects/StutterDeviceEditor.sass b/packages/app/studio/src/ui/devices/audio-effects/StutterDeviceEditor.sass new file mode 100644 index 00000000..0b162075 --- /dev/null +++ b/packages/app/studio/src/ui/devices/audio-effects/StutterDeviceEditor.sass @@ -0,0 +1,4 @@ +@use "@/mixins" + +component + @include mixins.ControlLayout(1) diff --git a/packages/app/studio/src/ui/devices/audio-effects/StutterDeviceEditor.tsx b/packages/app/studio/src/ui/devices/audio-effects/StutterDeviceEditor.tsx new file mode 100644 index 00000000..e251bc18 --- /dev/null +++ b/packages/app/studio/src/ui/devices/audio-effects/StutterDeviceEditor.tsx @@ -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 ( + MenuItems.forAudioUnitInput(parent, service, deviceHost)} + populateControls={() => ( +
+ {Object.values(params).map(p => knob(p))} +
+ )} + icon={IconSymbol.Effects} + populateMeter={() => ( + + )} + /> + ) +} diff --git a/packages/studio/adapters/src/BoxAdapters.ts b/packages/studio/adapters/src/BoxAdapters.ts index cb551b7f..ab9792c6 100644 --- a/packages/studio/adapters/src/BoxAdapters.ts +++ b/packages/studio/adapters/src/BoxAdapters.ts @@ -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}`) } diff --git a/packages/studio/adapters/src/DeviceManualUrls.ts b/packages/studio/adapters/src/DeviceManualUrls.ts index a9dd6b48..260f8a24 100644 --- a/packages/studio/adapters/src/DeviceManualUrls.ts +++ b/packages/studio/adapters/src/DeviceManualUrls.ts @@ -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" diff --git a/packages/studio/adapters/src/devices/audio-effects/ChorusDeviceBoxAdapter.ts b/packages/studio/adapters/src/devices/audio-effects/ChorusDeviceBoxAdapter.ts new file mode 100644 index 00000000..68bb71ec --- /dev/null +++ b/packages/studio/adapters/src/devices/audio-effects/ChorusDeviceBoxAdapter.ts @@ -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 {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 { + 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 + } +} diff --git a/packages/studio/adapters/src/devices/audio-effects/RingModDeviceBoxAdapter.ts b/packages/studio/adapters/src/devices/audio-effects/RingModDeviceBoxAdapter.ts new file mode 100644 index 00000000..c51db4fb --- /dev/null +++ b/packages/studio/adapters/src/devices/audio-effects/RingModDeviceBoxAdapter.ts @@ -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 {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 { + 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 + } +} diff --git a/packages/studio/adapters/src/devices/audio-effects/StutterDeviceBoxAdapter.ts b/packages/studio/adapters/src/devices/audio-effects/StutterDeviceBoxAdapter.ts new file mode 100644 index 00000000..8fa4c0e2 --- /dev/null +++ b/packages/studio/adapters/src/devices/audio-effects/StutterDeviceBoxAdapter.ts @@ -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 {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 { + 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 + } +} diff --git a/packages/studio/core-processors/src/DeviceProcessorFactory.ts b/packages/studio/core-processors/src/DeviceProcessorFactory.ts index 9ae7d8ff..413931ee 100644 --- a/packages/studio/core-processors/src/DeviceProcessorFactory.ts +++ b/packages/studio/core-processors/src/DeviceProcessorFactory.ts @@ -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}'`) diff --git a/packages/studio/core-processors/src/devices/audio-effects/ChorusDeviceProcessor.ts b/packages/studio/core-processors/src/devices/audio-effects/ChorusDeviceProcessor.ts new file mode 100644 index 00000000..21caba7e --- /dev/null +++ b/packages/studio/core-processors/src/devices/audio-effects/ChorusDeviceProcessor.ts @@ -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 = Option.None + rate = 1.5; depth = 0.5; feedback = 0.3; mix = 0.5 + readonly paramRate: AutomatableParameter + readonly paramDepth: AutomatableParameter + readonly paramFeedback: AutomatableParameter + readonly paramMix: AutomatableParameter + 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})}`} +} diff --git a/packages/studio/core-processors/src/devices/audio-effects/RingModDeviceProcessor.ts b/packages/studio/core-processors/src/devices/audio-effects/RingModDeviceProcessor.ts new file mode 100644 index 00000000..18d8444a --- /dev/null +++ b/packages/studio/core-processors/src/devices/audio-effects/RingModDeviceProcessor.ts @@ -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 = Option.None + freq = 440.0; depth = 1.0; mix = 0.5 + readonly paramFreq: AutomatableParameter + readonly paramDepth: AutomatableParameter + readonly paramMix: AutomatableParameter + 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})}`} +} diff --git a/packages/studio/core-processors/src/devices/audio-effects/StutterDeviceProcessor.ts b/packages/studio/core-processors/src/devices/audio-effects/StutterDeviceProcessor.ts new file mode 100644 index 00000000..a3a7c7d8 --- /dev/null +++ b/packages/studio/core-processors/src/devices/audio-effects/StutterDeviceProcessor.ts @@ -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 = Option.None + rate = 8.0; depth = 1.0; chance = 0.5; mix = 1.0 + readonly paramRate: AutomatableParameter + readonly paramDepth: AutomatableParameter + readonly paramChance: AutomatableParameter + readonly paramMix: AutomatableParameter + 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})}`} +} diff --git a/packages/studio/forge-boxes/src/schema/devices/audio-effects/ChorusDeviceBox.ts b/packages/studio/forge-boxes/src/schema/devices/audio-effects/ChorusDeviceBox.ts new file mode 100644 index 00000000..1b55dd7a --- /dev/null +++ b/packages/studio/forge-boxes/src/schema/devices/audio-effects/ChorusDeviceBox.ts @@ -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 = 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: "%" + } +}) diff --git a/packages/studio/forge-boxes/src/schema/devices/audio-effects/RingModDeviceBox.ts b/packages/studio/forge-boxes/src/schema/devices/audio-effects/RingModDeviceBox.ts new file mode 100644 index 00000000..957b65aa --- /dev/null +++ b/packages/studio/forge-boxes/src/schema/devices/audio-effects/RingModDeviceBox.ts @@ -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 = 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: "%" + } +}) diff --git a/packages/studio/forge-boxes/src/schema/devices/audio-effects/StutterDeviceBox.ts b/packages/studio/forge-boxes/src/schema/devices/audio-effects/StutterDeviceBox.ts new file mode 100644 index 00000000..9dfdc809 --- /dev/null +++ b/packages/studio/forge-boxes/src/schema/devices/audio-effects/StutterDeviceBox.ts @@ -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 = 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: "%" + } +}) diff --git a/packages/studio/forge-boxes/src/schema/devices/index.ts b/packages/studio/forge-boxes/src/schema/devices/index.ts index 700007f3..db80abfb 100644 --- a/packages/studio/forge-boxes/src/schema/devices/index.ts +++ b/packages/studio/forge-boxes/src/schema/devices/index.ts @@ -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 ] \ No newline at end of file