From ee71013f938d3c8477235b540b7627b7256d23dd Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Thu, 8 Aug 2024 14:53:25 -0700 Subject: [PATCH] AudioGenerator: TR909 --- src/common/util/audio/AudioGenerator.ts | 211 +++++++++++++++++++++++- 1 file changed, 209 insertions(+), 2 deletions(-) diff --git a/src/common/util/audio/AudioGenerator.ts b/src/common/util/audio/AudioGenerator.ts index ea14a3a83..3894f7901 100644 --- a/src/common/util/audio/AudioGenerator.ts +++ b/src/common/util/audio/AudioGenerator.ts @@ -175,6 +175,29 @@ export namespace AudioGenerator { // o.stop(ctx.currentTime + 0.5); // } + export function basicNote(note: string = 'C4', duration: number = 1, options: SoundOptions = {}) { + const ctx = singleContext(); + if (!ctx) return; + + const now = ctx.currentTime; + const o = ctx.createOscillator(); + const g = ctx.createGain(); + + o.type = 'sine'; + o.frequency.setValueAtTime(noteToFrequency(note), now); + + g.gain.setValueAtTime(0, now); + g.gain.linearRampToValueAtTime(options.volume || 0.3, now + 0.1); + g.gain.exponentialRampToValueAtTime(0.001, now + duration); + + o.connect(g); + g.connect(agMasterGain); + // applyRoomAcoustics(ctx, g, options.roomSize || 'small'); + + o.start(now); + o.stop(now + duration); + } + export function basicRandomSound(options: SoundOptions = {}): void { const ctx = singleContext(); if (!ctx) return; @@ -353,6 +376,171 @@ export namespace AudioGenerator { } +// export namespace TR909 { +// interface TR909Options { +// volume?: number; +// pitch?: number; +// decay?: number; +// tone?: number; +// noiseType?: 'white' | 'pink'; +// } +// +// export function kick(options: TR909Options = {}): void { +// const ctx = singleContext(); +// if (!ctx) return; +// +// const now = ctx.currentTime; +// const { pitch = 80, decay = 0.35, tone = 0.40, noiseType = 'white' } = options; +// +// // oscillator +// const o = ctx.createOscillator(); +// o.type = 'sine'; +// o.frequency.setValueAtTime(pitch, now); +// o.frequency.exponentialRampToValueAtTime(pitch * 0.5, now + 0.15); +// +// const og = ctx.createGain(); +// createEnvelope(ctx, og.gain, 0.001, decay, 0.1, 0.1); +// o.connect(og); +// +// // noise +// const n = createNoise(ctx, decay + 0.1, noiseType); +// +// const ng = ctx.createGain(); +// createEnvelope(ctx, ng.gain, 0.001, 0.05, 0.1, 0.05); +// n.connect(ng); +// +// // filter +// const oToneFilter = ctx.createBiquadFilter(); +// oToneFilter.type = 'lowpass'; +// oToneFilter.frequency.setValueAtTime(1000 * tone, now); +// og.connect(oToneFilter); +// ng.connect(oToneFilter); +// +// const compressor = ctx.createDynamicsCompressor(); +// oToneFilter.connect(compressor); +// compressor.connect(agMasterGain); +// +// o.start(now); +// n.start(now); +// o.stop(now + decay + 0.2); +// n.stop(now + decay + 0.1); +// } +// +// export function snare(options: TR909Options = {}): void { +// const ctx = singleContext(); +// if (!ctx) return; +// +// const now = ctx.currentTime; +// const { pitch = 150, decay = 0.2, tone = 1, noiseType = 'white' } = options; +// +// // Oscillator +// const o = ctx.createOscillator(); +// o.type = 'triangle'; +// o.frequency.setValueAtTime(pitch, now); +// +// const oscGain = ctx.createGain(); +// createEnvelope(ctx, oscGain.gain, 0.001, 0.06, 0, 0.1); +// o.connect(oscGain); +// +// const oToneFilter = ctx.createBiquadFilter(); +// oToneFilter.type = 'bandpass'; +// oToneFilter.frequency.setValueAtTime(400 * tone, now); +// oToneFilter.Q.setValueAtTime(1, now); +// oscGain.connect(oToneFilter); +// +// // Noise +// const n = createNoise(ctx, decay + 0.1, noiseType); +// +// const ng = ctx.createGain(); +// createEnvelope(ctx, ng.gain, 0.001, decay, 0, 0.01, 0.2); +// n.connect(ng); +// +// const nHighpassFilter = ctx.createBiquadFilter(); +// nHighpassFilter.type = 'highpass'; +// nHighpassFilter.frequency.setValueAtTime(2000, now); +// ng.connect(nHighpassFilter); +// +// +// // Compressor +// const compressor = ctx.createDynamicsCompressor(); +// oToneFilter.connect(compressor); +// nHighpassFilter.connect(compressor); +// compressor.connect(agMasterGain); +// +// n.start(now); +// o.start(now); +// n.stop(now + decay + 0.2); +// o.stop(now + decay + 0.2); +// } +// +// export function hihat(options: TR909Options = {}): void { +// const ctx = singleContext(); +// if (!ctx) return; +// +// const now = ctx.currentTime; +// const { decay = 0.08, tone = 1.5, noiseType = 'white' } = options; +// +// // noise +// const noise = createNoise(ctx, decay + 0.05, noiseType); +// const noiseGain = ctx.createGain(); +// createEnvelope(ctx, noiseGain.gain, 0.001, decay, 0.1, 0.05); +// noise.connect(noiseGain); +// +// // noise filter +// const nHighpassFilter = ctx.createBiquadFilter(); +// nHighpassFilter.type = 'highpass'; +// nHighpassFilter.frequency.setValueAtTime(7000 * tone, now); +// noiseGain.connect(nHighpassFilter); +// +// // compressor +// const compressor = ctx.createDynamicsCompressor(); +// nHighpassFilter.connect(compressor); +// compressor.connect(agMasterGain); +// +// noise.start(now); +// noise.stop(now + decay + 0.05); +// } +// +// export function clap(options: TR909Options = {}): void { +// const ctx = singleContext(); +// if (!ctx) return; +// +// const now = ctx.currentTime; +// const { decay = 0.2, tone = 1, noiseType = 'white' } = options; +// +// // noise 1 +// const n1 = createNoise(ctx, 0.05, noiseType); +// const n1gain = ctx.createGain(); +// createEnvelope(ctx, n1gain.gain, 0.001, 0.03, 0, 0.02); +// n1.connect(n1gain); +// +// // noise 2 +// const n2 = createNoise(ctx, decay, noiseType); +// const n2gain = ctx.createGain(); +// createEnvelope(ctx, n2gain.gain, 0.02, decay - 0.02, 0.1, 0.05); +// n2.connect(n2gain); +// +// // n1 + n2 filter +// const bandpassFilter = ctx.createBiquadFilter(); +// bandpassFilter.type = 'bandpass'; +// bandpassFilter.frequency.setValueAtTime(1000 * tone, now); +// bandpassFilter.Q.setValueAtTime(1.6, now); +// n1gain.connect(bandpassFilter); +// n2gain.connect(bandpassFilter); +// +// // compressor +// const compressor = ctx.createDynamicsCompressor(); +// bandpassFilter.connect(compressor); +// compressor.connect(agMasterGain); +// +// n1.start(now); +// n2.start(now + 0.02); +// n1.stop(now + 0.05); +// n2.stop(now + decay); +// } +// } + + /// Utility Functions /// function applyRoomAcoustics(ctx: AudioContext, source: AudioNode, roomSize: 'small' | 'large' = 'small'): void { @@ -380,10 +568,10 @@ function applyRoomAcoustics(ctx: AudioContext, source: AudioNode, roomSize: 'sma source.connect(agMasterGain); } -function createEnvelope(ctx: AudioContext, param: AudioParam, attackTime: number, decayTime: number, sustainLevel: number, releaseTime: number): void { +function createEnvelope(ctx: AudioContext, param: AudioParam, attackTime: number, decayTime: number, sustainLevel: number, releaseTime: number, amplitude?: number): void { const now = ctx.currentTime; param.setValueAtTime(0, now); - param.linearRampToValueAtTime(1, now + attackTime); + param.linearRampToValueAtTime(amplitude !== undefined ? amplitude : 1, now + attackTime); param.linearRampToValueAtTime(sustainLevel, now + attackTime + decayTime); param.linearRampToValueAtTime(0, now + attackTime + decayTime + releaseTime); } @@ -422,6 +610,25 @@ function createNoise(ctx: AudioContext, duration: number, type: 'white' | 'pink' return noiseSource; } +function noteToFrequency(note: string /* = 'C4' */): number { + const notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; + const octave = parseInt(note.slice(-1)); + const keyNumber = notes.indexOf(note.slice(0, -1)); + + if (keyNumber === -1) throw new Error('Invalid note'); + + // A4 is 440 Hz + const a4 = 440; + + // Calculate half steps from A4 + const halfStepsFromA4 = (octave - 4) * 12 + keyNumber - 9; + + // Formula: f = 440 * (2^(1/12))^n + // Where n is the number of half steps from A4 + return a4 * Math.pow(2, halfStepsFromA4 / 12); +} + + // (Single) Global Audio Generation Context let agCtx: AudioContext; let agMasterGain: GainNode;