199 lines
6.4 KiB
TypeScript
199 lines
6.4 KiB
TypeScript
// AudioEngine classes
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
|
|
import { AudioEngine, PlayingNode } from "./AudioEngine";
|
|
import { Notification } from "../Interface/Notification"
|
|
import { SynthVoice } from "../Synth/SynthVoice";
|
|
import { Synth } from "../Synth/Synth";
|
|
import { MultiplayerPianoClient } from "../MultiplayerPianoClient";
|
|
|
|
export class AudioEngineWeb extends AudioEngine {
|
|
threshold: number;
|
|
worker: Worker;
|
|
context: AudioContext;
|
|
masterGain: GainNode;
|
|
limiterNode: DynamicsCompressorNode;
|
|
pianoGain: GainNode;
|
|
synthGain: GainNode;
|
|
playings: Record<string, PlayingNode>;
|
|
synth: Synth;
|
|
gInterface: MultiplayerPianoClient;
|
|
|
|
constructor(gInterface: MultiplayerPianoClient) {
|
|
super();
|
|
this.synth = gInterface.synth;
|
|
this.gInterface = gInterface;
|
|
this.threshold = 1000;
|
|
//worker is unneeded
|
|
/*this.worker = new Worker("/js/workerTimer.js");
|
|
let self = this;
|
|
this.worker.onmessage = event => {
|
|
if (event.data.args)
|
|
if (event.data.args.action === 0) {
|
|
self.actualPlay(event.data.args.id, event.data.args.vol, event.data.args.time, event.data.args.part_id);
|
|
}
|
|
else {
|
|
self.actualStop(event.data.args.id, event.data.args.time, event.data.args.part_id);
|
|
}
|
|
}*/
|
|
}
|
|
audioEngineOnMessage(event: any) {
|
|
let self = this;
|
|
if (event.data.args)
|
|
if (event.data.args.action === 0) {
|
|
self.actualPlay(event.data.args.id, event.data.args.vol, event.data.args.time, event.data.args.part_id);
|
|
}
|
|
else {
|
|
self.actualStop(event.data.args.id, event.data.args.time, event.data.args.part_id);
|
|
}
|
|
}
|
|
init(cb?: Function): this {
|
|
super.init();
|
|
this.context = new AudioContext({
|
|
latencyHint: "interactive"
|
|
});
|
|
|
|
this.masterGain = this.context.createGain();
|
|
this.masterGain.connect(this.context.destination);
|
|
this.masterGain.gain.value = this.volume;
|
|
|
|
this.limiterNode = this.context.createDynamicsCompressor();
|
|
this.limiterNode.threshold.value = -10;
|
|
this.limiterNode.knee.value = 0;
|
|
this.limiterNode.ratio.value = 20;
|
|
this.limiterNode.attack.value = 0;
|
|
this.limiterNode.release.value = 0.1;
|
|
this.limiterNode.connect(this.masterGain);
|
|
|
|
// for synth mix
|
|
this.pianoGain = this.context.createGain();
|
|
this.pianoGain.gain.value = 0.5;
|
|
this.pianoGain.connect(this.limiterNode);
|
|
this.synthGain = this.context.createGain();
|
|
this.synthGain.gain.value = 0.5;
|
|
this.synthGain.connect(this.limiterNode);
|
|
|
|
this.playings = {};
|
|
|
|
if (cb) setTimeout(cb, 0);
|
|
return this;
|
|
}
|
|
load(id: string, url: string, cb?: Function) {
|
|
let audio = this;
|
|
let req = new XMLHttpRequest();
|
|
req.open("GET", url);
|
|
req.responseType = "arraybuffer";
|
|
req.addEventListener("readystatechange", function(evt: Event) {
|
|
if (req.readyState !== 4) return;
|
|
try {
|
|
audio.context.decodeAudioData(req.response, function(buffer) {
|
|
audio.sounds[id] = buffer;
|
|
if (cb) cb();
|
|
});
|
|
} catch (e) {
|
|
/*throw new Error(e.message
|
|
+ " / id: " + id
|
|
+ " / url: " + url
|
|
+ " / status: " + req.status
|
|
+ " / ArrayBuffer: " + (req.response instanceof ArrayBuffer)
|
|
+ " / byteLength: " + (req.response && req.response.byteLength ? req.response.byteLength : "undefined"));*/
|
|
new Notification({
|
|
id: "audio-download-error",
|
|
title: "Problem",
|
|
text: "For some reason, an audio download failed with a status of " + req.status + ". ",
|
|
target: "#piano",
|
|
duration: 10000
|
|
});
|
|
}
|
|
});
|
|
req.send();
|
|
}
|
|
actualPlay(id: string, vol: number, time: number, part_id: string) { //the old play(), but with time insted of delay_ms.
|
|
if (this.paused) return;
|
|
if (!this.sounds.hasOwnProperty(id)) return;
|
|
let source = this.context.createBufferSource();
|
|
source.buffer = this.sounds[id];
|
|
let gain = this.context.createGain();
|
|
gain.gain.value = vol;
|
|
source.connect(gain);
|
|
gain.connect(this.pianoGain);
|
|
source.start(time);
|
|
// Patch from ste-art remedies stuttering under heavy load
|
|
if (this.playings[id]) {
|
|
let playing = this.playings[id];
|
|
playing.gain.gain.setValueAtTime(playing.gain.gain.value, time);
|
|
playing.gain.gain.linearRampToValueAtTime(0.0, time + 0.2);
|
|
playing.source.stop(time + 0.21);
|
|
if (this.synth?.enableSynth && playing.voice) {
|
|
playing.voice.stop(time);
|
|
}
|
|
}
|
|
this.playings[id] = {
|
|
"source": source,
|
|
"gain": gain,
|
|
"part_id": part_id
|
|
};
|
|
if (this.synth?.enableSynth) {
|
|
this.playings[id].voice = new SynthVoice(this.gInterface, id, time);
|
|
}
|
|
}
|
|
play(id: string, vol: number, delay_ms: number, part_id: string) {
|
|
if (!this.sounds.hasOwnProperty(id)) return;
|
|
let time = this.context.currentTime + (delay_ms / 1000); //calculate time on note receive.
|
|
let delay = delay_ms - this.threshold;
|
|
if (delay <= 0) this.actualPlay(id, vol, time, part_id);
|
|
else {
|
|
this.audioEngineOnMessage({
|
|
delay: delay,
|
|
args: {
|
|
action: 0 /*play*/,
|
|
id: id,
|
|
vol: vol,
|
|
time: time,
|
|
part_id: part_id
|
|
}
|
|
}) // but start scheduling right before play.
|
|
}
|
|
}
|
|
actualStop(id: string, time: number, part_id: string) {
|
|
if (this.playings.hasOwnProperty(id) && this.playings[id] && this.playings[id].part_id === part_id) {
|
|
let gain = this.playings[id].gain.gain;
|
|
gain.setValueAtTime(gain.value, time);
|
|
gain.linearRampToValueAtTime(gain.value * 0.1, time + 0.16);
|
|
gain.linearRampToValueAtTime(0.0, time + 0.4);
|
|
this.playings[id].source.stop(time + 0.41);
|
|
|
|
|
|
if (this.playings[id].voice) {
|
|
this.playings[id].voice!.stop(time);
|
|
}
|
|
|
|
delete this.playings[id];
|
|
}
|
|
}
|
|
stop(id: string, delay_ms: number, part_id: string) {
|
|
let time = this.context.currentTime + (delay_ms / 1000);
|
|
let delay = delay_ms - this.threshold;
|
|
if (delay <= 0) this.actualStop(id, time, part_id);
|
|
else {
|
|
this.audioEngineOnMessage({
|
|
delay: delay,
|
|
args: {
|
|
action: 1 /*stop*/,
|
|
id: id,
|
|
time: time,
|
|
part_id: part_id
|
|
}
|
|
});
|
|
}
|
|
}
|
|
setVolume(vol: number) {
|
|
super.setVolume(vol);
|
|
this.masterGain.gain.value = this.volume;
|
|
}
|
|
resume() {
|
|
this.paused = false;
|
|
this.context.resume();
|
|
}
|
|
} |