soiz1's picture
Upload 811 files
30c32c8 verified
raw
history blame
14.2 kB
const Cast = require("../../util/cast");
const Timer = require("./timer");
function MathOver(number, max) {
let num = number;
while (num > max) {
num -= max;
}
return num;
}
function Clamp(number, min, max) {
return Math.min(Math.max(number, min), max);
}
class AudioSource {
/**
* @param {AudioContext} audioContext
* @param {object} audioGroup
* @param {AudioBuffer} source
* @param {object} data
* @param {object} parent
*/
constructor(audioContext, audioGroup, source, data, parent, runtime) {
if (source == null) source = "";
if (data == null) data = {};
this.runtime = runtime;
this.src = source;
this.duration = source.duration;
this.originAudioName = "";
this.volume = data.volume ?? 1;
this.speed = data.speed ?? 1;
this.pitch = data.pitch ?? 0;
this.pan = data.pan ?? 0;
this.looping = data.looping ?? false;
this.startPosition = data.startPosition ?? 0;
this.endPosition = data.endPosition ?? Infinity;
this.loopStartPosition = data.loopStartPosition ?? 0;
this.loopEndPosition = data.loopEndPosition ?? Infinity;
this.resumeSpot = 0;
this.paused = false;
this.notPlaying = true;
this.parent = parent;
this._audioNode = null;
this._audioContext = audioContext;
this._audioGroup = audioGroup;
this._audioPanner = this._audioContext.createPanner();
this._audioGainNode = this._audioContext.createGain();
this._audioAnalyzerNode = this._audioContext.createAnalyser();
this._audioPanner.panningModel = 'equalpower';
this._audioGainNode.gain.value = 1;
this._audioGainNode.connect(this._audioPanner);
this._audioPanner.connect(this._audioAnalyzerNode);
this._audioAnalyzerNode.connect(parent.audioGlobalVolumeNode);
this._originalConfig = data;
this._playingSrc = null;
this._timer = new Timer(runtime, audioContext);
this._disposed = false;
}
play(atTime) {
if (!this.src) throw "Cannot play an empty audio source";
try {
if (this._audioNode) {
this._audioNode.onended = null;
this._audioNode.stop();
}
} catch {
// ... idk
} finally {
this._audioNode = null;
}
const source = this._audioContext.createBufferSource();
this._audioNode = source;
this.update();
source.buffer = this.src;
source.connect(this._audioGainNode);
this._playingSrc = source.buffer;
if (!this.paused) {
this._timer.reset();
this._timer.setTime(Clamp(atTime ?? this.startPosition, 0, this.duration) * 1000);
this._timer.start();
} else {
this.resumeSpot = this.getTimePosition();
this._timer.start();
}
// we need to know when the sound starts, so we know how long to play for
// we also need to change endTimePos if we are looping
let startTimePos = this.resumeSpot;
let endTimePos = this.endPosition;
if (this.paused) {
this.paused = false;
} else {
startTimePos = atTime ?? this.startPosition;
}
if (this.looping) {
endTimePos = this.loopEndPosition;
}
// dont play the sound if the playback duration is less than 1 sample frame, otherwise the ended event will not fire
this.notPlaying = false;
const playbackDuration = Clamp(endTimePos - startTimePos, 0, this.duration);
if (playbackDuration < 1 / this.src.sampleRate) {
this._onNodeStop(true);
} else {
source.start(0, Clamp(startTimePos, 0, this.duration), playbackDuration);
source.onended = () => {
this._onNodeStop();
}
}
}
stop() {
this.notPlaying = true;
this.paused = false;
this._timer.stop();
try {
if (this._audioNode) {
this._audioNode.stop();
}
} catch {
// ... idk
} finally {
this._audioNode = null;
}
}
pause() {
if (!this._audioNode) return;
this.paused = true;
this.notPlaying = true;
this._timer.pause();
// onended is already ignored when paused, and stopped nodes cannot restart
this._audioNode.onended = null;
this._audioNode.stop();
this._audioNode = null;
}
update() {
if (!this._audioNode) return;
const audioNode = this._audioNode;
const audioGroup = this._audioGroup;
const audioGainNode = this._audioGainNode;
const audioPanner = this._audioPanner;
// we need to manually calculate detune to prevent problems when using playbackRate for other things
audioNode.playbackRate.value = this.speed * Math.pow(2, this.pitch / 1200);
audioGainNode.gain.value = this.volume;
audioNode.playbackRate.value *= audioGroup.globalSpeed * Math.pow(2, audioGroup.globalPitch / 1200);
audioGainNode.gain.value *= audioGroup.globalVolume;
this._timer.speed = audioNode.playbackRate.value;
const pan = Clamp(this.pan + audioGroup.globalPan, -1, 1);
audioPanner.positionX.value = pan;
audioPanner.positionY.value = 0;
audioPanner.positionZ.value = 1 - Math.abs(pan);
}
dispose() {
this._disposed = true;
this._timer.dispose();
this.stop();
}
clone() {
const newSource = new AudioSource(this._audioContext, this._audioGroup, this.src, this._originalConfig, this.parent, this.runtime);
return newSource;
}
reverse() {
if (!this.src) throw "Cannot reverse an empty audio source";
const buffer = this.src;
const reversedBuffer = this._audioContext.createBuffer(
buffer.numberOfChannels,
buffer.length,
buffer.sampleRate
);
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
const sourceData = buffer.getChannelData(channel);
const destinationData = reversedBuffer.getChannelData(channel);
for (let i = 0; i < buffer.length; i++) {
destinationData[i] = sourceData[buffer.length - 1 - i];
}
}
this.src = reversedBuffer;
}
setTimePosition(newSeconds) {
if (!this._audioNode && !this.paused) return;
const src = this._getActiveSource();
newSeconds = Clamp(newSeconds, 0, src.duration);
if (this.paused) {
// only update the time
this._timer.setTime(newSeconds * 1000);
return;
}
this._timer.setTime(newSeconds * 1000);
this.play(newSeconds);
}
getVolume() {
const analyserNode = this._audioAnalyzerNode;
const bufferLength = analyserNode.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyserNode.getByteTimeDomainData(dataArray);
let sumSquares = 0.0;
for (let i = 0; i < bufferLength; i++) {
const sample = (dataArray[i] / 128.0) - 1.0;
sumSquares += sample * sample;
}
const volume = Math.sqrt(sumSquares / bufferLength);
return volume;
}
getFrequency() {
const analyserNode = this._audioAnalyzerNode;
const src = this._getActiveSource();
const bufferLength = analyserNode.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyserNode.getByteFrequencyData(dataArray);
// find the max frequency
let maxIndex = 0;
for (let i = 1; i < bufferLength; i++) {
if (dataArray[i] > dataArray[maxIndex]) {
maxIndex = i;
}
}
// return the dominant freq
const nyquist = src.sampleRate / 2;
return maxIndex * nyquist / bufferLength;
}
getTimePosition() {
const src = this._getActiveSource();
return Clamp(this._timer.getTime(true), 0, src.duration);
}
_getActiveSource() {
if (this._audioNode) return this._playingSrc;
return this.src;
}
_onNodeStop(didNotPlay) {
if (this.paused || !this._audioNode) return;
if (!didNotPlay) {
if (this.looping && !this.notPlaying) {
this.play(this.loopStartPosition || 0);
return;
}
}
this._audioNode.onended = null;
this.notPlaying = true;
this._audioNode = null;
this._timer.stop();
}
}
class AudioExtensionHelper {
constructor(runtime) {
/**
* The runtime that the helper will use for all functions.
* @type {runtime}
*/
this.runtime = runtime;
this.audioGroups = {};
this.audioContext = new AudioContext();
this.audioGlobalVolumeNode = this.audioContext.createGain();
this.audioGlobalVolumeNode.gain.value = 1;
this.audioGlobalVolumeNode.connect(this.audioContext.destination);
}
/**
* Creates a new AudioGroup.
* @type {string} AudioGroup name
* @type {object} AudioGroup settings (optional)
* @type {object[]} AudioGroup sources (optional)
*/
AddAudioGroup(name, data, sources) {
if (data == null) data = {};
this.audioGroups[name] = {
id: name,
sources: (sources == null ? {} : sources),
globalVolume: (data.globalVolume == null ? 1 : data.globalVolume),
globalSpeed: (data.globalSpeed == null ? 1 : data.globalSpeed),
globalPitch: (data.globalPitch == null ? 0 : data.globalPitch),
globalPan: (data.globalPan == null ? 0 : data.globalPan)
};
return this.audioGroups[name];
}
/**
* Deletes an AudioGroup by name.
* @type {string}
*/
DeleteAudioGroup(name) {
const audioGroup = this.audioGroups[name];
if (!audioGroup) return;
this.DisposeAudioGroupSources(audioGroup);
delete this.audioGroups[name];
}
/**
* Gets an AudioGroup by name.
* @type {string}
*/
GetAudioGroup(name) {
return this.audioGroups[name];
}
/**
* Gets all AudioGroups and returns them in an array.
*/
GetAllAudioGroups() {
return Object.values(this.audioGroups);
}
/**
* Gets all AudioSources in an AudioGroup and updates them.
* @type {AudioGroup}
*/
UpdateAudioGroupSources(audioGroup) {
const audioSources = this.GrabAllGrabAudioSources(audioGroup);
for (let i = 0; i < audioSources.length; i++) {
const source = audioSources[i];
source.update();
}
}
/**
* Gets all AudioSources in an AudioGroup and disposes them.
* @type {AudioGroup}
*/
DisposeAudioGroupSources(audioGroup) {
const audioSources = this.GrabAllGrabAudioSources(audioGroup);
for (let i = 0; i < audioSources.length; i++) {
const source = audioSources[i];
source.dispose();
}
}
/**
* Creates a new AudioSource inside of an AudioGroup.
* @type {AudioGroup} AudioSource parent
* @type {string} AudioSource name
* @type {string} AudioSource source (optional)
* @type {object} AudioSource settings (optional)
*/
AppendAudioSource(parent, name, src, settings) {
const group = typeof parent == "string" ? this.GetAudioGroup(parent) : parent;
if (!group) return;
group.sources[name] = new AudioSource(this.audioContext, group, src, settings, this, this.runtime);
return group.sources[name];
}
/**
* Deletes an AudioSource by name.
* @type {AudioGroup} AudioSource parent
* @type {string}
*/
RemoveAudioSource(parent, name) {
const group = typeof parent == "string" ? this.GetAudioGroup(parent) : parent;
if (!group) return;
const audioSource = group.sources[name];
if (!audioSource) return;
audioSource.dispose();
delete group.sources[name];
}
/**
* Gets an AudioSource by name.
* @type {AudioGroup} AudioSource parent
* @type {string}
*/
GrabAudioSource(audioGroup, name) {
const group = typeof audioGroup == "string" ? this.GetAudioGroup(audioGroup) : audioGroup;
if (!group) return;
return group.sources[name];
}
/**
* Gets all AudioSources and returns them in an array.
* @type {AudioGroup} AudioSource parent
*/
GrabAllGrabAudioSources(audioGroup) {
const group = typeof audioGroup == "string" ? this.GetAudioGroup(audioGroup) : audioGroup;
if (!group) return [];
return Object.values(group.sources);
}
/**
* Finds a sound with the specified ID in the sound list.
* @type {Array} soundList
* @type {string} Sound ID
*/
FindSoundBySoundId(soundList, id) {
for (let i = 0; i < soundList.length; i++) {
const sound = soundList[i];
if (sound.soundId == id) return sound;
}
return null;
}
/**
* Finds a sound with the specified name in the sound list.
* @type {Array} soundList
* @type {string} Sound name
*/
FindSoundByName(soundList, name) {
for (let i = 0; i < soundList.length; i++) {
const sound = soundList[i];
if (sound.name == name) return sound;
}
return null;
}
/**
* Clamps numbers to stay inbetween 2 values.
* @type {number}
*/
Clamp(number, min, max) {
return Math.min(Math.max(number, min), max);
}
}
module.exports.Helper = AudioExtensionHelper
module.exports.AudioSource = AudioSource