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 |