@4players/odin
Version:
A cross-platform SDK enabling developers to integrate real-time VoIP chat technology into their projects
203 lines (202 loc) • 6.96 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.OdinError = void 0;
exports.sleep = sleep;
exports.assert = assert;
exports.normalizeUrl = normalizeUrl;
exports.updateVoiceActivity = updateVoiceActivity;
exports.updateVolumeGate = updateVolumeGate;
exports.isAudioCapable = isAudioCapable;
exports.generateCaptureParameters = generateCaptureParameters;
exports.generateVadConfig = generateVadConfig;
exports.generateApmConfig = generateApmConfig;
exports.calcPlaybackVolume = calcPlaybackVolume;
exports.calculateBytesPerSeconds = calculateBytesPerSeconds;
const odin_common_1 = require("@4players/odin-common");
/**
* Pauses the execution of asynchronous code for a specified amount of time.
*
* @param {number} ms - The duration to sleep in milliseconds.
* @return {Promise<void>} A promise that resolves after the specified duration.
*/
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}
class OdinError extends Error {
constructor(message = '') {
super(message);
this.message = message;
this.name = 'OdinError';
const date = new Date();
const hours = date.getHours();
const minutes = date.getMinutes();
const seconds = date.getSeconds();
this.message = `${hours}:${minutes}:${seconds} -> ${message}\n`;
}
}
exports.OdinError = OdinError;
/**
* Throws an error if the provided condition is falsy.
*
* @param {unknown} condition - The condition to evaluate. If falsy, an error is thrown.
* @param {string} message - The error message to be included in the thrown error.
* @return {void}
*/
function assert(condition, message) {
if (!condition) {
throw new OdinError(message);
}
}
/**
* Normalizes the given URL by ensuring it starts with "https://".
*
* @param {string} url - The URL to normalize.
* @return {string} The normalized URL that begins with "https://".
*/
function normalizeUrl(url) {
let normalized = url;
if (url.indexOf('https://') === -1) {
normalized = `https://${normalized}`;
}
return normalized;
}
/**
* Updates the voice activity sensitivity range based on the input level.
*
* @param {number} lvl - The input level that defines the current sensitivity adjustment. It is clamped between 0 and 1 if outside of this range.
* @return {Backend.SensitivityRange} An object containing the calculated `releaseThreshold` and `attackThreshold`, both of which are adjusted based on the level.
*/
function updateVoiceActivity(lvl) {
let lvlRounded = lvl;
if (lvl < 0) {
lvlRounded = 0;
}
if (lvl > 1) {
lvlRounded = 1;
}
const i = 2;
const j = 1.5;
const k = 0.2;
const result = 0.5 * (k * (j - lvlRounded) ** i);
return {
releaseThreshold: Math.min(1, Math.max(0.05, lvlRounded - result)),
attackThreshold: Math.min(1, Math.max(0.05, lvlRounded + result)),
};
}
/**
* Updates the volume gate sensitivity range based on the provided active threshold.
*
* @param {number} goingActive - The desired threshold for activating the volume gate.
* @param {number} [offset=10] - The offset value used to calculate the release threshold. Defaults to 10.
* @return {Backend.SensitivityRange} An object representing the updated sensitivity range with `attackThreshold` and `releaseThreshold`.
*/
function updateVolumeGate(goingActive, offset = 10) {
let active = goingActive;
if (goingActive < -80) {
active = -80;
}
if (goingActive > 0) {
active = 0;
}
return {
attackThreshold: active,
releaseThreshold: active - offset,
};
}
/**
* Determines whether the current platform is capable of supporting audio functionality
* by checking the availability of `AudioContext` and `Worker`.
*
* @return {boolean} Returns true if the platform supports audio functionality; otherwise, false.
*/
function isAudioCapable() {
if (typeof AudioContext === 'undefined') {
console.warn('AudioContext is not available on this platform; disabling ODIN audio functionality');
return false;
}
else if (typeof Worker === 'undefined') {
console.warn('Worker is not available on this platform; disabling ODIN audio functionality');
return false;
}
return true;
}
function generateCaptureParameters(device, settings) {
const parameters = {
volume: typeof settings?.volume === 'undefined' ? 1 : settings.volume,
...device,
};
const vad = generateVadConfig(settings);
if (vad) {
parameters.vad = vad;
}
const apm = generateApmConfig(settings);
if (apm) {
parameters.apm = apm;
}
return parameters;
}
function generateVadConfig(settings) {
const vad = {
...odin_common_1.VAD_DEFAULTS,
voiceActivity: { ...odin_common_1.VAD_DEFAULTS.voiceActivity },
volumeGate: { ...odin_common_1.VAD_DEFAULTS.volumeGate },
};
if (!settings)
return vad;
// Voice activity
if (typeof settings.voiceActivity === 'number') {
vad.voiceActivity = updateVoiceActivity(settings.voiceActivity);
}
if (settings.voiceActivity === false) {
vad.voiceActivity = undefined;
}
if (typeof settings.voiceActivity === 'object' &&
'attackThreshold' in settings.voiceActivity) {
vad.voiceActivity = settings.voiceActivity;
}
// Volume gate
if (typeof settings.volumeGate === 'number') {
vad.volumeGate = updateVolumeGate(settings.volumeGate);
}
if (settings.volumeGate === false) {
vad.volumeGate = undefined;
}
if (typeof settings.volumeGate === 'object' &&
'attackThreshold' in settings.volumeGate) {
vad.volumeGate = settings.volumeGate;
}
return vad;
}
function generateApmConfig(settings) {
const apm = { ...odin_common_1.APM_DEFAULTS };
if (!settings)
return apm;
apm.echoCanceller = settings.echoCanceller ?? apm.echoCanceller;
apm.noiseSuppression =
settings.noiseSuppression === false ? 'None' : 'Moderate';
apm.gainController = settings.gainController ?? apm.gainController;
return apm;
}
/**
* Calculates the playback volume by combining an array of volumes.
*
* @param {PlaybackVolume[]} volumes - An array of volume values. Each value can be a number or the string 'muted'.
* A 'muted' value is treated as 0 during calculation.
* @return {number} The resulting playback volume after combining all provided volumes.
*/
function calcPlaybackVolume(volumes) {
const volume = [1, 1];
for (const v of volumes) {
volume[0] *= v[0];
volume[1] *= v[1];
}
return volume;
}
function calculateBytesPerSeconds(currentBytes, lastBytes, ms) {
const difference = currentBytes - lastBytes;
return difference / (ms / 1000);
}