UNPKG

aqualink

Version:

An Lavalink/Nodelink client, focused in pure performance and features

435 lines (385 loc) 12 kB
const FILTER_DEFAULTS = Object.freeze({ karaoke: Object.freeze({ level: 1, monoLevel: 1, filterBand: 220, filterWidth: 100 }), timescale: Object.freeze({ speed: 1, pitch: 1, rate: 1 }), tremolo: Object.freeze({ frequency: 2, depth: 0.5 }), vibrato: Object.freeze({ frequency: 2, depth: 0.5 }), rotation: Object.freeze({ rotationHz: 0 }), distortion: Object.freeze({ sinOffset: 0, sinScale: 1, cosOffset: 0, cosScale: 1, tanOffset: 0, tanScale: 1, offset: 0, scale: 1 }), channelMix: Object.freeze({ leftToLeft: 1, leftToRight: 0, rightToLeft: 0, rightToRight: 1 }), lowPass: Object.freeze({ smoothing: 20 }) }) const { reportSuppressedError } = require('./Reporting') const FILTER_KEYS = Object.freeze( Object.fromEntries( Object.entries(FILTER_DEFAULTS).map(([k, v]) => [ k, Object.freeze(Object.keys(v)) ]) ) ) const EMPTY_ARRAY = Object.freeze([]) const EMPTY_OBJECT = Object.freeze({}) const FILTER_POOL_SIZE = 16 const filterPool = { pools: Object.fromEntries(Object.keys(FILTER_DEFAULTS).map((k) => [k, []])), acquire(type) { const pool = this.pools[type] if (pool && pool.length > 0) { return pool.pop() } return { ...FILTER_DEFAULTS[type] } }, release(type, obj) { if (!obj || !this.pools[type]) return const pool = this.pools[type] if (pool.length < FILTER_POOL_SIZE) { pool.push(obj) } } } const _utils = Object.freeze({ shallowEqual(current, defaults, override, keys) { if (!current) return false for (let i = 0; i < keys.length; i++) { const k = keys[i] const expected = k in override ? override[k] : defaults[k] if (current[k] !== expected) return false } return true }, equalizerEqual(a, b) { if (a === b) return true const lenA = a?.length || 0 const lenB = b?.length || 0 if (lenA !== lenB) return false for (let i = 0; i < lenA; i++) { const x = a[i], y = b[i] if (x.band !== y.band || x.gain !== y.gain) return false } return true }, eqIsEmpty(arr) { return !arr || arr.length === 0 }, objectEqual(a, b) { if (a === b) return true const keysA = a ? Object.keys(a) : EMPTY_ARRAY const keysB = b ? Object.keys(b) : EMPTY_ARRAY if (keysA.length !== keysB.length) return false for (let i = 0; i < keysA.length; i++) { const key = keysA[i] if (!(key in b) || a[key] !== b[key]) return false } return true }, normalizePluginFilters(filters) { if (!filters || typeof filters !== 'object') return null const entries = Object.entries(filters).filter( ([key, value]) => key && value && typeof value === 'object' ) if (!entries.length) return null return Object.fromEntries(entries) }, makeEqArray(len, gain) { const out = new Array(len) for (let i = 0; i < len; i++) out[i] = { band: i, gain } return out }, mutateFilter(target, defaults, options, keys) { let changed = false for (let i = 0; i < keys.length; i++) { const k = keys[i] const newVal = k in options ? options[k] : defaults[k] if (target[k] !== newVal) { target[k] = newVal changed = true } } return changed } }) class Filters { constructor(player, options = {}) { if (!player) throw new Error('Player instance is required') this.player = player this._pendingUpdate = false this._dirty = new Set() this.filters = { volume: options.volume ?? 1, equalizer: options.equalizer ?? EMPTY_ARRAY, karaoke: options.karaoke ?? null, timescale: options.timescale ?? null, tremolo: options.tremolo ?? null, vibrato: options.vibrato ?? null, rotation: options.rotation ?? null, distortion: options.distortion ?? null, channelMix: options.channelMix ?? null, lowPass: options.lowPass ?? null, pluginFilters: _utils.normalizePluginFilters(options.pluginFilters) } this.presets = { bassboost: options.bassboost ?? null, slowmode: options.slowmode ?? null, nightcore: options.nightcore ?? null, vaporwave: options.vaporwave ?? null, _8d: options._8d ?? null } } destroy() { for (const [key, value] of Object.entries(this.filters)) { if (value && typeof value === 'object' && key !== 'equalizer') { if (key === 'pluginFilters') continue filterPool.release(key, value) } } this._pendingUpdate = false this.player = null } _setFilter(filterName, enabled, options = {}) { const current = this.filters[filterName] if (!enabled) { if (current === null) return this filterPool.release(filterName, current) this.filters[filterName] = null this._dirty.add(filterName) return this._scheduleUpdate() } const defaults = FILTER_DEFAULTS[filterName] const keys = FILTER_KEYS[filterName] if (current) { if (_utils.shallowEqual(current, defaults, options, keys)) { return this } _utils.mutateFilter(current, defaults, options, keys) this._dirty.add(filterName) return this._scheduleUpdate() } const newFilter = filterPool.acquire(filterName) _utils.mutateFilter(newFilter, defaults, options, keys) this.filters[filterName] = newFilter this._dirty.add(filterName) return this._scheduleUpdate() } _scheduleUpdate() { if (this._pendingUpdate || !this.player || this.player.destroyed) return this this._pendingUpdate = true queueMicrotask(() => { this._pendingUpdate = false if (this.player) { this.updateFilters().catch((error) => reportSuppressedError(this.player, 'filters.update', error, { guildId: this.player.guildId }) ) } }) return this } setEqualizer(bands) { const next = bands ?? EMPTY_ARRAY if (_utils.equalizerEqual(this.filters.equalizer, next)) return this this.filters.equalizer = next this._dirty.add('equalizer') return this._scheduleUpdate() } setKaraoke(enabled, options = {}) { return this._setFilter('karaoke', enabled, options) } setTimescale(enabled, options = {}) { return this._setFilter('timescale', enabled, options) } setTremolo(enabled, options = {}) { return this._setFilter('tremolo', enabled, options) } setVibrato(enabled, options = {}) { return this._setFilter('vibrato', enabled, options) } setRotation(enabled, options = {}) { return this._setFilter('rotation', enabled, options) } setDistortion(enabled, options = {}) { return this._setFilter('distortion', enabled, options) } setChannelMix(enabled, options = {}) { return this._setFilter('channelMix', enabled, options) } setLowPass(enabled, options = {}) { return this._setFilter('lowPass', enabled, options) } setPluginFilters(filters) { const next = _utils.normalizePluginFilters(filters) if (_utils.objectEqual(this.filters.pluginFilters, next)) return this this.filters.pluginFilters = next this._dirty.add('pluginFilters') return this._scheduleUpdate() } setPluginFilter(name, config) { if (!name || typeof name !== 'string') throw new TypeError('Plugin filter name is required') if (!config || typeof config !== 'object') { if (!this.filters.pluginFilters?.[name]) return this const next = { ...this.filters.pluginFilters } delete next[name] return this.setPluginFilters(next) } const current = this.filters.pluginFilters || EMPTY_OBJECT if (current[name] === config) return this return this.setPluginFilters({ ...current, [name]: config }) } clearPluginFilters() { if (!this.filters.pluginFilters) return this this.filters.pluginFilters = null this._dirty.add('pluginFilters') return this._scheduleUpdate() } setBassboost(enabled, options = {}) { if (!enabled) { if ( this.presets.bassboost === null && _utils.eqIsEmpty(this.filters.equalizer) ) return this this.presets.bassboost = null return this.setEqualizer(EMPTY_ARRAY) } const value = options.value ?? 5 if (value < 0 || value > 5) throw new Error('Bassboost value must be between 0 and 5') if (this.presets.bassboost === value) return this this.presets.bassboost = value const gain = (value - 1) * (1.25 / 9) - 0.25 const current = Array.isArray(this.filters.equalizer) ? [...this.filters.equalizer] : [] const bands = _utils.makeEqArray(13, gain) for (const b of bands) { const idx = current.findIndex((e) => e.band === b.band) if (idx !== -1) current[idx] = b else current.push(b) } return this.setEqualizer(current) } setSlowmode(enabled, options = {}) { const rate = enabled ? (options.rate ?? 0.8) : 1 if ( this.presets.slowmode === enabled && this.filters.timescale?.rate === rate ) return this this.presets.slowmode = enabled return this.setTimescale(enabled, { rate }) } setNightcore(enabled, options = {}) { const rate = enabled ? (options.rate ?? 1.5) : 1 if ( this.presets.nightcore === enabled && this.filters.timescale?.rate === rate ) return this this.presets.nightcore = enabled return this.setTimescale(enabled, { rate }) } setVaporwave(enabled, options = {}) { const pitch = enabled ? (options.pitch ?? 0.5) : 1 if ( this.presets.vaporwave === enabled && this.filters.timescale?.pitch === pitch ) return this this.presets.vaporwave = enabled return this.setTimescale(enabled, { pitch }) } set8D(enabled, options = {}) { const rotationHz = enabled ? (options.rotationHz ?? 0.2) : 0 if ( this.presets._8d === enabled && this.filters.rotation?.rotationHz === rotationHz ) return this this.presets._8d = enabled return this.setRotation(enabled, { rotationHz }) } async clearFilters() { const f = this.filters let changed = false if (f.volume !== 1) { f.volume = 1 this._dirty.add('volume') changed = true } if (!_utils.eqIsEmpty(f.equalizer)) { f.equalizer = EMPTY_ARRAY this._dirty.add('equalizer') changed = true } const filterNames = Object.keys(FILTER_DEFAULTS) for (let i = 0; i < filterNames.length; i++) { const key = filterNames[i] if (f[key] !== null) { if (key !== 'pluginFilters') filterPool.release(key, f[key]) f[key] = null this._dirty.add(key) changed = true } } if (f.pluginFilters !== null) { f.pluginFilters = null this._dirty.add('pluginFilters') changed = true } for (const key in this.presets) { if (this.presets[key] !== null) this.presets[key] = null } return changed ? this.updateFilters() : this } async updateFilters() { this._pendingUpdate = false if (!this.player || !this._dirty.size) return this const dirtyKeys = [...this._dirty] const dirtySet = new Set(dirtyKeys) const payload = { volume: this.filters.volume, equalizer: this.filters.equalizer } const filterNames = Object.keys(FILTER_DEFAULTS) for (let i = 0; i < filterNames.length; i++) { const key = filterNames[i] if (this.filters[key] !== null) payload[key] = this.filters[key] else if (dirtySet.has(key)) payload[key] = null } payload.pluginFilters = this.filters.pluginFilters || {} for (const key of dirtyKeys) this._dirty.delete(key) try { await this.player.nodes.rest.updatePlayer({ guildId: this.player.guildId, data: { filters: payload } }) } catch (error) { for (const key of dirtyKeys) this._dirty.add(key) throw error } return this } } module.exports = Filters