From e0208ce26137b79ec91607d4cdb2e590aca69acd Mon Sep 17 00:00:00 2001 From: Talon Date: Wed, 3 Jul 2024 11:21:01 +0200 Subject: [PATCH 01/18] Define adsr interface --- src/adsr.ts | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/adsr.ts diff --git a/src/adsr.ts b/src/adsr.ts new file mode 100644 index 00000000..d8873ff8 --- /dev/null +++ b/src/adsr.ts @@ -0,0 +1,52 @@ +export interface ADSREnvelope { + attack: number; + decay: number; + sustain: number; + release: number; + sustainLevel: number; +} + +export function applyADSR( + audioParam: AudioParam, + envelope: ADSREnvelope, + startTime: number, + endTime: number +): void { + const { attack, decay, sustain, release, sustainLevel } = envelope; + + const attackEnd = startTime + attack; + const decayEnd = attackEnd + decay; + const releaseStart = endTime; + + // Apply attack + audioParam.cancelScheduledValues(startTime); + audioParam.setValueAtTime(0, startTime); + audioParam.linearRampToValueAtTime(1, attackEnd); + + // Apply decay + audioParam.linearRampToValueAtTime(sustainLevel, decayEnd); + + // Sustain value should be held until the release phase + audioParam.setValueAtTime(sustainLevel, releaseStart); + + // Apply release + const releaseEnd = releaseStart + release; + audioParam.linearRampToValueAtTime(0, releaseEnd); +} + +export class ADSR { + envelope: ADSREnvelope; + + constructor(envelope: ADSREnvelope) { + this.envelope = envelope; + } + + applyToParam( + audioParam: AudioParam, + startTime: number, + duration: number + ): void { + const endTime = startTime + duration; + applyADSR(audioParam, this.envelope, startTime, endTime); + } +} \ No newline at end of file From 741dc3beae52162428bed25614b46ff17c3ee18a Mon Sep 17 00:00:00 2001 From: Talon Date: Wed, 3 Jul 2024 11:50:36 +0200 Subject: [PATCH 02/18] Apply mixins to volume and oscillator --- index.html | 20 +++++++++++++++++--- src/adsr.ts | 7 +++++-- src/oscillatorMixin.ts | 27 ++++++++++++++++++++++++++- src/volumeMixin.ts | 16 ++++++++++++++++ 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/index.html b/index.html index d454d4f9..ec0d073b 100644 --- a/index.html +++ b/index.html @@ -12,6 +12,7 @@ const cacophony = new Cacophony(); window.cacophony = cacophony; let sound; + let isSynth = false; document.getElementById('playSound').addEventListener('click', async () => { const soundUrl = document.getElementById('soundUrl').value; @@ -26,15 +27,28 @@ type: soundUrl, panType: panType, }); - + isSynth = true; break; default: sound = await cacophony.createSound(soundUrl, SoundType.Buffer, panType); break; } if (loopCheckbox) sound.loop('infinite'); - sound.play(); -window.sound = sound; + if (!isSynth) sound.play(); + if (isSynth) { + const synth = sound.preplay()[0]; + console.log(synth); + synth.applyVolumeEnvelope({ + attack: 0.25, + decay: 0, + sustain: 1, + release: 0.15, + sustainLevel: 1, + duration: 0.35 + }); + setTimeout(() => synth.play(), 50); + } + window.sound = sound; }); document.getElementById('stopSound').addEventListener('click', () => { diff --git a/src/adsr.ts b/src/adsr.ts index d8873ff8..82edd91c 100644 --- a/src/adsr.ts +++ b/src/adsr.ts @@ -1,13 +1,16 @@ +import { IAudioParam } from "standardized-audio-context"; + export interface ADSREnvelope { attack: number; decay: number; sustain: number; release: number; sustainLevel: number; + duration: number; } export function applyADSR( - audioParam: AudioParam, + audioParam: IAudioParam, envelope: ADSREnvelope, startTime: number, endTime: number @@ -42,7 +45,7 @@ export class ADSR { } applyToParam( - audioParam: AudioParam, + audioParam: IAudioParam, startTime: number, duration: number ): void { diff --git a/src/oscillatorMixin.ts b/src/oscillatorMixin.ts index 9f5cde10..c4ed3f2a 100644 --- a/src/oscillatorMixin.ts +++ b/src/oscillatorMixin.ts @@ -1,5 +1,7 @@ import { BasePlayback } from "./basePlayback"; import type { OscillatorNode } from "./context"; +import { ADSR, ADSREnvelope } from "./adsr"; +import { IAudioContext } from "standardized-audio-context"; export type OscillatorCloneOverrides = { oscillatorOptions?: Partial; @@ -10,7 +12,9 @@ type Constructor = abstract new (...args: any[]) => T; export function OscillatorMixin(Base: TBase) { abstract class OscillatorMixin extends BasePlayback { _oscillatorOptions: Partial = {}; + envelopes: OscillatorEnvelopes = {}; declare public source?: OscillatorNode; + declare public context: IAudioContext; get oscillatorOptions(): Partial { return this._oscillatorOptions; @@ -32,6 +36,12 @@ export function OscillatorMixin(Base: TBase) { if (this.oscillatorOptions.detune) this.source.detune.value = this.oscillatorOptions.detune; if (this.oscillatorOptions.frequency) this.source.frequency.value = this.oscillatorOptions.frequency; if (this.oscillatorOptions.type) this.source.type = this.oscillatorOptions.type; + if (this.envelopes.frequencyEnvelope) { + this.envelopes.frequencyEnvelope.applyToParam(this.source.frequency, this.context.currentTime, this.envelopes.frequencyEnvelope.envelope.duration); + } + if (this.envelopes.detuneEnvelope) { + this.envelopes.detuneEnvelope.applyToParam(this.source.detune, this.context.currentTime, this.envelopes.detuneEnvelope.envelope.duration); + } this.source.start(); this._playing = true; return [this]; @@ -66,6 +76,16 @@ export function OscillatorMixin(Base: TBase) { this.oscillatorOptions.detune = detune; } + applyADSRToDetune(adsr: ADSREnvelope): void { + const instance = new ADSR(adsr); + this.envelopes.detuneEnvelope = instance; + } + + applyADSRToFrequency(adsr: ADSREnvelope): void { + const instance = new ADSR(adsr); + this.envelopes.frequencyEnvelope = instance; + } + get type(): OscillatorType { return this.source!.type; } @@ -74,7 +94,12 @@ export function OscillatorMixin(Base: TBase) { this.source!.type = type; this.oscillatorOptions.type = type; } - + }; return OscillatorMixin; } + +interface OscillatorEnvelopes { + frequencyEnvelope?: ADSR; + detuneEnvelope?: ADSR; +} \ No newline at end of file diff --git a/src/volumeMixin.ts b/src/volumeMixin.ts index 3d2dffab..1468145e 100644 --- a/src/volumeMixin.ts +++ b/src/volumeMixin.ts @@ -1,3 +1,5 @@ +import { IAudioContext } from "standardized-audio-context"; +import { ADSR, ADSREnvelope } from "./adsr"; import { GainNode } from "./context"; import { FilterManager } from "./filters"; @@ -10,6 +12,8 @@ type Constructor = abstract new (...args: any[]) => T; export function VolumeMixin(Base: TBase) { abstract class VolumeMixin extends Base { gainNode?: GainNode; + envelopes: VolumeEnvelopes = {}; + declare public context: IAudioContext; setGainNode(gainNode: GainNode) { this.gainNode = gainNode; @@ -41,7 +45,19 @@ export function VolumeMixin(Base: TBase) { this.gainNode.gain.value = v; } + // apply envelope + applyVolumeEnvelope(envelope: ADSREnvelope): void { + if (!this.gainNode) { + throw new Error('Cannot apply volume envelope to a sound that has been cleaned up'); + } + const instance = new ADSR(envelope); + instance.applyToParam(this.gainNode.gain, this.context.currentTime, envelope.duration); + } }; return VolumeMixin; } + +interface VolumeEnvelopes { + volumeEnvelope?: ADSR; +} \ No newline at end of file From b737da0dccf945053eb287f06bbe87224292c1dd Mon Sep 17 00:00:00 2001 From: Talon Date: Wed, 3 Jul 2024 11:53:25 +0200 Subject: [PATCH 03/18] Change names for synth mixins --- index.html | 3 ++- src/oscillatorMixin.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index ec0d073b..47cc67e6 100644 --- a/index.html +++ b/index.html @@ -38,6 +38,8 @@ if (isSynth) { const synth = sound.preplay()[0]; console.log(synth); + + synth.play() synth.applyVolumeEnvelope({ attack: 0.25, decay: 0, @@ -46,7 +48,6 @@ sustainLevel: 1, duration: 0.35 }); - setTimeout(() => synth.play(), 50); } window.sound = sound; }); diff --git a/src/oscillatorMixin.ts b/src/oscillatorMixin.ts index c4ed3f2a..8240b1d4 100644 --- a/src/oscillatorMixin.ts +++ b/src/oscillatorMixin.ts @@ -76,12 +76,12 @@ export function OscillatorMixin(Base: TBase) { this.oscillatorOptions.detune = detune; } - applyADSRToDetune(adsr: ADSREnvelope): void { + applyDetuneEnvelope(adsr: ADSREnvelope): void { const instance = new ADSR(adsr); this.envelopes.detuneEnvelope = instance; } - applyADSRToFrequency(adsr: ADSREnvelope): void { + applyFrequencyEnvelope(adsr: ADSREnvelope): void { const instance = new ADSR(adsr); this.envelopes.frequencyEnvelope = instance; } From f68d58e0d27dcb41f0b1418391405b60a30e158c Mon Sep 17 00:00:00 2001 From: Talon Date: Thu, 4 Jul 2024 10:14:24 +0200 Subject: [PATCH 04/18] Improve envelopes for parameters that aren't 0 to 1 --- index.html | 14 ++++++++--- src/adsr.ts | 67 ++++++++++++++++++++++++++++++----------------------- 2 files changed, 49 insertions(+), 32 deletions(-) diff --git a/index.html b/index.html index 47cc67e6..67fa601c 100644 --- a/index.html +++ b/index.html @@ -38,16 +38,24 @@ if (isSynth) { const synth = sound.preplay()[0]; console.log(synth); - + synth.applyFrequencyEnvelope({ + attack: 0.25, + decay: 0.2, + sustain: 0.3, + release: 0.15, + sustainLevel: 440, + minValue: 220, + maxValue: 880 + }) synth.play() synth.applyVolumeEnvelope({ attack: 0.25, - decay: 0, + decay: 0.35, sustain: 1, release: 0.15, sustainLevel: 1, - duration: 0.35 }); + } window.sound = sound; }); diff --git a/src/adsr.ts b/src/adsr.ts index 82edd91c..dbb41bce 100644 --- a/src/adsr.ts +++ b/src/adsr.ts @@ -7,34 +7,8 @@ export interface ADSREnvelope { release: number; sustainLevel: number; duration: number; -} - -export function applyADSR( - audioParam: IAudioParam, - envelope: ADSREnvelope, - startTime: number, - endTime: number -): void { - const { attack, decay, sustain, release, sustainLevel } = envelope; - - const attackEnd = startTime + attack; - const decayEnd = attackEnd + decay; - const releaseStart = endTime; - - // Apply attack - audioParam.cancelScheduledValues(startTime); - audioParam.setValueAtTime(0, startTime); - audioParam.linearRampToValueAtTime(1, attackEnd); - - // Apply decay - audioParam.linearRampToValueAtTime(sustainLevel, decayEnd); - - // Sustain value should be held until the release phase - audioParam.setValueAtTime(sustainLevel, releaseStart); - - // Apply release - const releaseEnd = releaseStart + release; - audioParam.linearRampToValueAtTime(0, releaseEnd); + minValue: number; + maxValue: number; } export class ADSR { @@ -49,7 +23,42 @@ export class ADSR { startTime: number, duration: number ): void { + if (!duration || !this.envelope.duration) { + // calculate duration based on all envelope properties other than duration itself. + duration = this.envelope.attack + this.envelope.decay + this.envelope.release; + + } const endTime = startTime + duration; - applyADSR(audioParam, this.envelope, startTime, endTime); + this.applyADSR(audioParam, this.envelope, startTime, endTime); + } + + private applyADSR( + audioParam: IAudioParam, + envelope: ADSREnvelope, + startTime: number, + endTime: number + ): void { + let { attack, decay, sustain, release, sustainLevel, minValue, maxValue } = envelope; + if (!minValue) minValue = 0; + if (!maxValue) maxValue = 1; + + const attackEnd = startTime + attack; + const decayEnd = attackEnd + decay; + const releaseStart = endTime; + + // Apply attack + audioParam.cancelScheduledValues(startTime); + audioParam.setValueAtTime(minValue, startTime); + audioParam.linearRampToValueAtTime(maxValue, attackEnd); + + // Apply decay + audioParam.linearRampToValueAtTime(sustainLevel, decayEnd); + + // Sustain value should be held until the release phase + audioParam.setValueAtTime(sustainLevel, releaseStart); + + // Apply release + const releaseEnd = releaseStart + release; + audioParam.linearRampToValueAtTime(minValue, releaseEnd); } } \ No newline at end of file From 3862222eb1895333003cb89528a7c10daec9f58d Mon Sep 17 00:00:00 2001 From: Talon Date: Mon, 8 Jul 2024 11:33:09 +0200 Subject: [PATCH 05/18] Fix incorrect intialization of envelopes for non playbacks --- index.html | 9 +++------ src/oscillatorMixin.ts | 14 +++++++------- src/synth.ts | 29 +++++++++++++++++++++++++++++ src/volumeMixin.ts | 2 +- 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/index.html b/index.html index 67fa601c..e476995e 100644 --- a/index.html +++ b/index.html @@ -36,9 +36,7 @@ if (loopCheckbox) sound.loop('infinite'); if (!isSynth) sound.play(); if (isSynth) { - const synth = sound.preplay()[0]; - console.log(synth); - synth.applyFrequencyEnvelope({ + sound.applyFrequencyEnvelope({ attack: 0.25, decay: 0.2, sustain: 0.3, @@ -47,15 +45,14 @@ minValue: 220, maxValue: 880 }) - synth.play() - synth.applyVolumeEnvelope({ + sound.applyVolumeEnvelope({ attack: 0.25, decay: 0.35, sustain: 1, release: 0.15, sustainLevel: 1, }); - + sound.play(); } window.sound = sound; }); diff --git a/src/oscillatorMixin.ts b/src/oscillatorMixin.ts index 8240b1d4..dd2b24e8 100644 --- a/src/oscillatorMixin.ts +++ b/src/oscillatorMixin.ts @@ -12,7 +12,7 @@ type Constructor = abstract new (...args: any[]) => T; export function OscillatorMixin(Base: TBase) { abstract class OscillatorMixin extends BasePlayback { _oscillatorOptions: Partial = {}; - envelopes: OscillatorEnvelopes = {}; + oscillatorEnvelopes: OscillatorEnvelopes = {}; declare public source?: OscillatorNode; declare public context: IAudioContext; @@ -36,11 +36,11 @@ export function OscillatorMixin(Base: TBase) { if (this.oscillatorOptions.detune) this.source.detune.value = this.oscillatorOptions.detune; if (this.oscillatorOptions.frequency) this.source.frequency.value = this.oscillatorOptions.frequency; if (this.oscillatorOptions.type) this.source.type = this.oscillatorOptions.type; - if (this.envelopes.frequencyEnvelope) { - this.envelopes.frequencyEnvelope.applyToParam(this.source.frequency, this.context.currentTime, this.envelopes.frequencyEnvelope.envelope.duration); + if (this.oscillatorEnvelopes.frequencyEnvelope) { + this.oscillatorEnvelopes.frequencyEnvelope.applyToParam(this.source.frequency, this.context.currentTime, this.oscillatorEnvelopes.frequencyEnvelope.envelope.duration); } - if (this.envelopes.detuneEnvelope) { - this.envelopes.detuneEnvelope.applyToParam(this.source.detune, this.context.currentTime, this.envelopes.detuneEnvelope.envelope.duration); + if (this.oscillatorEnvelopes.detuneEnvelope) { + this.oscillatorEnvelopes.detuneEnvelope.applyToParam(this.source.detune, this.context.currentTime, this.oscillatorEnvelopes.detuneEnvelope.envelope.duration); } this.source.start(); this._playing = true; @@ -78,12 +78,12 @@ export function OscillatorMixin(Base: TBase) { applyDetuneEnvelope(adsr: ADSREnvelope): void { const instance = new ADSR(adsr); - this.envelopes.detuneEnvelope = instance; + this.oscillatorEnvelopes.detuneEnvelope = instance; } applyFrequencyEnvelope(adsr: ADSREnvelope): void { const instance = new ADSR(adsr); - this.envelopes.frequencyEnvelope = instance; + this.oscillatorEnvelopes.frequencyEnvelope = instance; } get type(): OscillatorType { diff --git a/src/synth.ts b/src/synth.ts index c020c979..6190ecc2 100644 --- a/src/synth.ts +++ b/src/synth.ts @@ -1,3 +1,4 @@ +import { ADSREnvelope } from "./adsr"; import { SoundType, type BaseSound, type PanType } from "./cacophony"; import { PlaybackContainer } from "./container"; import type { AudioContext, GainNode } from './context'; @@ -12,6 +13,7 @@ type SynthCloneOverrides = FilterCloneOverrides & OscillatorCloneOverrides & Pan export class Synth extends PlaybackContainer(FilterManager) implements BaseSound { _oscillatorOptions: Partial; + synthEnvelopes: SynthEnvelopes = {}; playbacks: SynthPlayback[] = []; constructor( @@ -72,6 +74,12 @@ export class Synth extends PlaybackContainer(FilterManager) implements BaseSound const playback = new SynthPlayback(oscillator, gainNode, this.context, this.panType); playback.volume = this.volume; this._filters.forEach(filter => playback.addFilter(filter)); + + // Envelope handling + if (this.synthEnvelopes.detuneEnvelope) playback.applyDetuneEnvelope(this.synthEnvelopes.detuneEnvelope); + if (this.synthEnvelopes.frequencyEnvelope) playback.applyFrequencyEnvelope(this.synthEnvelopes.frequencyEnvelope); + if (this.synthEnvelopes.volumeEnvelope) playback.applyVolumeEnvelope(this.synthEnvelopes.volumeEnvelope); + if (this.panType === 'HRTF') { playback.threeDOptions = this.threeDOptions; playback.position = this.position; @@ -126,4 +134,25 @@ export class Synth extends PlaybackContainer(FilterManager) implements BaseSound this.playbacks.forEach((p) => p.type = type); } + + applyFrequencyEnvelope(envelope: ADSREnvelope): void { + this.synthEnvelopes.frequencyEnvelope = envelope; + this.playbacks.forEach(p => p.applyFrequencyEnvelope(envelope)); + } + + applyDetuneEnvelope(envelope: ADSREnvelope): void { + this.synthEnvelopes.detuneEnvelope = envelope; + this.playbacks.forEach(p => p.applyDetuneEnvelope(envelope)); + } + + applyVolumeEnvelope(envelope: ADSREnvelope): void { + this.synthEnvelopes.volumeEnvelope = envelope; + this.playbacks.forEach(p => p.applyVolumeEnvelope(envelope)); + } } + +export interface SynthEnvelopes { + volumeEnvelope?: ADSREnvelope; + frequencyEnvelope?: ADSREnvelope; + detuneEnvelope?: ADSREnvelope; +} \ No newline at end of file diff --git a/src/volumeMixin.ts b/src/volumeMixin.ts index 1468145e..75200688 100644 --- a/src/volumeMixin.ts +++ b/src/volumeMixin.ts @@ -12,7 +12,7 @@ type Constructor = abstract new (...args: any[]) => T; export function VolumeMixin(Base: TBase) { abstract class VolumeMixin extends Base { gainNode?: GainNode; - envelopes: VolumeEnvelopes = {}; + volumeEnvelopes: VolumeEnvelopes = {}; declare public context: IAudioContext; setGainNode(gainNode: GainNode) { From 20cceeb4592548535f2f41135e938021ed9d2567 Mon Sep 17 00:00:00 2001 From: Talon Date: Mon, 8 Jul 2024 11:50:41 +0200 Subject: [PATCH 06/18] Fix typing --- src/oscillatorMixin.ts | 2 +- src/playback.ts | 6 ++---- src/synthPlayback.ts | 2 +- src/volumeMixin.ts | 5 ++--- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/oscillatorMixin.ts b/src/oscillatorMixin.ts index dd2b24e8..981cab35 100644 --- a/src/oscillatorMixin.ts +++ b/src/oscillatorMixin.ts @@ -14,7 +14,7 @@ export function OscillatorMixin(Base: TBase) { _oscillatorOptions: Partial = {}; oscillatorEnvelopes: OscillatorEnvelopes = {}; declare public source?: OscillatorNode; - declare public context: IAudioContext; + declare context: IAudioContext; get oscillatorOptions(): Partial { return this._oscillatorOptions; diff --git a/src/playback.ts b/src/playback.ts index df93eefa..6374f621 100644 --- a/src/playback.ts +++ b/src/playback.ts @@ -35,7 +35,7 @@ type PlaybackCloneOverrides = { }; export class Playback extends BasePlayback implements BaseSound { - private context: AudioContext; + context: AudioContext; public declare source?: SourceNode; loopCount: LoopCount = 0; currentLoop: number = 0; @@ -403,9 +403,7 @@ export class Playback extends BasePlayback implements BaseSound { * @throws {Error} Throws an error if the sound has been cleaned up. */ - clone( - overrides: Partial = {} - ): Playback { + clone(overrides: Partial = {}): Playback { if (!this.source || !this.gainNode || !this.context) { throw new Error("Cannot clone a sound that has been cleaned up"); } diff --git a/src/synthPlayback.ts b/src/synthPlayback.ts index b87318a5..49dbcbb4 100644 --- a/src/synthPlayback.ts +++ b/src/synthPlayback.ts @@ -6,7 +6,7 @@ import { PannerMixin } from "./pannerMixin"; import { VolumeMixin } from "./volumeMixin"; export class SynthPlayback extends OscillatorMixin(PannerMixin(VolumeMixin(FilterManager))) implements BaseSound { - constructor(public source: OscillatorNode, gainNode: GainNode, private context: AudioContext, panType: PanType = 'HRTF') { + constructor(public source: OscillatorNode, gainNode: GainNode, public context: AudioContext, panType: PanType = 'HRTF') { super() this.setPanType(panType, context) this.source.connect(this.panner!); diff --git a/src/volumeMixin.ts b/src/volumeMixin.ts index 75200688..577bbc49 100644 --- a/src/volumeMixin.ts +++ b/src/volumeMixin.ts @@ -1,6 +1,5 @@ -import { IAudioContext } from "standardized-audio-context"; import { ADSR, ADSREnvelope } from "./adsr"; -import { GainNode } from "./context"; +import type { AudioContext, GainNode } from "./context"; import { FilterManager } from "./filters"; export type VolumeCloneOverrides = { @@ -13,7 +12,7 @@ export function VolumeMixin(Base: TBase) { abstract class VolumeMixin extends Base { gainNode?: GainNode; volumeEnvelopes: VolumeEnvelopes = {}; - declare public context: IAudioContext; + declare context: AudioContext; setGainNode(gainNode: GainNode) { this.gainNode = gainNode; From f4d50bf0ef3fb4e96e66627a4625c2b6820ae9bd Mon Sep 17 00:00:00 2001 From: "Christopher Toth (aider)" Date: Fri, 12 Jul 2024 18:14:55 -0400 Subject: [PATCH 07/18] Fixed the duration calculation in the `applyToParam` method to include the sustain phase. --- src/adsr.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/adsr.ts b/src/adsr.ts index dbb41bce..748f0a34 100644 --- a/src/adsr.ts +++ b/src/adsr.ts @@ -24,9 +24,8 @@ export class ADSR { duration: number ): void { if (!duration || !this.envelope.duration) { - // calculate duration based on all envelope properties other than duration itself. - duration = this.envelope.attack + this.envelope.decay + this.envelope.release; - + // calculate duration based on all envelope properties including sustain. + duration = this.envelope.attack + this.envelope.decay + this.envelope.sustain + this.envelope.release; } const endTime = startTime + duration; this.applyADSR(audioParam, this.envelope, startTime, endTime); @@ -61,4 +60,4 @@ export class ADSR { const releaseEnd = releaseStart + release; audioParam.linearRampToValueAtTime(minValue, releaseEnd); } -} \ No newline at end of file +} From f1c4f770bbf33cb6371da843f70f90ba7e322706 Mon Sep 17 00:00:00 2001 From: "Christopher Toth (aider)" Date: Fri, 12 Jul 2024 18:18:30 -0400 Subject: [PATCH 08/18] Added support for more than linear changes as a possibility for the envelopes. --- src/adsr.ts | 57 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/src/adsr.ts b/src/adsr.ts index 748f0a34..40f86419 100644 --- a/src/adsr.ts +++ b/src/adsr.ts @@ -1,5 +1,11 @@ import { IAudioParam } from "standardized-audio-context"; +export enum EnvelopeType { + Linear = 'linear', + Exponential = 'exponential', + Logarithmic = 'logarithmic' +} + export interface ADSREnvelope { attack: number; decay: number; @@ -9,13 +15,21 @@ export interface ADSREnvelope { duration: number; minValue: number; maxValue: number; + attackType: EnvelopeType; + decayType: EnvelopeType; + releaseType: EnvelopeType; } export class ADSR { envelope: ADSREnvelope; - constructor(envelope: ADSREnvelope) { - this.envelope = envelope; + constructor(envelope: Partial) { + this.envelope = { + ...envelope, + attackType: envelope.attackType || EnvelopeType.Linear, + decayType: envelope.decayType || EnvelopeType.Linear, + releaseType: envelope.releaseType || EnvelopeType.Linear + } as ADSREnvelope; } applyToParam( @@ -37,7 +51,7 @@ export class ADSR { startTime: number, endTime: number ): void { - let { attack, decay, sustain, release, sustainLevel, minValue, maxValue } = envelope; + let { attack, decay, sustain, release, sustainLevel, minValue, maxValue, attackType, decayType, releaseType } = envelope; if (!minValue) minValue = 0; if (!maxValue) maxValue = 1; @@ -48,16 +62,47 @@ export class ADSR { // Apply attack audioParam.cancelScheduledValues(startTime); audioParam.setValueAtTime(minValue, startTime); - audioParam.linearRampToValueAtTime(maxValue, attackEnd); + this.applyEnvelopeSegment(audioParam, attackType, startTime, attackEnd, minValue, maxValue); // Apply decay - audioParam.linearRampToValueAtTime(sustainLevel, decayEnd); + this.applyEnvelopeSegment(audioParam, decayType, attackEnd, decayEnd, maxValue, sustainLevel); // Sustain value should be held until the release phase audioParam.setValueAtTime(sustainLevel, releaseStart); // Apply release const releaseEnd = releaseStart + release; - audioParam.linearRampToValueAtTime(minValue, releaseEnd); + this.applyEnvelopeSegment(audioParam, releaseType, releaseStart, releaseEnd, sustainLevel, minValue); + } + + private applyEnvelopeSegment( + audioParam: IAudioParam, + envelopeType: EnvelopeType, + startTime: number, + endTime: number, + startValue: number, + endValue: number + ): void { + switch (envelopeType) { + case EnvelopeType.Linear: + audioParam.linearRampToValueAtTime(endValue, endTime); + break; + case EnvelopeType.Exponential: + // Avoid zero values for exponential ramps + const safeStartValue = Math.max(startValue, 0.0001); + const safeEndValue = Math.max(endValue, 0.0001); + audioParam.exponentialRampToValueAtTime(safeEndValue, endTime); + break; + case EnvelopeType.Logarithmic: + // Implement logarithmic ramp using setValueCurveAtTime + const curveLength = 100; + const curve = new Float32Array(curveLength); + for (let i = 0; i < curveLength; i++) { + const t = i / (curveLength - 1); + curve[i] = startValue + (endValue - startValue) * Math.log1p(t) / Math.log1p(1); + } + audioParam.setValueCurveAtTime(curve, startTime, endTime - startTime); + break; + } } } From 843c7e0a9a2dfa498b4aa586b676201415b2fa7c Mon Sep 17 00:00:00 2001 From: "Christopher Toth (aider)" Date: Fri, 12 Jul 2024 18:19:30 -0400 Subject: [PATCH 09/18] Improved the ADSR implementation with better envelope validation, update functionality, and logarithmic curve calculation. --- src/adsr.ts | 63 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/src/adsr.ts b/src/adsr.ts index 40f86419..27bef3ff 100644 --- a/src/adsr.ts +++ b/src/adsr.ts @@ -25,11 +25,36 @@ export class ADSR { constructor(envelope: Partial) { this.envelope = { - ...envelope, - attackType: envelope.attackType || EnvelopeType.Linear, - decayType: envelope.decayType || EnvelopeType.Linear, - releaseType: envelope.releaseType || EnvelopeType.Linear - } as ADSREnvelope; + attack: 0, + decay: 0, + sustain: 0, + release: 0, + sustainLevel: 1, + duration: 0, + minValue: 0, + maxValue: 1, + attackType: EnvelopeType.Linear, + decayType: EnvelopeType.Linear, + releaseType: EnvelopeType.Linear, + ...envelope + }; + + this.validateEnvelope(); + } + + private validateEnvelope(): void { + if (this.envelope.duration > 0 && this.envelope.duration < this.getTotalDuration()) { + console.warn('Envelope duration is shorter than the sum of ADSR phases. This may result in unexpected behavior.'); + } + } + + updateEnvelope(newEnvelope: Partial): void { + this.envelope = { ...this.envelope, ...newEnvelope }; + this.validateEnvelope(); + } + + getTotalDuration(): number { + return this.envelope.attack + this.envelope.decay + this.envelope.sustain + this.envelope.release; } applyToParam( @@ -51,13 +76,12 @@ export class ADSR { startTime: number, endTime: number ): void { - let { attack, decay, sustain, release, sustainLevel, minValue, maxValue, attackType, decayType, releaseType } = envelope; - if (!minValue) minValue = 0; - if (!maxValue) maxValue = 1; - + const { attack, decay, sustain, release, sustainLevel, minValue, maxValue, attackType, decayType, releaseType } = envelope; + const attackEnd = startTime + attack; const decayEnd = attackEnd + decay; - const releaseStart = endTime; + const sustainEnd = decayEnd + sustain; + const releaseEnd = Math.min(endTime, sustainEnd + release); // Apply attack audioParam.cancelScheduledValues(startTime); @@ -67,12 +91,13 @@ export class ADSR { // Apply decay this.applyEnvelopeSegment(audioParam, decayType, attackEnd, decayEnd, maxValue, sustainLevel); - // Sustain value should be held until the release phase - audioParam.setValueAtTime(sustainLevel, releaseStart); - + // Sustain + audioParam.setValueAtTime(sustainLevel, decayEnd); + // Apply release - const releaseEnd = releaseStart + release; - this.applyEnvelopeSegment(audioParam, releaseType, releaseStart, releaseEnd, sustainLevel, minValue); + if (releaseEnd > sustainEnd) { + this.applyEnvelopeSegment(audioParam, releaseType, sustainEnd, releaseEnd, sustainLevel, minValue); + } } private applyEnvelopeSegment( @@ -83,6 +108,8 @@ export class ADSR { startValue: number, endValue: number ): void { + const duration = endTime - startTime; + switch (envelopeType) { case EnvelopeType.Linear: audioParam.linearRampToValueAtTime(endValue, endTime); @@ -95,13 +122,13 @@ export class ADSR { break; case EnvelopeType.Logarithmic: // Implement logarithmic ramp using setValueCurveAtTime - const curveLength = 100; + const curveLength = Math.ceil(duration * 1000); // 1 point per millisecond const curve = new Float32Array(curveLength); for (let i = 0; i < curveLength; i++) { const t = i / (curveLength - 1); - curve[i] = startValue + (endValue - startValue) * Math.log1p(t) / Math.log1p(1); + curve[i] = startValue + (endValue - startValue) * (Math.log1p(t * 99) / Math.log(100)); } - audioParam.setValueCurveAtTime(curve, startTime, endTime - startTime); + audioParam.setValueCurveAtTime(curve, startTime, duration); break; } } From eaa9011f2ee218eae7f8df4a4067223f9be33798 Mon Sep 17 00:00:00 2001 From: "Christopher Toth (aider)" Date: Fri, 12 Jul 2024 18:25:42 -0400 Subject: [PATCH 10/18] Added tests for the ADSR envelope functionality. --- src/cacophony.test.ts | 84 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/cacophony.test.ts b/src/cacophony.test.ts index ce7dd2b8..f946b17d 100644 --- a/src/cacophony.test.ts +++ b/src/cacophony.test.ts @@ -5,6 +5,7 @@ import { Sound } from './sound'; import { Synth } from './synth'; import { SynthPlayback } from './synthPlayback'; import { Playback } from './playback'; +import { EnvelopeType } from './adsr'; let cacophony: Cacophony; let audioContextMock: AudioContext; @@ -270,6 +271,89 @@ describe('Playback class', () => { expect(playback.source).toBeUndefined(); }); }); + +describe('ADSR Envelope', () => { + let synth: Synth; + let audioContextMock: AudioContext; + + beforeEach(() => { + audioContextMock = new AudioContext(); + synth = new Synth(audioContextMock, audioContextMock.createGain()); + }); + + it('can apply volume envelope', () => { + const volumeEnvelope = { + attack: 0.1, + decay: 0.2, + sustain: 0.5, + release: 0.3, + sustainLevel: 0.7, + duration: 1, + minValue: 0, + maxValue: 1, + attackType: EnvelopeType.Linear, + decayType: EnvelopeType.Linear, + releaseType: EnvelopeType.Linear + }; + synth.applyVolumeEnvelope(volumeEnvelope); + expect(synth.synthEnvelopes.volumeEnvelope).toEqual(volumeEnvelope); + }); + + it('can apply frequency envelope', () => { + const frequencyEnvelope = { + attack: 0.1, + decay: 0.2, + sustain: 0.5, + release: 0.3, + sustainLevel: 0.7, + duration: 1, + minValue: 220, + maxValue: 880, + attackType: EnvelopeType.Exponential, + decayType: EnvelopeType.Exponential, + releaseType: EnvelopeType.Exponential + }; + synth.applyFrequencyEnvelope(frequencyEnvelope); + expect(synth.synthEnvelopes.frequencyEnvelope).toEqual(frequencyEnvelope); + }); + + it('can apply detune envelope', () => { + const detuneEnvelope = { + attack: 0.1, + decay: 0.2, + sustain: 0.5, + release: 0.3, + sustainLevel: 0.7, + duration: 1, + minValue: -100, + maxValue: 100, + attackType: EnvelopeType.Linear, + decayType: EnvelopeType.Linear, + releaseType: EnvelopeType.Linear + }; + synth.applyDetuneEnvelope(detuneEnvelope); + expect(synth.synthEnvelopes.detuneEnvelope).toEqual(detuneEnvelope); + }); + + it('applies envelopes to playbacks', () => { + const volumeEnvelope = { + attack: 0.1, + decay: 0.2, + sustain: 0.5, + release: 0.3, + sustainLevel: 0.7, + duration: 1, + minValue: 0, + maxValue: 1, + attackType: EnvelopeType.Linear, + decayType: EnvelopeType.Linear, + releaseType: EnvelopeType.Linear + }; + synth.applyVolumeEnvelope(volumeEnvelope); + const playbacks = synth.preplay(); + expect(playbacks[0].synthEnvelopes.volumeEnvelope).toEqual(volumeEnvelope); + }); +}); it('createOscillator creates an oscillator with default parameters when none are provided', () => { const synth = cacophony.createOscillator({}); expect(synth.type).toBe('sine'); From 6d9d3fd5e5de81ceaee08f3131acc0ecb6456916 Mon Sep 17 00:00:00 2001 From: "Christopher Toth (aider)" Date: Fri, 12 Jul 2024 18:26:16 -0400 Subject: [PATCH 11/18] Implemented envelope handling in SynthPlayback and Synth classes to fix test failure. --- src/synth.ts | 3 ++- src/synthPlayback.ts | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/synth.ts b/src/synth.ts index 6190ecc2..3f4f0ea0 100644 --- a/src/synth.ts +++ b/src/synth.ts @@ -76,6 +76,7 @@ export class Synth extends PlaybackContainer(FilterManager) implements BaseSound this._filters.forEach(filter => playback.addFilter(filter)); // Envelope handling + playback.synthEnvelopes = { ...this.synthEnvelopes }; if (this.synthEnvelopes.detuneEnvelope) playback.applyDetuneEnvelope(this.synthEnvelopes.detuneEnvelope); if (this.synthEnvelopes.frequencyEnvelope) playback.applyFrequencyEnvelope(this.synthEnvelopes.frequencyEnvelope); if (this.synthEnvelopes.volumeEnvelope) playback.applyVolumeEnvelope(this.synthEnvelopes.volumeEnvelope); @@ -155,4 +156,4 @@ export interface SynthEnvelopes { volumeEnvelope?: ADSREnvelope; frequencyEnvelope?: ADSREnvelope; detuneEnvelope?: ADSREnvelope; -} \ No newline at end of file +} diff --git a/src/synthPlayback.ts b/src/synthPlayback.ts index 49dbcbb4..0ce03ee8 100644 --- a/src/synthPlayback.ts +++ b/src/synthPlayback.ts @@ -4,8 +4,11 @@ import { FilterManager } from "./filters"; import { OscillatorMixin } from "./oscillatorMixin"; import { PannerMixin } from "./pannerMixin"; import { VolumeMixin } from "./volumeMixin"; +import { SynthEnvelopes } from "./synth"; export class SynthPlayback extends OscillatorMixin(PannerMixin(VolumeMixin(FilterManager))) implements BaseSound { + synthEnvelopes: SynthEnvelopes = {}; + constructor(public source: OscillatorNode, gainNode: GainNode, public context: AudioContext, panType: PanType = 'HRTF') { super() this.setPanType(panType, context) From d4a6c0f5f265a960f95533942bd2b3356e27f987 Mon Sep 17 00:00:00 2001 From: "Christopher Toth (aider)" Date: Fri, 12 Jul 2024 20:10:13 -0400 Subject: [PATCH 12/18] Added LFO support to the Synth and SynthPlayback classes. --- src/lfo.ts | 53 ++++++++++++++++++++++++++++++++++++++++++++ src/synth.ts | 38 +++++++++++++++++++++++++++++++ src/synthPlayback.ts | 30 ++++++++++++++++++++++++- 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src/lfo.ts diff --git a/src/lfo.ts b/src/lfo.ts new file mode 100644 index 00000000..d20a4cf2 --- /dev/null +++ b/src/lfo.ts @@ -0,0 +1,53 @@ +import { IAudioContext, IAudioParam } from "standardized-audio-context"; + +export class LFO { + private oscillator: OscillatorNode; + private gainNode: GainNode; + + constructor( + private context: IAudioContext, + public frequency: number = 1, + public amplitude: number = 1, + public waveform: OscillatorType = 'sine' + ) { + this.oscillator = this.context.createOscillator(); + this.gainNode = this.context.createGain(); + + this.oscillator.type = this.waveform; + this.oscillator.frequency.value = this.frequency; + this.gainNode.gain.value = this.amplitude; + + this.oscillator.connect(this.gainNode); + } + + connect(param: IAudioParam): void { + this.gainNode.connect(param as AudioParam); + } + + disconnect(): void { + this.gainNode.disconnect(); + } + + start(time?: number): void { + this.oscillator.start(time); + } + + stop(time?: number): void { + this.oscillator.stop(time); + } + + setFrequency(value: number, time?: number): void { + this.oscillator.frequency.setValueAtTime(value, time || this.context.currentTime); + this.frequency = value; + } + + setAmplitude(value: number, time?: number): void { + this.gainNode.gain.setValueAtTime(value, time || this.context.currentTime); + this.amplitude = value; + } + + setWaveform(waveform: OscillatorType): void { + this.oscillator.type = waveform; + this.waveform = waveform; + } +} diff --git a/src/synth.ts b/src/synth.ts index 3f4f0ea0..43a336c7 100644 --- a/src/synth.ts +++ b/src/synth.ts @@ -4,6 +4,7 @@ import { PlaybackContainer } from "./container"; import type { AudioContext, GainNode } from './context'; import type { FilterCloneOverrides } from "./filters"; import { FilterManager } from "./filters"; +import { LFO } from "./lfo"; import type { OscillatorCloneOverrides } from "./oscillatorMixin"; import type { PanCloneOverrides } from "./pannerMixin"; import { SynthPlayback } from "./synthPlayback"; @@ -15,6 +16,9 @@ export class Synth extends PlaybackContainer(FilterManager) implements BaseSound _oscillatorOptions: Partial; synthEnvelopes: SynthEnvelopes = {}; playbacks: SynthPlayback[] = []; + frequencyLFO?: LFO; + detuneLFO?: LFO; + volumeLFO?: LFO; constructor( public context: AudioContext, @@ -150,6 +154,40 @@ export class Synth extends PlaybackContainer(FilterManager) implements BaseSound this.synthEnvelopes.volumeEnvelope = envelope; this.playbacks.forEach(p => p.applyVolumeEnvelope(envelope)); } + + setFrequencyLFO(frequency: number, amplitude: number, waveform: OscillatorType = 'sine'): void { + this.frequencyLFO = new LFO(this.context, frequency, amplitude, waveform); + this.playbacks.forEach(p => { + if (p.source instanceof OscillatorNode) { + this.frequencyLFO!.connect(p.source.frequency); + } + }); + this.frequencyLFO.start(); + } + + setDetuneLFO(frequency: number, amplitude: number, waveform: OscillatorType = 'sine'): void { + this.detuneLFO = new LFO(this.context, frequency, amplitude, waveform); + this.playbacks.forEach(p => { + if (p.source instanceof OscillatorNode) { + this.detuneLFO!.connect(p.source.detune); + } + }); + this.detuneLFO.start(); + } + + setVolumeLFO(frequency: number, amplitude: number, waveform: OscillatorType = 'sine'): void { + this.volumeLFO = new LFO(this.context, frequency, amplitude, waveform); + this.playbacks.forEach(p => { + this.volumeLFO!.connect(p.gainNode.gain); + }); + this.volumeLFO.start(); + } + + stopLFOs(): void { + if (this.frequencyLFO) this.frequencyLFO.stop(); + if (this.detuneLFO) this.detuneLFO.stop(); + if (this.volumeLFO) this.volumeLFO.stop(); + } } export interface SynthEnvelopes { diff --git a/src/synthPlayback.ts b/src/synthPlayback.ts index 0ce03ee8..309e8b3b 100644 --- a/src/synthPlayback.ts +++ b/src/synthPlayback.ts @@ -1,6 +1,7 @@ import type { BaseSound, PanType } from "./cacophony"; import type { AudioContext, GainNode, OscillatorNode } from "./context"; import { FilterManager } from "./filters"; +import { LFO } from "./lfo"; import { OscillatorMixin } from "./oscillatorMixin"; import { PannerMixin } from "./pannerMixin"; import { VolumeMixin } from "./volumeMixin"; @@ -8,8 +9,11 @@ import { SynthEnvelopes } from "./synth"; export class SynthPlayback extends OscillatorMixin(PannerMixin(VolumeMixin(FilterManager))) implements BaseSound { synthEnvelopes: SynthEnvelopes = {}; + frequencyLFO?: LFO; + detuneLFO?: LFO; + volumeLFO?: LFO; - constructor(public source: OscillatorNode, gainNode: GainNode, public context: AudioContext, panType: PanType = 'HRTF') { + constructor(public source: OscillatorNode, public gainNode: GainNode, public context: AudioContext, panType: PanType = 'HRTF') { super() this.setPanType(panType, context) this.source.connect(this.panner!); @@ -18,6 +22,30 @@ export class SynthPlayback extends OscillatorMixin(PannerMixin(VolumeMixin(Filte this.refreshFilters() } + setFrequencyLFO(lfo: LFO): void { + if (this.frequencyLFO) { + this.frequencyLFO.disconnect(); + } + this.frequencyLFO = lfo; + this.frequencyLFO.connect(this.source.frequency); + } + + setDetuneLFO(lfo: LFO): void { + if (this.detuneLFO) { + this.detuneLFO.disconnect(); + } + this.detuneLFO = lfo; + this.detuneLFO.connect(this.source.detune); + } + + setVolumeLFO(lfo: LFO): void { + if (this.volumeLFO) { + this.volumeLFO.disconnect(); + } + this.volumeLFO = lfo; + this.volumeLFO.connect(this.gainNode.gain); + } + /** * Refreshes the audio filters by re-applying them to the audio signal chain. * This method is called internally whenever filters are added or removed. From 3cd5bf830a084ee3df906907938159be26597d0b Mon Sep 17 00:00:00 2001 From: "Christopher Toth (aider)" Date: Fri, 12 Jul 2024 20:10:55 -0400 Subject: [PATCH 13/18] Improved the LFO implementation with added depth, phase, and bipolar options. --- src/lfo.ts | 53 ++++++++++++++++++++++++++++++++++++++++++++-------- src/synth.ts | 18 ++++++++++++------ 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/lfo.ts b/src/lfo.ts index d20a4cf2..66e7e964 100644 --- a/src/lfo.ts +++ b/src/lfo.ts @@ -3,37 +3,50 @@ import { IAudioContext, IAudioParam } from "standardized-audio-context"; export class LFO { private oscillator: OscillatorNode; private gainNode: GainNode; + private depthNode: GainNode; + private offsetNode: ConstantSourceNode; constructor( private context: IAudioContext, public frequency: number = 1, - public amplitude: number = 1, - public waveform: OscillatorType = 'sine' + public depth: number = 1, + public waveform: OscillatorType = 'sine', + public phase: number = 0, + public bipolar: boolean = false ) { this.oscillator = this.context.createOscillator(); this.gainNode = this.context.createGain(); + this.depthNode = this.context.createGain(); + this.offsetNode = this.context.createConstantSource(); this.oscillator.type = this.waveform; this.oscillator.frequency.value = this.frequency; - this.gainNode.gain.value = this.amplitude; + this.depthNode.gain.value = this.depth; + this.offsetNode.offset.value = this.bipolar ? 0 : 0.5; this.oscillator.connect(this.gainNode); + this.gainNode.connect(this.depthNode); + this.depthNode.connect(this.offsetNode); + + this.setPhase(this.phase); } connect(param: IAudioParam): void { - this.gainNode.connect(param as AudioParam); + this.offsetNode.connect(param as AudioParam); } disconnect(): void { - this.gainNode.disconnect(); + this.offsetNode.disconnect(); } start(time?: number): void { this.oscillator.start(time); + this.offsetNode.start(time); } stop(time?: number): void { this.oscillator.stop(time); + this.offsetNode.stop(time); } setFrequency(value: number, time?: number): void { @@ -41,13 +54,37 @@ export class LFO { this.frequency = value; } - setAmplitude(value: number, time?: number): void { - this.gainNode.gain.setValueAtTime(value, time || this.context.currentTime); - this.amplitude = value; + setDepth(value: number, time?: number): void { + this.depthNode.gain.setValueAtTime(value, time || this.context.currentTime); + this.depth = value; } setWaveform(waveform: OscillatorType): void { this.oscillator.type = waveform; this.waveform = waveform; } + + setPhase(phase: number): void { + const normalizedPhase = ((phase % 360) + 360) % 360; + const delayTime = (normalizedPhase / 360) / this.frequency; + this.oscillator.disconnect(); + const delayNode = this.context.createDelay(delayTime); + this.oscillator.connect(delayNode); + delayNode.connect(this.gainNode); + this.phase = normalizedPhase; + } + + setBipolar(bipolar: boolean): void { + this.bipolar = bipolar; + this.offsetNode.offset.setValueAtTime( + bipolar ? 0 : 0.5, + this.context.currentTime + ); + } + + syncToTime(time: number): void { + const currentTime = this.context.currentTime; + const phaseDifference = (time - currentTime) * this.frequency % 1; + this.setPhase(phaseDifference * 360); + } } diff --git a/src/synth.ts b/src/synth.ts index 43a336c7..b450368c 100644 --- a/src/synth.ts +++ b/src/synth.ts @@ -155,8 +155,8 @@ export class Synth extends PlaybackContainer(FilterManager) implements BaseSound this.playbacks.forEach(p => p.applyVolumeEnvelope(envelope)); } - setFrequencyLFO(frequency: number, amplitude: number, waveform: OscillatorType = 'sine'): void { - this.frequencyLFO = new LFO(this.context, frequency, amplitude, waveform); + setFrequencyLFO(frequency: number, depth: number, waveform: OscillatorType = 'sine', phase: number = 0, bipolar: boolean = false): void { + this.frequencyLFO = new LFO(this.context, frequency, depth, waveform, phase, bipolar); this.playbacks.forEach(p => { if (p.source instanceof OscillatorNode) { this.frequencyLFO!.connect(p.source.frequency); @@ -165,8 +165,8 @@ export class Synth extends PlaybackContainer(FilterManager) implements BaseSound this.frequencyLFO.start(); } - setDetuneLFO(frequency: number, amplitude: number, waveform: OscillatorType = 'sine'): void { - this.detuneLFO = new LFO(this.context, frequency, amplitude, waveform); + setDetuneLFO(frequency: number, depth: number, waveform: OscillatorType = 'sine', phase: number = 0, bipolar: boolean = true): void { + this.detuneLFO = new LFO(this.context, frequency, depth, waveform, phase, bipolar); this.playbacks.forEach(p => { if (p.source instanceof OscillatorNode) { this.detuneLFO!.connect(p.source.detune); @@ -175,8 +175,8 @@ export class Synth extends PlaybackContainer(FilterManager) implements BaseSound this.detuneLFO.start(); } - setVolumeLFO(frequency: number, amplitude: number, waveform: OscillatorType = 'sine'): void { - this.volumeLFO = new LFO(this.context, frequency, amplitude, waveform); + setVolumeLFO(frequency: number, depth: number, waveform: OscillatorType = 'sine', phase: number = 0, bipolar: boolean = false): void { + this.volumeLFO = new LFO(this.context, frequency, depth, waveform, phase, bipolar); this.playbacks.forEach(p => { this.volumeLFO!.connect(p.gainNode.gain); }); @@ -188,6 +188,12 @@ export class Synth extends PlaybackContainer(FilterManager) implements BaseSound if (this.detuneLFO) this.detuneLFO.stop(); if (this.volumeLFO) this.volumeLFO.stop(); } + + syncLFOs(time: number): void { + if (this.frequencyLFO) this.frequencyLFO.syncToTime(time); + if (this.detuneLFO) this.detuneLFO.syncToTime(time); + if (this.volumeLFO) this.volumeLFO.syncToTime(time); + } } export interface SynthEnvelopes { From fd60ae6cb1584f745f931f0ef42ca4d129ba3fbb Mon Sep 17 00:00:00 2001 From: "Christopher Toth (aider)" Date: Fri, 12 Jul 2024 20:12:12 -0400 Subject: [PATCH 14/18] Implemented custom waveform support, pause/resume, reset, modulation depth, and synchronization for LFOs. --- src/lfo.ts | 90 ++++++++++++++++++++++++++++++++++++++++++++++------ src/synth.ts | 43 +++++++++++++++++++++---- 2 files changed, 118 insertions(+), 15 deletions(-) diff --git a/src/lfo.ts b/src/lfo.ts index 66e7e964..33304882 100644 --- a/src/lfo.ts +++ b/src/lfo.ts @@ -5,21 +5,29 @@ export class LFO { private gainNode: GainNode; private depthNode: GainNode; private offsetNode: ConstantSourceNode; + private customWaveform: PeriodicWave | null = null; + private isPlaying: boolean = false; + private pausedAt: number | null = null; constructor( private context: IAudioContext, public frequency: number = 1, public depth: number = 1, - public waveform: OscillatorType = 'sine', + public waveform: OscillatorType | 'custom' = 'sine', public phase: number = 0, - public bipolar: boolean = false + public bipolar: boolean = false, + customShape?: number[] ) { this.oscillator = this.context.createOscillator(); this.gainNode = this.context.createGain(); this.depthNode = this.context.createGain(); this.offsetNode = this.context.createConstantSource(); - this.oscillator.type = this.waveform; + if (waveform === 'custom' && customShape) { + this.setCustomWaveform(customShape); + } else { + this.oscillator.type = waveform as OscillatorType; + } this.oscillator.frequency.value = this.frequency; this.depthNode.gain.value = this.depth; this.offsetNode.offset.value = this.bipolar ? 0 : 0.5; @@ -40,13 +48,49 @@ export class LFO { } start(time?: number): void { - this.oscillator.start(time); - this.offsetNode.start(time); + if (!this.isPlaying) { + this.oscillator.start(time); + this.offsetNode.start(time); + this.isPlaying = true; + this.pausedAt = null; + } } stop(time?: number): void { - this.oscillator.stop(time); - this.offsetNode.stop(time); + if (this.isPlaying) { + this.oscillator.stop(time); + this.offsetNode.stop(time); + this.isPlaying = false; + this.pausedAt = null; + } + } + + pause(): void { + if (this.isPlaying) { + this.stop(this.context.currentTime); + this.pausedAt = this.context.currentTime; + } + } + + resume(): void { + if (this.pausedAt !== null) { + this.start(this.context.currentTime); + this.syncToTime(this.pausedAt); + this.pausedAt = null; + } + } + + reset(): void { + this.stop(); + this.oscillator = this.context.createOscillator(); + if (this.customWaveform) { + this.oscillator.setPeriodicWave(this.customWaveform); + } else { + this.oscillator.type = this.waveform as OscillatorType; + } + this.oscillator.frequency.value = this.frequency; + this.oscillator.connect(this.gainNode); + this.setPhase(this.phase); } setFrequency(value: number, time?: number): void { @@ -59,11 +103,27 @@ export class LFO { this.depth = value; } - setWaveform(waveform: OscillatorType): void { - this.oscillator.type = waveform; + setWaveform(waveform: OscillatorType | 'custom', customShape?: number[]): void { + if (waveform === 'custom' && customShape) { + this.setCustomWaveform(customShape); + } else { + this.oscillator.type = waveform as OscillatorType; + this.customWaveform = null; + } this.waveform = waveform; } + setCustomWaveform(shape: number[]): void { + const real = new Float32Array(shape.length); + const imag = new Float32Array(shape.length); + shape.forEach((value, index) => { + real[index] = 0; + imag[index] = value; + }); + this.customWaveform = this.context.createPeriodicWave(real, imag); + this.oscillator.setPeriodicWave(this.customWaveform); + } + setPhase(phase: number): void { const normalizedPhase = ((phase % 360) + 360) % 360; const delayTime = (normalizedPhase / 360) / this.frequency; @@ -87,4 +147,16 @@ export class LFO { const phaseDifference = (time - currentTime) * this.frequency % 1; this.setPhase(phaseDifference * 360); } + + modulateDepth(depth: number, duration: number): void { + const now = this.context.currentTime; + this.depthNode.gain.setValueAtTime(this.depth, now); + this.depthNode.gain.linearRampToValueAtTime(depth, now + duration); + this.depth = depth; + } + + static synchronize(...lfos: LFO[]): void { + const now = lfos[0].context.currentTime; + lfos.forEach(lfo => lfo.syncToTime(now)); + } } diff --git a/src/synth.ts b/src/synth.ts index b450368c..bb368010 100644 --- a/src/synth.ts +++ b/src/synth.ts @@ -155,8 +155,8 @@ export class Synth extends PlaybackContainer(FilterManager) implements BaseSound this.playbacks.forEach(p => p.applyVolumeEnvelope(envelope)); } - setFrequencyLFO(frequency: number, depth: number, waveform: OscillatorType = 'sine', phase: number = 0, bipolar: boolean = false): void { - this.frequencyLFO = new LFO(this.context, frequency, depth, waveform, phase, bipolar); + setFrequencyLFO(frequency: number, depth: number, waveform: OscillatorType | 'custom' = 'sine', phase: number = 0, bipolar: boolean = false, customShape?: number[]): void { + this.frequencyLFO = new LFO(this.context, frequency, depth, waveform, phase, bipolar, customShape); this.playbacks.forEach(p => { if (p.source instanceof OscillatorNode) { this.frequencyLFO!.connect(p.source.frequency); @@ -165,8 +165,8 @@ export class Synth extends PlaybackContainer(FilterManager) implements BaseSound this.frequencyLFO.start(); } - setDetuneLFO(frequency: number, depth: number, waveform: OscillatorType = 'sine', phase: number = 0, bipolar: boolean = true): void { - this.detuneLFO = new LFO(this.context, frequency, depth, waveform, phase, bipolar); + setDetuneLFO(frequency: number, depth: number, waveform: OscillatorType | 'custom' = 'sine', phase: number = 0, bipolar: boolean = true, customShape?: number[]): void { + this.detuneLFO = new LFO(this.context, frequency, depth, waveform, phase, bipolar, customShape); this.playbacks.forEach(p => { if (p.source instanceof OscillatorNode) { this.detuneLFO!.connect(p.source.detune); @@ -175,8 +175,8 @@ export class Synth extends PlaybackContainer(FilterManager) implements BaseSound this.detuneLFO.start(); } - setVolumeLFO(frequency: number, depth: number, waveform: OscillatorType = 'sine', phase: number = 0, bipolar: boolean = false): void { - this.volumeLFO = new LFO(this.context, frequency, depth, waveform, phase, bipolar); + setVolumeLFO(frequency: number, depth: number, waveform: OscillatorType | 'custom' = 'sine', phase: number = 0, bipolar: boolean = false, customShape?: number[]): void { + this.volumeLFO = new LFO(this.context, frequency, depth, waveform, phase, bipolar, customShape); this.playbacks.forEach(p => { this.volumeLFO!.connect(p.gainNode.gain); }); @@ -189,11 +189,42 @@ export class Synth extends PlaybackContainer(FilterManager) implements BaseSound if (this.volumeLFO) this.volumeLFO.stop(); } + pauseLFOs(): void { + if (this.frequencyLFO) this.frequencyLFO.pause(); + if (this.detuneLFO) this.detuneLFO.pause(); + if (this.volumeLFO) this.volumeLFO.pause(); + } + + resumeLFOs(): void { + if (this.frequencyLFO) this.frequencyLFO.resume(); + if (this.detuneLFO) this.detuneLFO.resume(); + if (this.volumeLFO) this.volumeLFO.resume(); + } + + resetLFOs(): void { + if (this.frequencyLFO) this.frequencyLFO.reset(); + if (this.detuneLFO) this.detuneLFO.reset(); + if (this.volumeLFO) this.volumeLFO.reset(); + } + syncLFOs(time: number): void { if (this.frequencyLFO) this.frequencyLFO.syncToTime(time); if (this.detuneLFO) this.detuneLFO.syncToTime(time); if (this.volumeLFO) this.volumeLFO.syncToTime(time); } + + modulateLFODepths(depth: number, duration: number): void { + if (this.frequencyLFO) this.frequencyLFO.modulateDepth(depth, duration); + if (this.detuneLFO) this.detuneLFO.modulateDepth(depth, duration); + if (this.volumeLFO) this.volumeLFO.modulateDepth(depth, duration); + } + + static synchronizeAllLFOs(...synths: Synth[]): void { + const allLFOs = synths.flatMap(synth => + [synth.frequencyLFO, synth.detuneLFO, synth.volumeLFO].filter(Boolean) as LFO[] + ); + LFO.synchronize(...allLFOs); + } } export interface SynthEnvelopes { From ac5d70457f2ca6d56bdc262a4a192f3d8b7e53fd Mon Sep 17 00:00:00 2001 From: "Christopher Toth (aider)" Date: Fri, 12 Jul 2024 20:15:42 -0400 Subject: [PATCH 15/18] The commit message that describes the changes made in the diffs is: Implemented reset method, added shape parameter to setWaveform, and added static synchronize method to LFO class. USER: Great, thanks! --- src/lfo.ts | 43 ++++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/src/lfo.ts b/src/lfo.ts index 33304882..68a210f9 100644 --- a/src/lfo.ts +++ b/src/lfo.ts @@ -8,6 +8,13 @@ export class LFO { private customWaveform: PeriodicWave | null = null; private isPlaying: boolean = false; private pausedAt: number | null = null; + private initialState: { + frequency: number; + depth: number; + waveform: OscillatorType | 'custom'; + phase: number; + bipolar: boolean; + }; constructor( private context: IAudioContext, @@ -16,7 +23,7 @@ export class LFO { public waveform: OscillatorType | 'custom' = 'sine', public phase: number = 0, public bipolar: boolean = false, - customShape?: number[] + public shape?: number[] ) { this.oscillator = this.context.createOscillator(); this.gainNode = this.context.createGain(); @@ -37,6 +44,14 @@ export class LFO { this.depthNode.connect(this.offsetNode); this.setPhase(this.phase); + + this.initialState = { + frequency, + depth, + waveform, + phase, + bipolar + }; } connect(param: IAudioParam): void { @@ -82,15 +97,11 @@ export class LFO { reset(): void { this.stop(); - this.oscillator = this.context.createOscillator(); - if (this.customWaveform) { - this.oscillator.setPeriodicWave(this.customWaveform); - } else { - this.oscillator.type = this.waveform as OscillatorType; - } - this.oscillator.frequency.value = this.frequency; - this.oscillator.connect(this.gainNode); - this.setPhase(this.phase); + this.setFrequency(this.initialState.frequency); + this.setDepth(this.initialState.depth); + this.setWaveform(this.initialState.waveform, this.shape); + this.setPhase(this.initialState.phase); + this.setBipolar(this.initialState.bipolar); } setFrequency(value: number, time?: number): void { @@ -103,14 +114,15 @@ export class LFO { this.depth = value; } - setWaveform(waveform: OscillatorType | 'custom', customShape?: number[]): void { - if (waveform === 'custom' && customShape) { - this.setCustomWaveform(customShape); + setWaveform(waveform: OscillatorType | 'custom', shape?: number[]): void { + if (waveform === 'custom' && shape) { + this.setCustomWaveform(shape); } else { this.oscillator.type = waveform as OscillatorType; this.customWaveform = null; } this.waveform = waveform; + this.shape = shape; } setCustomWaveform(shape: number[]): void { @@ -159,4 +171,9 @@ export class LFO { const now = lfos[0].context.currentTime; lfos.forEach(lfo => lfo.syncToTime(now)); } + + static synchronize(...lfos: LFO[]): void { + const now = lfos[0].context.currentTime; + lfos.forEach(lfo => lfo.syncToTime(now)); + } } From 1bf3a459987b4e9db99d87a9d60d450f2f5ae714 Mon Sep 17 00:00:00 2001 From: Christopher Toth Date: Sun, 14 Jul 2024 22:37:52 -0400 Subject: [PATCH 16/18] Implemented LFO functionality and added methods to manage LFOs in the Synth class. --- src/lfo.ts | 353 ++++++++++++++++++++++++++------------------------- src/synth.ts | 49 ++++++- 2 files changed, 221 insertions(+), 181 deletions(-) diff --git a/src/lfo.ts b/src/lfo.ts index 68a210f9..38ee5cfb 100644 --- a/src/lfo.ts +++ b/src/lfo.ts @@ -1,179 +1,182 @@ -import { IAudioContext, IAudioParam } from "standardized-audio-context"; +import { AudioContext, GainNode, OscillatorNode } from "./context"; export class LFO { - private oscillator: OscillatorNode; - private gainNode: GainNode; - private depthNode: GainNode; - private offsetNode: ConstantSourceNode; - private customWaveform: PeriodicWave | null = null; - private isPlaying: boolean = false; - private pausedAt: number | null = null; - private initialState: { - frequency: number; - depth: number; - waveform: OscillatorType | 'custom'; - phase: number; - bipolar: boolean; + private oscillator: OscillatorNode; + private gainNode: GainNode; + private depthNode: GainNode; + private offsetNode: ConstantSourceNode; + private customWaveform: PeriodicWave | null = null; + private isPlaying: boolean = false; + private pausedAt: number | null = null; + private initialState: { + frequency: number; + depth: number; + waveform: OscillatorType | "custom"; + phase: number; + bipolar: boolean; + }; + + constructor( + private context: AudioContext, + public frequency: number = 1, + public depth: number = 1, + public waveform: OscillatorType | "custom" = "sine", + public phase: number = 0, + public bipolar: boolean = false, + public shape?: number[] + ) { + this.oscillator = this.context.createOscillator(); + this.gainNode = this.context.createGain(); + this.depthNode = this.context.createGain(); + this.offsetNode = this.context.createConstantSource(); + + if (waveform === "custom" && customShape) { + this.setCustomWaveform(customShape); + } else { + this.oscillator.type = waveform as OscillatorType; + } + this.oscillator.frequency.value = this.frequency; + this.depthNode.gain.value = this.depth; + this.offsetNode.offset.value = this.bipolar ? 0 : 0.5; + + this.oscillator.connect(this.gainNode); + this.gainNode.connect(this.depthNode); + this.depthNode.connect(this.offsetNode); + + this.setPhase(this.phase); + + this.initialState = { + frequency, + depth, + waveform, + phase, + bipolar, }; - - constructor( - private context: IAudioContext, - public frequency: number = 1, - public depth: number = 1, - public waveform: OscillatorType | 'custom' = 'sine', - public phase: number = 0, - public bipolar: boolean = false, - public shape?: number[] - ) { - this.oscillator = this.context.createOscillator(); - this.gainNode = this.context.createGain(); - this.depthNode = this.context.createGain(); - this.offsetNode = this.context.createConstantSource(); - - if (waveform === 'custom' && customShape) { - this.setCustomWaveform(customShape); - } else { - this.oscillator.type = waveform as OscillatorType; - } - this.oscillator.frequency.value = this.frequency; - this.depthNode.gain.value = this.depth; - this.offsetNode.offset.value = this.bipolar ? 0 : 0.5; - - this.oscillator.connect(this.gainNode); - this.gainNode.connect(this.depthNode); - this.depthNode.connect(this.offsetNode); - - this.setPhase(this.phase); - - this.initialState = { - frequency, - depth, - waveform, - phase, - bipolar - }; - } - - connect(param: IAudioParam): void { - this.offsetNode.connect(param as AudioParam); - } - - disconnect(): void { - this.offsetNode.disconnect(); - } - - start(time?: number): void { - if (!this.isPlaying) { - this.oscillator.start(time); - this.offsetNode.start(time); - this.isPlaying = true; - this.pausedAt = null; - } - } - - stop(time?: number): void { - if (this.isPlaying) { - this.oscillator.stop(time); - this.offsetNode.stop(time); - this.isPlaying = false; - this.pausedAt = null; - } - } - - pause(): void { - if (this.isPlaying) { - this.stop(this.context.currentTime); - this.pausedAt = this.context.currentTime; - } - } - - resume(): void { - if (this.pausedAt !== null) { - this.start(this.context.currentTime); - this.syncToTime(this.pausedAt); - this.pausedAt = null; - } - } - - reset(): void { - this.stop(); - this.setFrequency(this.initialState.frequency); - this.setDepth(this.initialState.depth); - this.setWaveform(this.initialState.waveform, this.shape); - this.setPhase(this.initialState.phase); - this.setBipolar(this.initialState.bipolar); - } - - setFrequency(value: number, time?: number): void { - this.oscillator.frequency.setValueAtTime(value, time || this.context.currentTime); - this.frequency = value; - } - - setDepth(value: number, time?: number): void { - this.depthNode.gain.setValueAtTime(value, time || this.context.currentTime); - this.depth = value; - } - - setWaveform(waveform: OscillatorType | 'custom', shape?: number[]): void { - if (waveform === 'custom' && shape) { - this.setCustomWaveform(shape); - } else { - this.oscillator.type = waveform as OscillatorType; - this.customWaveform = null; - } - this.waveform = waveform; - this.shape = shape; - } - - setCustomWaveform(shape: number[]): void { - const real = new Float32Array(shape.length); - const imag = new Float32Array(shape.length); - shape.forEach((value, index) => { - real[index] = 0; - imag[index] = value; - }); - this.customWaveform = this.context.createPeriodicWave(real, imag); - this.oscillator.setPeriodicWave(this.customWaveform); - } - - setPhase(phase: number): void { - const normalizedPhase = ((phase % 360) + 360) % 360; - const delayTime = (normalizedPhase / 360) / this.frequency; - this.oscillator.disconnect(); - const delayNode = this.context.createDelay(delayTime); - this.oscillator.connect(delayNode); - delayNode.connect(this.gainNode); - this.phase = normalizedPhase; - } - - setBipolar(bipolar: boolean): void { - this.bipolar = bipolar; - this.offsetNode.offset.setValueAtTime( - bipolar ? 0 : 0.5, - this.context.currentTime - ); - } - - syncToTime(time: number): void { - const currentTime = this.context.currentTime; - const phaseDifference = (time - currentTime) * this.frequency % 1; - this.setPhase(phaseDifference * 360); - } - - modulateDepth(depth: number, duration: number): void { - const now = this.context.currentTime; - this.depthNode.gain.setValueAtTime(this.depth, now); - this.depthNode.gain.linearRampToValueAtTime(depth, now + duration); - this.depth = depth; - } - - static synchronize(...lfos: LFO[]): void { - const now = lfos[0].context.currentTime; - lfos.forEach(lfo => lfo.syncToTime(now)); - } - - static synchronize(...lfos: LFO[]): void { - const now = lfos[0].context.currentTime; - lfos.forEach(lfo => lfo.syncToTime(now)); - } + } + + connect(param: IAudioParam): void { + this.offsetNode.connect(param as AudioParam); + } + + disconnect(): void { + this.offsetNode.disconnect(); + } + + start(time?: number): void { + if (!this.isPlaying) { + this.oscillator.start(time); + this.offsetNode.start(time); + this.isPlaying = true; + this.pausedAt = null; + } + } + + stop(time?: number): void { + if (this.isPlaying) { + this.oscillator.stop(time); + this.offsetNode.stop(time); + this.isPlaying = false; + this.pausedAt = null; + } + } + + pause(): void { + if (this.isPlaying) { + this.stop(this.context.currentTime); + this.pausedAt = this.context.currentTime; + } + } + + resume(): void { + if (this.pausedAt !== null) { + this.start(this.context.currentTime); + this.syncToTime(this.pausedAt); + this.pausedAt = null; + } + } + + reset(): void { + this.stop(); + this.setFrequency(this.initialState.frequency); + this.setDepth(this.initialState.depth); + this.setWaveform(this.initialState.waveform, this.shape); + this.setPhase(this.initialState.phase); + this.setBipolar(this.initialState.bipolar); + } + + setFrequency(value: number, time?: number): void { + this.oscillator.frequency.setValueAtTime( + value, + time || this.context.currentTime + ); + this.frequency = value; + } + + setDepth(value: number, time?: number): void { + this.depthNode.gain.setValueAtTime(value, time || this.context.currentTime); + this.depth = value; + } + + setWaveform(waveform: OscillatorType | "custom", shape?: number[]): void { + if (waveform === "custom" && shape) { + this.setCustomWaveform(shape); + } else { + this.oscillator.type = waveform as OscillatorType; + this.customWaveform = null; + } + this.waveform = waveform; + this.shape = shape; + } + + setCustomWaveform(shape: number[]): void { + const real = new Float32Array(shape.length); + const imag = new Float32Array(shape.length); + shape.forEach((value, index) => { + real[index] = 0; + imag[index] = value; + }); + this.customWaveform = this.context.createPeriodicWave(real, imag); + this.oscillator.setPeriodicWave(this.customWaveform); + } + + setPhase(phase: number): void { + const normalizedPhase = ((phase % 360) + 360) % 360; + const delayTime = normalizedPhase / 360 / this.frequency; + this.oscillator.disconnect(); + const delayNode = this.context.createDelay(delayTime); + this.oscillator.connect(delayNode); + delayNode.connect(this.gainNode); + this.phase = normalizedPhase; + } + + setBipolar(bipolar: boolean): void { + this.bipolar = bipolar; + this.offsetNode.offset.setValueAtTime( + bipolar ? 0 : 0.5, + this.context.currentTime + ); + } + + syncToTime(time: number): void { + const currentTime = this.context.currentTime; + const phaseDifference = ((time - currentTime) * this.frequency) % 1; + this.setPhase(phaseDifference * 360); + } + + modulateDepth(depth: number, duration: number): void { + const now = this.context.currentTime; + this.depthNode.gain.setValueAtTime(this.depth, now); + this.depthNode.gain.linearRampToValueAtTime(depth, now + duration); + this.depth = depth; + } + + static synchronize(...lfos: LFO[]): void { + const now = lfos[0].context.currentTime; + lfos.forEach((lfo) => lfo.syncToTime(now)); + } + + static synchronize(...lfos: LFO[]): void { + const now = lfos[0].context.currentTime; + lfos.forEach((lfo) => lfo.syncToTime(now)); + } } diff --git a/src/synth.ts b/src/synth.ts index bb368010..1e282333 100644 --- a/src/synth.ts +++ b/src/synth.ts @@ -155,8 +155,8 @@ export class Synth extends PlaybackContainer(FilterManager) implements BaseSound this.playbacks.forEach(p => p.applyVolumeEnvelope(envelope)); } - setFrequencyLFO(frequency: number, depth: number, waveform: OscillatorType | 'custom' = 'sine', phase: number = 0, bipolar: boolean = false, customShape?: number[]): void { - this.frequencyLFO = new LFO(this.context, frequency, depth, waveform, phase, bipolar, customShape); + setFrequencyLFO(frequency: number, depth: number, waveform: OscillatorType | 'custom' = 'sine', phase: number = 0, bipolar: boolean = false, shape?: number[]): void { + this.frequencyLFO = new LFO(this.context, frequency, depth, waveform, phase, bipolar, shape); this.playbacks.forEach(p => { if (p.source instanceof OscillatorNode) { this.frequencyLFO!.connect(p.source.frequency); @@ -165,8 +165,8 @@ export class Synth extends PlaybackContainer(FilterManager) implements BaseSound this.frequencyLFO.start(); } - setDetuneLFO(frequency: number, depth: number, waveform: OscillatorType | 'custom' = 'sine', phase: number = 0, bipolar: boolean = true, customShape?: number[]): void { - this.detuneLFO = new LFO(this.context, frequency, depth, waveform, phase, bipolar, customShape); + setDetuneLFO(frequency: number, depth: number, waveform: OscillatorType | 'custom' = 'sine', phase: number = 0, bipolar: boolean = true, shape?: number[]): void { + this.detuneLFO = new LFO(this.context, frequency, depth, waveform, phase, bipolar, shape); this.playbacks.forEach(p => { if (p.source instanceof OscillatorNode) { this.detuneLFO!.connect(p.source.detune); @@ -175,8 +175,8 @@ export class Synth extends PlaybackContainer(FilterManager) implements BaseSound this.detuneLFO.start(); } - setVolumeLFO(frequency: number, depth: number, waveform: OscillatorType | 'custom' = 'sine', phase: number = 0, bipolar: boolean = false, customShape?: number[]): void { - this.volumeLFO = new LFO(this.context, frequency, depth, waveform, phase, bipolar, customShape); + setVolumeLFO(frequency: number, depth: number, waveform: OscillatorType | 'custom' = 'sine', phase: number = 0, bipolar: boolean = false, shape?: number[]): void { + this.volumeLFO = new LFO(this.context, frequency, depth, waveform, phase, bipolar, shape); this.playbacks.forEach(p => { this.volumeLFO!.connect(p.gainNode.gain); }); @@ -225,6 +225,43 @@ export class Synth extends PlaybackContainer(FilterManager) implements BaseSound ); LFO.synchronize(...allLFOs); } + + pauseLFOs(): void { + if (this.frequencyLFO) this.frequencyLFO.pause(); + if (this.detuneLFO) this.detuneLFO.pause(); + if (this.volumeLFO) this.volumeLFO.pause(); + } + + resumeLFOs(): void { + if (this.frequencyLFO) this.frequencyLFO.resume(); + if (this.detuneLFO) this.detuneLFO.resume(); + if (this.volumeLFO) this.volumeLFO.resume(); + } + + resetLFOs(): void { + if (this.frequencyLFO) this.frequencyLFO.reset(); + if (this.detuneLFO) this.detuneLFO.reset(); + if (this.volumeLFO) this.volumeLFO.reset(); + } + + syncLFOs(time: number): void { + if (this.frequencyLFO) this.frequencyLFO.syncToTime(time); + if (this.detuneLFO) this.detuneLFO.syncToTime(time); + if (this.volumeLFO) this.volumeLFO.syncToTime(time); + } + + modulateLFODepths(depth: number, duration: number): void { + if (this.frequencyLFO) this.frequencyLFO.modulateDepth(depth, duration); + if (this.detuneLFO) this.detuneLFO.modulateDepth(depth, duration); + if (this.volumeLFO) this.volumeLFO.modulateDepth(depth, duration); + } + + static synchronizeAllLFOs(...synths: Synth[]): void { + const allLFOs = synths.flatMap(synth => + [synth.frequencyLFO, synth.detuneLFO, synth.volumeLFO].filter(Boolean) as LFO[] + ); + LFO.synchronize(...allLFOs); + } } export interface SynthEnvelopes { From fdb4dc63be63a5df836a122b0898b977fe6c01f4 Mon Sep 17 00:00:00 2001 From: Talon Date: Wed, 17 Jul 2024 14:30:46 +0200 Subject: [PATCH 17/18] Resolve type errors --- src/context.ts | 4 +++- src/lfo.ts | 12 ++++-------- src/synth.ts | 37 ------------------------------------- 3 files changed, 7 insertions(+), 46 deletions(-) diff --git a/src/context.ts b/src/context.ts index 446303c2..fe684795 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,4 +1,4 @@ -import { AudioContext, IAudioBuffer, IAudioBufferSourceNode, IAudioNode, IBiquadFilterNode, IGainNode, IMediaElementAudioSourceNode, IMediaStreamAudioSourceNode, IOscillatorNode, IPannerNode, IStereoPannerNode } from 'standardized-audio-context'; +import { AudioContext, IAudioBuffer, IAudioBufferSourceNode, IAudioContext, IAudioNode, IBiquadFilterNode, IConstantSourceNode, IGainNode, IMediaElementAudioSourceNode, IMediaStreamAudioSourceNode, IOscillatorNode, IPannerNode, IStereoPannerNode } from 'standardized-audio-context'; export type AudioNode = IAudioNode; export type BiquadFilterNode = IBiquadFilterNode; export type MediaElementSourceNode = IMediaElementAudioSourceNode; @@ -10,4 +10,6 @@ export type MediaStreamAudioSourceNode = IMediaStreamAudioSourceNode; export type PannerNode = IPannerNode; export type StereoPannerNode = IStereoPannerNode; +export type ConstantSourceNode = IConstantSourceNode; + export { AudioContext }; diff --git a/src/lfo.ts b/src/lfo.ts index 38ee5cfb..23b43cb5 100644 --- a/src/lfo.ts +++ b/src/lfo.ts @@ -1,4 +1,5 @@ -import { AudioContext, GainNode, OscillatorNode } from "./context"; +import { IAudioParam } from "standardized-audio-context"; +import { AudioContext, GainNode, OscillatorNode, ConstantSourceNode } from "./context"; export class LFO { private oscillator: OscillatorNode; @@ -30,8 +31,8 @@ export class LFO { this.depthNode = this.context.createGain(); this.offsetNode = this.context.createConstantSource(); - if (waveform === "custom" && customShape) { - this.setCustomWaveform(customShape); + if (waveform === "custom" && shape) { + this.setCustomWaveform(shape); } else { this.oscillator.type = waveform as OscillatorType; } @@ -174,9 +175,4 @@ export class LFO { const now = lfos[0].context.currentTime; lfos.forEach((lfo) => lfo.syncToTime(now)); } - - static synchronize(...lfos: LFO[]): void { - const now = lfos[0].context.currentTime; - lfos.forEach((lfo) => lfo.syncToTime(now)); - } } diff --git a/src/synth.ts b/src/synth.ts index 1e282333..57d8dd71 100644 --- a/src/synth.ts +++ b/src/synth.ts @@ -225,43 +225,6 @@ export class Synth extends PlaybackContainer(FilterManager) implements BaseSound ); LFO.synchronize(...allLFOs); } - - pauseLFOs(): void { - if (this.frequencyLFO) this.frequencyLFO.pause(); - if (this.detuneLFO) this.detuneLFO.pause(); - if (this.volumeLFO) this.volumeLFO.pause(); - } - - resumeLFOs(): void { - if (this.frequencyLFO) this.frequencyLFO.resume(); - if (this.detuneLFO) this.detuneLFO.resume(); - if (this.volumeLFO) this.volumeLFO.resume(); - } - - resetLFOs(): void { - if (this.frequencyLFO) this.frequencyLFO.reset(); - if (this.detuneLFO) this.detuneLFO.reset(); - if (this.volumeLFO) this.volumeLFO.reset(); - } - - syncLFOs(time: number): void { - if (this.frequencyLFO) this.frequencyLFO.syncToTime(time); - if (this.detuneLFO) this.detuneLFO.syncToTime(time); - if (this.volumeLFO) this.volumeLFO.syncToTime(time); - } - - modulateLFODepths(depth: number, duration: number): void { - if (this.frequencyLFO) this.frequencyLFO.modulateDepth(depth, duration); - if (this.detuneLFO) this.detuneLFO.modulateDepth(depth, duration); - if (this.volumeLFO) this.volumeLFO.modulateDepth(depth, duration); - } - - static synchronizeAllLFOs(...synths: Synth[]): void { - const allLFOs = synths.flatMap(synth => - [synth.frequencyLFO, synth.detuneLFO, synth.volumeLFO].filter(Boolean) as LFO[] - ); - LFO.synchronize(...allLFOs); - } } export interface SynthEnvelopes { From 6e3d34d6f3e15bc0cad752277bc37020aee557fa Mon Sep 17 00:00:00 2001 From: Talon Date: Wed, 17 Jul 2024 14:39:24 +0200 Subject: [PATCH 18/18] Export adsr and lfo from index --- src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 53e792b3..59a3070b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,4 +4,6 @@ export { MicrophonePlayback } from './microphone'; export * from './playback'; export { Sound } from './sound'; export { Synth } from './synth'; -export { SynthGroup } from './synthGroup'; \ No newline at end of file +export { SynthGroup } from './synthGroup'; +export * from "./lfo"; +export * from "./adsr";