UNPKG

@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
"use strict"; 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); }