Spaces:
Runtime error
Runtime error
| import { PassThrough, Transform } from 'node:stream' | |
| import config from '../config.js' | |
| import { debugLog, clamp16Bit, isEmpty } from './utils.js' | |
| import soundcloud from './sources/soundcloud.js' | |
| import voiceUtils from './voice/utils.js' | |
| import constants from '../constants.js' | |
| import prism from 'prism-media' | |
| class ChannelProcessor { | |
| constructor(data, type) { | |
| this.type = type | |
| switch (type) { | |
| case constants.filtering.types.equalizer: { | |
| this.history = new Array(constants.filtering.equalizerBands * 6).fill(0) | |
| this.bandMultipliers = data | |
| this.current = 0 | |
| this.minus1 = 2 | |
| this.minus2 = 1 | |
| break | |
| } | |
| case constants.filtering.types.tremolo: { | |
| this.frequency = data.frequency | |
| this.depth = data.depth | |
| this.phase = 0 | |
| this.offset = 1 - this.depth / 2 | |
| break | |
| } | |
| case constants.filtering.types.rotationHz: { | |
| this.phase = 0 | |
| this.rotationStep = (constants.circunferece.diameter * data.rotationHz) / constants.opus.samplingRate | |
| break | |
| } | |
| } | |
| } | |
| processEqualizer(band) { | |
| let processedBand = band * 0.25 | |
| for (let bandIndex = 0; bandIndex < constants.filtering.equalizerBands; bandIndex++) { | |
| const coefficient = constants.sampleRate.coefficients[bandIndex] | |
| const x = bandIndex * 6 | |
| const y = x + 3 | |
| const bandResult = coefficient.alpha * (band - this.history[x + this.minus2]) + coefficient.gamma * this.history[y + this.minus1] - coefficient.beta * this.history[y + this.minus2] | |
| this.history[x + this.current] = band | |
| this.history[y + this.current] = bandResult | |
| processedBand += bandResult * this.bandMultipliers[bandIndex] | |
| } | |
| return processedBand * 4 | |
| } | |
| getTremoloMultiplier() { | |
| let env = this.frequency * this.phase / constants.opus.samplingRate | |
| env = Math.sin(2 * Math.PI * ((env + 0.25) % 1.0)) | |
| this.phase++ | |
| return env * (1 - Math.abs(this.offset)) + this.offset | |
| } | |
| processRotationHz(leftSample, rightSample) { | |
| const panning = Math.sin(this.phase) | |
| const leftMultiplier = panning <= 0 ? 1 : 1 - panning | |
| const rightMultiplier = panning >= 0 ? 1 : 1 + panning | |
| this.phase += this.rotationStep | |
| if (this.phase > constants.circunferece.diameter) | |
| this.phase -= constants.circunferece.diameter | |
| return { | |
| left: leftSample * leftMultiplier, | |
| right: rightSample * rightMultiplier | |
| } | |
| } | |
| process(samples) { | |
| let bytes = constants.pcm.bytes | |
| if ([ constants.filtering.types.rotationHz, constants.filtering.types.tremolo ].includes(this.type)) bytes *= 2 | |
| for (let i = 0; i < samples.length - constants.pcm.bytes; i += bytes) { | |
| const sample = samples.readInt16LE(i) | |
| let result = null | |
| switch (this.type) { | |
| case constants.filtering.types.equalizer: { | |
| result = this.processEqualizer(sample) | |
| if (++this.current === 3) this.current = 0 | |
| if (++this.minus1 === 3) this.minus1 = 0 | |
| if (++this.minus2 === 3) this.minus2 = 0 | |
| samples.writeInt16LE(clamp16Bit(result), i) | |
| break | |
| } | |
| case constants.filtering.types.tremolo: { | |
| const multiplier = this.getTremoloMultiplier() | |
| const rightSample = samples.readInt16LE(i + 2) | |
| samples.writeInt16LE(clamp16Bit(sample * multiplier), i) | |
| samples.writeInt16LE(clamp16Bit(rightSample * multiplier), i + 2) | |
| break | |
| } | |
| case constants.filtering.types.rotationHz: { | |
| const { left, right } = this.processRotationHz(sample, samples.readInt16LE(i + 2)) | |
| samples.writeInt16LE(clamp16Bit(left), i) | |
| samples.writeInt16LE(clamp16Bit(right), i + 2) | |
| break | |
| } | |
| } | |
| } | |
| return samples | |
| } | |
| } | |
| class Filtering extends Transform { | |
| constructor(data, type) { | |
| super() | |
| this.type = type | |
| this.channel = new ChannelProcessor(data, type) | |
| } | |
| process(input) { | |
| this.channel.process(input) | |
| } | |
| _transform(data, _encoding, callback) { | |
| this.process(data) | |
| return callback(null, data) | |
| } | |
| } | |
| class Filters { | |
| constructor() { | |
| this.command = [] | |
| this.equalizer = Array(constants.filtering.equalizerBands).fill(0).map((_, i) => ({ band: i, gain: 0 })) | |
| this.result = {} | |
| } | |
| configure(filters, decodedTrack) { | |
| const result = {} | |
| if (filters.equalizer && Array.isArray(filters.equalizer) && filters.equalizer.length && config.filters.list.equalizer) { | |
| filters.equalizer.forEach((equalizedBand) => { | |
| const band = this.equalizer.find((i) => i.band === equalizedBand.band) | |
| if (band) band.gain = Math.min(Math.max(equalizedBand.gain, -0.25), 1.0) | |
| }) | |
| result.equalizer = this.equalizer | |
| } | |
| if (!isEmpty(filters.karaoke) && config.filters.list.karaoke) { | |
| result.karaoke = { | |
| level: Math.min(Math.max(filters.karaoke.level, 0.0), 1.0), | |
| monoLevel: Math.min(Math.max(filters.karaoke.monoLevel, 0.0), 1.0), | |
| filterBand: filters.karaoke.filterBand, | |
| filterWidth: filters.karaoke.filterWidth | |
| } | |
| this.command.push(`stereotools=mlev=${result.karaoke.monoLevel}:mwid=${result.karaoke.filterWidth}:k=${result.karaoke.level}:kc=${result.karaoke.filterBand}`) | |
| } | |
| if (!isEmpty(filters.timescale) && config.filters.list.timescale) { | |
| result.timescale = { | |
| speed: Math.max(filters.timescale.speed, 0.0), | |
| pitch: Math.max(filters.timescale.pitch, 0.0), | |
| rate: Math.max(filters.timescale.rate, 0.0) | |
| } | |
| const finalspeed = result.timescale.speed + (1.0 - result.timescale.pitch) | |
| const ratedif = 1.0 - result.timescale.rate | |
| this.command.push(`asetrate=${constants.opus.samplingRate}*${result.timescale.pitch + ratedif},atempo=${finalspeed},aresample=${constants.opus.samplingRate}`) | |
| } | |
| if (!isEmpty(filters.tremolo) && config.filters.list.tremolo) { | |
| result.tremolo = { | |
| frequency: Math.min(Math.max(filters.tremolo.frequency, 0.0), 14.0), | |
| depth: Math.min(Math.max(filters.tremolo.depth, 0.0), 1.0) | |
| } | |
| } | |
| if (!isEmpty(filters.vibrato) && config.filters.list.vibrato) { | |
| result.vibrato = { | |
| frequency: Math.min(Math.max(filters.vibrato.frequency, 0.0), 14.0), | |
| depth: Math.min(Math.max(filters.vibrato.depth, 0.0), 1.0) | |
| } | |
| this.command.push(`vibrato=f=${result.vibrato.frequency}:d=${result.vibrato.depth}`) | |
| } | |
| if (!isEmpty(filters.rotation?.rotationHz) && config.filters.list.rotation) { | |
| result.rotation = { | |
| rotationHz: filters.rotation.rotationHz | |
| } | |
| } | |
| if (!isEmpty(filters.distortion) && config.filters.list.distortion) { | |
| result.distortion = { | |
| sinOffset: filters.distortion.sinOffset, | |
| sinScale: filters.distortion.sinScale, | |
| cosOffset: filters.distortion.cosOffset, | |
| cosScale: filters.distortion.cosScale, | |
| tanOffset: filters.distortion.tanOffset, | |
| tanScale: filters.distortion.tanScale, | |
| offset: filters.distortion.offset, | |
| scale: filters.distortion.scale | |
| } | |
| this.command.push(`afftfilt=real='hypot(re,im)*sin(0.1*${filters.distortion.sinOffset}*PI*t)*${filters.distortion.sinScale}+hypot(re,im)*cos(0.1*${filters.distortion.cosOffset}*PI*t)*${filters.distortion.cosScale}+hypot(re,im)*tan(0.1*${filters.distortion.tanOffset}*PI*t)*${filters.distortion.tanScale}+${filters.distortion.offset}':imag='hypot(re,im)*sin(0.1*${filters.distortion.sinOffset}*PI*t)*${filters.distortion.sinScale}+hypot(re,im)*cos(0.1*${filters.distortion.cosOffset}*PI*t)*${filters.distortion.cosScale}+hypot(re,im)*tan(0.1*${filters.distortion.tanOffset}*PI*t)*${filters.distortion.tanScale}+${filters.distortion.offset}':win_size=512:overlap=0.75:scale=${filters.distortion.scale}`) | |
| } | |
| if (filters.channelMix && filters.channelMix.leftToLeft !== undefined && filters.channelMix.leftToRight !== undefined && filters.channelMix.rightToLeft !== undefined && filters.channelMix.rightToRight !== undefined && config.filters.list.channelMix) { | |
| result.channelMix = { | |
| leftToLeft: Math.min(Math.max(filters.channelMix.leftToLeft, 0.0), 1.0), | |
| leftToRight: Math.min(Math.max(filters.channelMix.leftToRight, 0.0), 1.0), | |
| rightToLeft: Math.min(Math.max(filters.channelMix.rightToLeft, 0.0), 1.0), | |
| rightToRight: Math.min(Math.max(filters.channelMix.rightToRight, 0.0), 1.0) | |
| } | |
| this.command.push(`pan=stereo|c0<c0*${result.channelMix.leftToLeft}+c1*${result.channelMix.rightToLeft}|c1<c0*${result.channelMix.leftToRight}+c1*${result.channelMix.rightToRight}`) | |
| } | |
| if (filters.lowPass?.smoothing !== undefined && config.filters.list.lowPass) { | |
| result.lowPass = { | |
| smoothing: Math.max(filters.lowPass.smoothing, 1.0) | |
| } | |
| this.command.push(`lowpass=f=${filters.lowPass.smoothing / 500}`) | |
| } | |
| if (filters.seek !== undefined) { | |
| result.startTime = Math.min(filters.seek, decodedTrack.length) | |
| } | |
| this.result = result | |
| return result | |
| } | |
| getResource(decodedTrack, protocol, url, startTime, endTime, oldFFmpeg, additionalData) { | |
| return new Promise(async (resolve) => { | |
| if (decodedTrack.sourceName === 'deezer') { | |
| debugLog('retrieveStream', 4, { type: 2, sourceName: decodedTrack.sourceName, query: decodedTrack.title, message: 'Filtering does not support Deezer platform.' }) | |
| return resolve({ status: 1, exception: { message: 'Filtering does not support Deezer platform', severity: 'fault', cause: 'Unimplemented feature.' } }) | |
| } | |
| if (decodedTrack.sourceName === 'soundcloud') | |
| url = await soundcloud.loadFilters(url, protocol) | |
| const ffmpeg = new prism.FFmpeg({ | |
| args: [ | |
| '-loglevel', '0', | |
| '-analyzeduration', '0', | |
| '-hwaccel', 'auto', | |
| '-threads', config.filters.threads, | |
| '-filter_threads', config.filters.threads, | |
| '-filter_complex_threads', config.filters.threads, | |
| ...(this.result.startTime !== undefined || startTime ? ['-ss', `${this.result.startTime !== undefined ? this.result.startTime : startTime}ms`] : []), | |
| '-i', url, | |
| ...(this.command.length !== 0 ? [ '-af', this.command.join(',') ] : [] ), | |
| ...(endTime ? ['-t', `${endTime}ms`] : []), | |
| '-f', 's16le', | |
| '-ar', constants.opus.samplingRate, | |
| '-ac', '2', | |
| '-crf', '0' | |
| ] | |
| }) | |
| const stream = PassThrough() | |
| ffmpeg.process.stdout.on('data', (data) => stream.write(data)) | |
| ffmpeg.process.stdout.on('end', () => stream.end()) | |
| ffmpeg.on('error', (err) => { | |
| debugLog('retrieveStream', 4, { type: 2, sourceName: decodedTrack.sourceName, query: decodedTrack.title, message: err.message }) | |
| resolve({ status: 1, exception: { message: err.message, severity: 'fault', cause: 'Unknown' } }) | |
| }) | |
| ffmpeg.process.stdout.once('readable', () => { | |
| const pipelines = [ | |
| new prism.VolumeTransformer({ type: 's16le' }) | |
| ] | |
| if (this.equalizer.some((band) => band.gain !== 0)) { | |
| pipelines.push( | |
| new Filtering( | |
| this.equalizer.map((band) => band.gain), | |
| constants.filtering.types.equalizer | |
| ) | |
| ) | |
| } | |
| if (this.result.tremolo) { | |
| pipelines.push( | |
| new Filtering({ | |
| frequency: this.result.tremolo.frequency, | |
| depth: this.result.tremolo.depth | |
| }, | |
| constants.filtering.types.tremolo) | |
| ) | |
| } | |
| if (this.result.rotation) { | |
| pipelines.push( | |
| new Filtering({ | |
| rotationHz: this.result.rotation.rotationHz / 2 | |
| }, constants.filtering.types.rotationHz) | |
| ) | |
| } | |
| pipelines.push( | |
| new prism.opus.Encoder({ | |
| rate: constants.opus.samplingRate, | |
| channels: constants.opus.channels, | |
| frameSize: constants.opus.frameSize | |
| }) | |
| ) | |
| resolve({ stream: new voiceUtils.NodeLinkStream(stream, pipelines) }) | |
| }) | |
| }) | |
| } | |
| } | |
| export default Filters |