clean up
This commit is contained in:
parent
d6a8f898d1
commit
15e99380b5
84 changed files with 1659 additions and 4726 deletions
|
|
@ -1,7 +1,38 @@
|
|||
import { clamp } from '../utils/clamp';
|
||||
import { GardenAudioConfig, GardenAudioVibeProfile } from './garden-audio-config';
|
||||
import { clamp } from '../utils/math';
|
||||
import { isIosLike } from './audio-platform';
|
||||
import type { GardenAudioConfig, GardenAudioVibeProfile } from './garden-audio-config';
|
||||
import type { PianoNoteRole } from './garden-audio-types';
|
||||
|
||||
const outputHighPassFrequencyHz = 45;
|
||||
const graphTuning = {
|
||||
closeGain: 0.0001,
|
||||
closeRampSeconds: 0.015,
|
||||
delayMaxSeconds: 2,
|
||||
eventBusGain: 1,
|
||||
noiseMax: 1,
|
||||
noiseMin: -1,
|
||||
latencyHint: 'interactive' as AudioContextLatencyCategory,
|
||||
outputFilterType: 'highpass' as BiquadFilterType,
|
||||
noiseBufferChannels: 1,
|
||||
noiseBufferDurationSeconds: 1,
|
||||
unlockTickFrequencyHz: 440,
|
||||
unlockTickGain: 0.0001,
|
||||
unlockTickSeconds: 0.025,
|
||||
unlockTickType: 'sine' as OscillatorType,
|
||||
compressor: {
|
||||
thresholdDb: -18,
|
||||
kneeDb: 18,
|
||||
ratio: 2.1,
|
||||
attackSeconds: 0.018,
|
||||
releaseSeconds: 0.18,
|
||||
},
|
||||
};
|
||||
const delayFilterTuning = {
|
||||
feedbackHighPassHz: 180,
|
||||
feedbackLowPassHz: 5200,
|
||||
returnLowPassHz: 6200,
|
||||
};
|
||||
|
||||
export class GardenAudioGraph {
|
||||
public context: AudioContext | null = null;
|
||||
public eventBus: GainNode | null = null;
|
||||
|
|
@ -13,6 +44,8 @@ export class GardenAudioGraph {
|
|||
private delayNode: DelayNode | null = null;
|
||||
private delayFeedback: GainNode | null = null;
|
||||
private delayOutput: GainNode | null = null;
|
||||
private mediaStreamDestination: MediaStreamAudioDestinationNode | null = null;
|
||||
private mediaStreamElement: HTMLAudioElement | null = null;
|
||||
private readonly pianoBuses = new Map<PianoNoteRole, GainNode>();
|
||||
|
||||
public constructor(private readonly config: GardenAudioConfig) {}
|
||||
|
|
@ -34,7 +67,7 @@ export class GardenAudioGraph {
|
|||
let context: AudioContext;
|
||||
try {
|
||||
context = new AudioContextConstructor({
|
||||
latencyHint: this.config.graph.latencyHint,
|
||||
latencyHint: graphTuning.latencyHint,
|
||||
});
|
||||
} catch {
|
||||
context = new AudioContextConstructor();
|
||||
|
|
@ -42,22 +75,30 @@ export class GardenAudioGraph {
|
|||
const masterGain = context.createGain();
|
||||
const highPass = context.createBiquadFilter();
|
||||
const compressor = context.createDynamicsCompressor();
|
||||
const mediaStreamDestination = isIosLike()
|
||||
? context.createMediaStreamDestination()
|
||||
: null;
|
||||
|
||||
masterGain.gain.value = 0;
|
||||
highPass.type = this.config.graph.outputFilterType;
|
||||
highPass.frequency.value = this.config.highPassFrequencyHz;
|
||||
compressor.threshold.value = this.config.graph.compressor.thresholdDb;
|
||||
compressor.knee.value = this.config.graph.compressor.kneeDb;
|
||||
compressor.ratio.value = this.config.graph.compressor.ratio;
|
||||
compressor.attack.value = this.config.graph.compressor.attackSeconds;
|
||||
compressor.release.value = this.config.graph.compressor.releaseSeconds;
|
||||
highPass.type = graphTuning.outputFilterType;
|
||||
highPass.frequency.value = outputHighPassFrequencyHz;
|
||||
compressor.threshold.value = graphTuning.compressor.thresholdDb;
|
||||
compressor.knee.value = graphTuning.compressor.kneeDb;
|
||||
compressor.ratio.value = graphTuning.compressor.ratio;
|
||||
compressor.attack.value = graphTuning.compressor.attackSeconds;
|
||||
compressor.release.value = graphTuning.compressor.releaseSeconds;
|
||||
|
||||
masterGain.connect(highPass);
|
||||
highPass.connect(compressor);
|
||||
compressor.connect(context.destination);
|
||||
if (mediaStreamDestination) {
|
||||
compressor.connect(mediaStreamDestination);
|
||||
} else {
|
||||
compressor.connect(context.destination);
|
||||
}
|
||||
|
||||
this.context = context;
|
||||
this.masterGain = masterGain;
|
||||
this.mediaStreamDestination = mediaStreamDestination;
|
||||
this.noiseBuffer = this.createNoiseBuffer(context);
|
||||
this.createDelay(context, masterGain);
|
||||
this.createBuses(context, masterGain);
|
||||
|
|
@ -77,17 +118,13 @@ export class GardenAudioGraph {
|
|||
const source = this.context.createOscillator();
|
||||
const gain = this.context.createGain();
|
||||
|
||||
source.type = this.config.graph.unlockTickType;
|
||||
source.frequency.setValueAtTime(this.config.graph.unlockTickFrequencyHz, now);
|
||||
gain.gain.setValueAtTime(this.config.piano.minGain, now);
|
||||
gain.gain.exponentialRampToValueAtTime(
|
||||
this.config.piano.minGain,
|
||||
now + this.config.graph.unlockTickSeconds
|
||||
);
|
||||
source.type = graphTuning.unlockTickType;
|
||||
source.frequency.setValueAtTime(graphTuning.unlockTickFrequencyHz, now);
|
||||
gain.gain.setValueAtTime(graphTuning.unlockTickGain, now);
|
||||
source.connect(gain);
|
||||
gain.connect(this.context.destination);
|
||||
source.start(now);
|
||||
source.stop(now + this.config.graph.unlockTickSeconds);
|
||||
source.stop(now + graphTuning.unlockTickSeconds);
|
||||
source.addEventListener(
|
||||
'ended',
|
||||
() => {
|
||||
|
|
@ -110,6 +147,38 @@ export class GardenAudioGraph {
|
|||
);
|
||||
}
|
||||
|
||||
public startMediaElementOutput(): void {
|
||||
if (!this.mediaStreamDestination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaElement = this.ensureMediaStreamElement();
|
||||
const playPromise = mediaElement.play();
|
||||
void playPromise?.catch(() => undefined);
|
||||
}
|
||||
|
||||
private ensureMediaStreamElement(): HTMLAudioElement {
|
||||
if (this.mediaStreamElement) {
|
||||
return this.mediaStreamElement;
|
||||
}
|
||||
|
||||
const mediaElement = document.createElement('audio');
|
||||
mediaElement.autoplay = true;
|
||||
mediaElement.volume = 1;
|
||||
mediaElement.setAttribute('playsinline', '');
|
||||
mediaElement.setAttribute('aria-hidden', 'true');
|
||||
mediaElement.style.position = 'fixed';
|
||||
mediaElement.style.width = '1px';
|
||||
mediaElement.style.height = '1px';
|
||||
mediaElement.style.opacity = '0';
|
||||
mediaElement.style.pointerEvents = 'none';
|
||||
mediaElement.style.left = '-9999px';
|
||||
mediaElement.srcObject = this.mediaStreamDestination?.stream ?? null;
|
||||
document.body.append(mediaElement);
|
||||
this.mediaStreamElement = mediaElement;
|
||||
return mediaElement;
|
||||
}
|
||||
|
||||
public applyDelayProfile(profile: GardenAudioVibeProfile): void {
|
||||
if (!this.context || !this.delayNode) {
|
||||
return;
|
||||
|
|
@ -167,9 +236,9 @@ export class GardenAudioGraph {
|
|||
|
||||
if (this.masterGain && context.state !== 'closed') {
|
||||
this.masterGain.gain.setTargetAtTime(
|
||||
this.config.graph.closeGain,
|
||||
graphTuning.closeGain,
|
||||
context.currentTime,
|
||||
this.config.graph.closeRampSeconds
|
||||
graphTuning.closeRampSeconds
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -182,7 +251,7 @@ export class GardenAudioGraph {
|
|||
|
||||
private createDelay(context: AudioContext, masterGain: GainNode): void {
|
||||
const delayInput = context.createGain();
|
||||
const delayNode = context.createDelay(this.config.graph.delayMaxSeconds);
|
||||
const delayNode = context.createDelay(graphTuning.delayMaxSeconds);
|
||||
const delayFeedback = context.createGain();
|
||||
const delayOutput = context.createGain();
|
||||
const feedbackHighPass = context.createBiquadFilter();
|
||||
|
|
@ -193,11 +262,11 @@ export class GardenAudioGraph {
|
|||
delayFeedback.gain.value = this.config.delay.feedback;
|
||||
delayOutput.gain.value = this.config.delay.wetGain;
|
||||
feedbackHighPass.type = 'highpass';
|
||||
feedbackHighPass.frequency.value = this.config.delay.feedbackHighPassHz;
|
||||
feedbackHighPass.frequency.value = delayFilterTuning.feedbackHighPassHz;
|
||||
feedbackLowPass.type = 'lowpass';
|
||||
feedbackLowPass.frequency.value = this.config.delay.feedbackLowPassHz;
|
||||
feedbackLowPass.frequency.value = delayFilterTuning.feedbackLowPassHz;
|
||||
returnLowPass.type = 'lowpass';
|
||||
returnLowPass.frequency.value = this.config.delay.returnLowPassHz;
|
||||
returnLowPass.frequency.value = delayFilterTuning.returnLowPassHz;
|
||||
|
||||
delayInput.connect(delayNode);
|
||||
delayNode.connect(feedbackHighPass);
|
||||
|
|
@ -216,7 +285,7 @@ export class GardenAudioGraph {
|
|||
|
||||
private createBuses(context: AudioContext, masterGain: GainNode): void {
|
||||
const eventBus = context.createGain();
|
||||
eventBus.gain.value = this.config.graph.eventBusGain;
|
||||
eventBus.gain.value = graphTuning.eventBusGain;
|
||||
eventBus.connect(masterGain);
|
||||
this.eventBus = eventBus;
|
||||
this.pianoBuses.clear();
|
||||
|
|
@ -249,12 +318,11 @@ export class GardenAudioGraph {
|
|||
|
||||
private createNoiseBuffer(context: AudioContext): AudioBuffer {
|
||||
const buffer = context.createBuffer(
|
||||
appPositiveInteger(this.config.graph.noiseBufferChannels),
|
||||
appPositiveInteger(graphTuning.noiseBufferChannels),
|
||||
Math.max(
|
||||
1,
|
||||
Math.floor(
|
||||
context.sampleRate *
|
||||
Math.max(0.001, this.config.graph.noiseBufferDurationSeconds)
|
||||
context.sampleRate * Math.max(0.001, graphTuning.noiseBufferDurationSeconds)
|
||||
)
|
||||
),
|
||||
context.sampleRate
|
||||
|
|
@ -263,8 +331,8 @@ export class GardenAudioGraph {
|
|||
|
||||
for (let index = 0; index < data.length; index++) {
|
||||
data[index] =
|
||||
this.config.graph.noiseMin +
|
||||
Math.random() * (this.config.graph.noiseMax - this.config.graph.noiseMin);
|
||||
graphTuning.noiseMin +
|
||||
Math.random() * (graphTuning.noiseMax - graphTuning.noiseMin);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
|
|
@ -280,6 +348,13 @@ export class GardenAudioGraph {
|
|||
this.delayNode = null;
|
||||
this.delayFeedback = null;
|
||||
this.delayOutput = null;
|
||||
this.mediaStreamDestination = null;
|
||||
if (this.mediaStreamElement) {
|
||||
this.mediaStreamElement.pause();
|
||||
this.mediaStreamElement.srcObject = null;
|
||||
this.mediaStreamElement.remove();
|
||||
this.mediaStreamElement = null;
|
||||
}
|
||||
this.pianoBuses.clear();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue