Based on:

/* Verification of the filters compared to Lavaplayer:

   Equalizer: yes
   Karaoke: ? No native
   Timescale: ? No native
   Tremolo: yes
   Vibrato: ?
   Rotation: ? No native
   Distortion: ? No native
   ChannelMix: ? No native
   LowPass: ? No native
*/

import { PassThrough, Transform } from 'node:stream'

import config from '../config.js'
import { debugLog, clamp16Bit } 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.m1 = 2
        this.m2 = 1

        break
      }
      // case constants.filtering.types.vibrato:
      case constants.filtering.types.tremolo: {
        this.frequency = data.frequency
        this.depth = data.depth
        this.phase = 0

        break
      }
      case constants.filtering.types.rotationHz: {
        this.rotationHz = data.rotationHz
        this.phase = 0
        this.rotationStep = (2 * Math.PI * this.rotationHz) / constants.opus.samplingRate

        break
      }
    }
  }

  processEqualizer(int) {
    let result = int * 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 * (int - this.history[x + this.m2]) + coefficient.gamma * this.history[y + this.m1] - coefficient.beta * this.history[y + this.m2]

      this.history[x + this.current] = int
      this.history[y + this.current] = bandResult

      result += bandResult * this.bandMultipliers[bandIndex]
    }

    return result * 4
  }

  processVibrato(sample) {
    // TODO
  }

  processTremolo(sample) {
    const lfo = Math.sin(2 * Math.PI * this.frequency * this.phase / constants.opus.samplingRate)
    const newAmplitude = sample * ((1 - this.depth) + this.depth * lfo)

    if (this.phase >= constants.opus.samplingRate / this.frequency) this.phase = 0
    else this.phase += 1

    return clamp16Bit(newAmplitude)
  }

  // monoToStereo(samples) {
  //   const newBuffer = Buffer.alloc(samples.length * 2)

  //   for (let i = 0; i < samples.length; i += constants.pcm.bytes) {
  //     const sample = samples.readInt16LE(i)

  //     newBuffer.writeInt16LE(sample, i * 2)
  //     newBuffer.writeInt16LE(sample, i * 2 + 2)
  //   }
    
  //   return newBuffer
  // }

  processRotationHz(leftSample, rightSample) {
    const leftMultiplier = Math.cos(this.phase)
    const rightMultiplier = Math.sin(this.phase)

    const rotatedLeft = clamp16Bit(leftSample * leftMultiplier - rightSample * rightMultiplier)
    const rotatedRight = clamp16Bit(leftSample * rightMultiplier + rightSample * leftMultiplier)

    this.phase += this.rotationStep
    if (this.phase > 2 * Math.PI) this.phase -= 2 * Math.PI

    return {
      left: rotatedLeft,
      right: rotatedRight
    }
  }

  process(samples) {
  // console.log(samples)
    let bytes = constants.pcm.bytes / 2
    // if (this.type == constants.filtering.types.rotationHz) {
    //   samples = this.monoToStereo(samples)
    //   bytes = constants.pcm.bytes * 2
    // }

    for (let i = 0; i < samples.length - bytes; i += bytes) {
      const sample = samples.readInt16LE(i)
      let result = null
      
      switch (this.type) {
        case constants.filtering.types.equalizer: {
          result = this.processEqualizer(sample)

          this.step()

          break
        }
        // case constants.filtering.types.vibrato: {
        //   result = this.processVibrato(sample)

        //   break
        // }
        case constants.filtering.types.tremolo: {
          result = this.processTremolo(sample)

          break
        }
        case constants.filtering.types.rotationHz: {
          const rightSample = samples.readInt16LE(i + 2)
          const { left, right } = this.processRotationHz(sample, rightSample)

          // console.log(left, right)

          samples.writeInt16LE(left, i)
          samples.writeInt16LE(right, i + 2)

          break
        }
      }

      if (this.type != constants.filtering.types.rotationHz)
        samples.writeInt16LE(clamp16Bit(result), i)
    }

    return samples
  }

  step() {
    if (++this.current == 3) {
      this.current = 0
    }
    if (++this.m1 == 3) {
      this.m1 = 0
    }
    if (++this.m2 == 3) {
      this.m2 = 0
    }
  }
}

class Filtering extends Transform {
  constructor(data, type) {
    super()

    this.type = type

    if (type == constants.filtering.types.equalizer) {
      data = data.map((mul) => mul.gain)
    }

    this.channels = []

    for (let i = 0; i < constants.pcm.channels; i++) {
      this.channels.push(new ChannelProcessor(data, type))
    }
  }

  process(input) {
    return this.channels.map((c, i) => {
      const data = input[i]
      return c.process(data)
    })
  }

  _transform(data, _encoding, callback) {
    const left = data.map((_, i) => data[i * 2])
    const right = data.map((_, i) => data[i * 2 + 1])

    this.process([ left, right ])

    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) {
    const result = {}

    if (filters.volume && config.filters.list.volume) {
      result.volume = filters.volume

      this.command.push(`volume=${filters.volume}`)
    }

		if (filters.equalizer && Array.isArray(filters.equalizer) && filters.equalizer.length && config.filters.list.equalizer) {
      result.equalizer = filters.equalizer

      for (const equalizedBand of filters.equalizer) {
        const band = this.equalizer.find(i => i.band == equalizedBand.band)
        if (band) band.gain = Math.min(Math.max(equalizedBand.gain, -0.25), 1.0)
      }
		}

    if (filters.karaoke && filters.karaoke.level && filters.karaoke.monoLevel && filters.karaoke.filterBand && filters.karaoke.filterWidth && config.filters.list.karaoke) {
      result.karaoke = { level: filters.karaoke.level, monoLevel: filters.karaoke.monoLevel, filterBand: filters.karaoke.filterBand, filterWidth: filters.karaoke.filterWidth }

      this.command.push(`stereotools=mlev=${filters.karaoke.monoLevel}:mwid=${filters.karaoke.filterWidth}:k=${filters.karaoke.level}:kc=${filters.karaoke.filterBand}`)
    }
    if (filters.timescale && filters.timescale.speed && filters.timescale.pitch && filters.timescale.rate && config.filters.list.timescale) {
      result.timescale = { speed: filters.timescale.speed, pitch: filters.timescale.pitch, rate: filters.timescale.rate }

      const finalspeed = filters.timescale.speed + (1.0 - filters.timescale.pitch)
      const ratedif = 1.0 - filters.timescale.rate

      this.command.push(`asetrate=${constants.opus.samplingRate}*${filters.timescale.pitch + ratedif},atempo=${finalspeed},aresample=${constants.opus.samplingRate}`)
		}

    if (filters.tremolo && filters.tremolo.frequency && filters.tremolo.depth && config.filters.list.tremolo) {
      result.tremolo = { frequency: filters.tremolo.frequency, depth: filters.tremolo.depth }

      this.command.push(`tremolo=f=${filters.tremolo.frequency}:d=${filters.tremolo.depth}`)
    }

    if (filters.vibrato && filters.vibrato.frequency && filters.vibrato.depth && config.filters.list.vibrato) {
      result.vibrato = { frequency: filters.vibrato.frequency, depth: filters.vibrato.depth }
    }

    if (filters.rotation && filters.rotation.rotationHz && config.filters.list.rotation) {
      result.rotation = { rotationHz: filters.rotation.rotationHz }
    }

    if (filters.distortion && filters.distortion.sinOffset && filters.distortion.sinScale && filters.distortion.cosOffset && filters.distortion.cosScale && filters.distortion.tanOffset && filters.distortion.tanScale && filters.distortion.offset && filters.distortion.scale && 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 && filters.channelMix.leftToRight && filters.channelMix.rightToLeft && filters.channelMix.rightToRight && config.filters.list.channelMix) {
      result.channelMix = { leftToLeft: filters.channelMix.leftToLeft, leftToRight: filters.channelMix.leftToRight, rightToLeft: filters.channelMix.rightToLeft, rightToRight: filters.channelMix.rightToRight }

      this.command.push(`pan=stereo|c0<c0*${filters.channelMix.leftToLeft}+c1*${filters.channelMix.rightToLeft}|c1<c0*${filters.channelMix.leftToRight}+c1*${filters.channelMix.rightToRight}`)
    }

    if (filters.lowPass && filters.lowPass.smoothing && config.filters.list.lowPass) {
      result.lowPass = { smoothing: filters.lowPass.smoothing }

      this.command.push(`lowpass=f=${filters.lowPass.smoothing / 500}`)
    }

    this.result = result

    return result
  }

  getResource(guildId, decodedTrack, protocol, url, startTime, endTime, oldFFmpeg, additionalData) {
    return new Promise(async (resolve) => {
      // TODO: Deezer is not properly working, refactor this file's function.

      // const trackData = await sources.getTrackStream(decodedTrack, url, protocol, additionalData)

      // if (trackData.exception) {
      //   debugLog('retrieveStream', 4, { type: 2, sourceName: decodedTrack.sourceName, query: decodedTrack.title, message: trackData.exception.message })

      //   resolve({ status: 1, exception: { message: trackData.exception.message, severity: 'fault', cause: 'Unknown' } })
      // }

      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,
          ...(startTime ? ['-ss', `${startTime}ms`] : []),
          '-i', encodeURI(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, constants.filtering.types.equalizer))
        }

        if (this.result.vibrato) {
          // TODO
        }

        if (this.result.tremolo) {
          pipelines.push(new Filtering({ frequency: this.result.tremolo.frequency / 2, depth: this.result.tremolo.depth / 2 }, constants.filtering.types.tremolo))
        }

        if (this.result.rotation) {
          pipelines.push(new Filtering({ rotationHz: this.result.rotation.rotationHz }, 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

and: "Changes the speed, pitch, and rate. All default to 1.0."

Make a timescale filter