nodelink / src /filters.js
flameface's picture
Upload 25 files
b58c6cb verified
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