Skip to content

Commit

Permalink
add: more native filters; fix: rotation filter implementation
Browse files Browse the repository at this point in the history
This commit adds more native filters, built-in to NodeLink without using FFmpeg. Also fixes the implementation of "rotation" filter.
  • Loading branch information
ThePedroo committed Apr 26, 2024
1 parent 46a4001 commit e5ef1f9
Show file tree
Hide file tree
Showing 5 changed files with 326 additions and 26 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
*

!.github
!.github/**
!docs
!docs/**
!src
!src/**
!.dockerignore
!.gitignore
!config.js
Expand Down
5 changes: 5 additions & 0 deletions constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ export default {
equalizer: 1,
tremolo: 2,
rotationHz: 3,
karaoke: 4,
lowPass: 5,
distortion: 6,
channelMix: 7,
vibrato: 8
}
},
circunferece: {
Expand Down
262 changes: 236 additions & 26 deletions src/filters.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,38 @@
/*
* Copyright 2018 natanbc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
Filters of this file are based on the Java implementation of LavaDSP by natanbc,
translated to JavaScript by ThePedroo.
*/

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

import config from '../config.js'
import { debugLog, clamp16Bit, isEmpty } from './utils.js'
import voiceUtils from './voice/utils.js'
import constants from '../constants.js'
import RingBuffer from './ringbuffer.js'
import lfo from './lfo.js'

import prism from 'prism-media'

const ADDITIONAL_DELAY = 3
const BASE_DELAY_SEC = 0.002

class ChannelProcessor {
constructor(data, type) {
this.type = type
Expand All @@ -32,6 +58,58 @@ class ChannelProcessor {
case constants.filtering.types.rotationHz: {
this.phase = 0
this.rotationStep = (constants.circunferece.diameter * data.rotationHz) / constants.opus.samplingRate
this.samplesPerCycle = constants.opus.samplingRate / (data.rotationHz * constants.circunferece.diameter)
this.dI = data.rotationHz == 0 ? 0 : 1 / this.samplesPerCycle
this.x = 0

break
}
case constants.filtering.types.karaoke: {
this.level = data.level
this.monoLevel = data.monoLevel
this.filterBand = data.filterBand
this.filterWidth = data.filterWidth

this.C = Math.exp(-2 * Math.PI * this.filterWidth / constants.opus.samplingRate)
this.B = (-4 * this.C / (1 + this.C)) * Math.cos(2 * Math.PI * this.filterBand / constants.opus.samplingRate)
this.A = Math.sqrt(1 - this.B * this.B / (4 * this.C)) * (1 - this.C)

this.y1 = 0
this.y2 = 0

break
}
case constants.filtering.types.lowPass: {
this.smoothing = data.smoothing
this.value = 0
this.initialized = false

break
}
case constants.filtering.types.distortion: {
this.sinOffset = data.sinOffset
this.sinScale = data.sinScale
this.cosOffset = data.cosOffset
this.cosScale = data.cosScale
this.tanOffset = data.tanOffset
this.tanScale = data.tanScale
this.offset = data.offset
this.scale = data.scale

break
}
case constants.filtering.types.channelMix: {
this.leftToLeft = data.leftToLeft
this.leftToRight = data.leftToRight
this.rightToLeft = data.rightToLeft
this.rightToRight = data.rightToRight

break
}
case constants.filtering.types.vibrato: {
this.depth = data.depth
this.lfo = new lfo(data.frequency)
this.buffer = new RingBuffer(Math.ceil(BASE_DELAY_SEC * constants.opus.samplingRate * 2))

break
}
Expand Down Expand Up @@ -68,38 +146,84 @@ class ChannelProcessor {
}

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

const sin = Math.sin(this.x)
this.x += this.dI

return {
left: leftSample * (sin + 1) / 2,
right: rightSample * (-sin + 1) / 2
}
}

processKaraoke(leftSample, rightSample) {
const y = (this.A * ((leftSample + rightSample) / 2) - this.B * this.y1) - this.C * this.y2
this.y2 = this.y1
this.y1 = y

const output = y * this.monoLevel * this.level

return {
left: leftSample * leftMultiplier,
right: rightSample * rightMultiplier
left: leftSample - (rightSample * this.level) + output,
right: rightSample - (leftSample * this.level) + output
}
}

processLowPass(sample) {
this.value += (sample - this.value) / this.smoothing

return this.value
}

processDistortion(sample) {
const sampleSin = this.sinOffset + Math.sin(sample * this.sinScale)
const sampleCos = this.cosOffset + Math.cos(sample * this.cosScale)
const sampleTan = this.tanOffset + Math.tan(sample * this.tanScale)

return sample * (this.offset + this.scale * (this.sinScale !== 1 ? sampleSin : 1) * (this.cosScale !== 1 ? sampleCos : 1) * (this.tanScale !== 1 ? sampleTan : 1))
}

processChannelMix(leftSample, rightSample) {
return {
left: (this.leftToLeft * leftSample) + (this.rightToLeft * rightSample),
right: (this.leftToRight * leftSample) + (this.rightToRight * rightSample)
}
}

processVibrato(sample) {
const lfoValue = this.lfo.getValue()
const maxDelay = Math.ceil(BASE_DELAY_SEC * constants.opus.samplingRate)

const delay = lfoValue * this.depth * maxDelay + ADDITIONAL_DELAY

const result = this.buffer.getHermiteAt(delay)

this.buffer.writeMargined(sample)

return result
}

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) {
if (this.type === constants.filtering.types.lowPass && !this.initialized) {
this.value = samples.readInt16LE(0)
this.initialized = true
}

for (let i = 0; i < samples.length - constants.pcm.bytes; i += bytes * 2) {
const sample = samples.readInt16LE(i)
let result = null

switch (this.type) {
case constants.filtering.types.equalizer: {
result = this.processEqualizer(sample)
const result = this.processEqualizer(sample)
const rightResult = this.processEqualizer(samples.readInt16LE(i + 2))

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)
samples.writeInt16LE(clamp16Bit(rightResult), i + 2)

break
}
Expand All @@ -119,6 +243,48 @@ class ChannelProcessor {
samples.writeInt16LE(clamp16Bit(left), i)
samples.writeInt16LE(clamp16Bit(right), i + 2)

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

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

break
}
case constants.filtering.types.lowPass: {
const leftSample = this.processLowPass(sample)
const rightSample = this.processLowPass(samples.readInt16LE(i + 2))

samples.writeInt16LE(clamp16Bit(leftSample), i)
samples.writeInt16LE(clamp16Bit(rightSample), i + 2)

break
}
case constants.filtering.types.distortion: {
const leftSample = this.processDistortion(sample)
const rightSample = this.processDistortion(samples.readInt16LE(i + 2))

samples.writeInt16LE(clamp16Bit(leftSample), i)
samples.writeInt16LE(clamp16Bit(rightSample), i + 2)

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

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

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

samples.writeInt16LE(clamp16Bit(leftSample), i)
samples.writeInt16LE(clamp16Bit(leftSample), i + 2)

break
}
}
Expand Down Expand Up @@ -173,8 +339,6 @@ class Filters {
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) {
Expand Down Expand Up @@ -202,8 +366,6 @@ class Filters {
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) {
Expand All @@ -223,8 +385,6 @@ class Filters {
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) {
Expand All @@ -234,16 +394,12 @@ class Filters {
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)
Expand Down Expand Up @@ -326,11 +482,65 @@ class Filters {
if (this.result.rotation) {
pipelines.push(
new Filtering({
rotationHz: this.result.rotation.rotationHz / 2
rotationHz: this.result.rotation.rotationHz
}, constants.filtering.types.rotationHz)
)
}

if (this.result.karaoke) {
pipelines.push(
new Filtering({
level: this.result.karaoke.level,
monoLevel: this.result.karaoke.monoLevel,
filterBand: this.result.karaoke.filterBand,
filterWidth: this.result.karaoke.filterWidth
}, constants.filtering.types.karaoke)
)
}

if (this.result.lowPass) {
pipelines.push(
new Filtering({
smoothing: this.result.lowPass.smoothing
}, constants.filtering.types.lowPass)
)
}

if (this.result.distortion) {
pipelines.push(
new Filtering({
sinOffset: this.result.distortion.sinOffset,
sinScale: this.result.distortion.sinScale,
cosOffset: this.result.distortion.cosOffset,
cosScale: this.result.distortion.cosScale,
tanOffset: this.result.distortion.tanOffset,
tanScale: this.result.distortion.tanScale,
offset: this.result.distortion.offset,
scale: this.result.distortion.scale
}, constants.filtering.types.distortion)
)
}

if (this.result.channelMix) {
pipelines.push(
new Filtering({
leftToLeft: this.result.channelMix.leftToLeft,
leftToRight: this.result.channelMix.leftToRight,
rightToLeft: this.result.channelMix.rightToLeft,
rightToRight: this.result.channelMix.rightToRight
}, constants.filtering.types.channelMix)
)
}

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

pipelines.push(
new prism.opus.Encoder({
rate: constants.opus.samplingRate,
Expand Down
Loading

0 comments on commit e5ef1f9

Please sign in to comment.