Implementing custom filters to erela.js and Lavalink


If you're developing your own music bot using Lavalink, you're restricted to using only one type of filter, equalizer. However, you can use a modified version of Lavalink that utilizes lavadsp to provide more filters with Lavalink such as speed, pitch, tremolo, depth, and more. This modified version of Lavalink can be found here.

In this short guide I'll be going through how to set up this custom version of Lavalink and how to implement it with erela.js. Please note that this guide assumes you're already using erela.js in your project.

Installing Lavalink

First ensure that you have at least Java 13 then go to the releases section and download the latest Lavalink.jar file. Once you've downloaded the file, create a new folder for the Lavalink.jar on your local machine. Then create a file called application.yml which will store the credentials for your Lavalink API. In the application.yml, paste the following code:

server: # REST and WS server
  port: 2333
  address: 0.0.0.0

lavalink:
  server:
    password: "youshallnotpass"
    playerUpdateInterval: 5 # How frequently to send player updates to clients, in seconds
    koe:
      useEpoll: true
      highPacketPriority: true
      bufferDurationMs: 400
      byteBufAllocator: "default"
    sources:
      youtube: true
      bandcamp: true
      soundcloud: true
      twitch: true
      vimeo: true
      http: true
      local: false
    lavaplayer:
      nonAllocating: false # Whether to use the non-allocating frame buffer.
      frameBufferDuration: 5000 # The frame buffer duration, in milliseconds
      youtubePlaylistLoadLimit: 6 # Number of pages at 100 each
      gc-warnings: true
      youtubeSearchEnabled: true
      soundcloudSearchEnabled: true
      #ratelimit:
        #ipBlocks: ["1.0.0.0/8", "..."] # list of ip blocks
        #excludedIps: ["...", "..."] # ips which should be explicit excluded from usage by lavalink
        #strategy: "RotateOnBan" # RotateOnBan | LoadBalance | NanoSwitch | RotatingNanoSwitch
        #searchTriggersFail: true # Whether a search 429 should trigger marking the ip as failing
        #retryLimit: -1 # -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times

metrics:
  prometheus:
    enabled: false
    endpoint: /metrics

sentry:
  dsn: ""
  environment: ""
#  tags:
#    some_key: some_value
#    another_key: another_value

logging:
  file:
    path: ./logs/
  logback:
    rollingpolicy:
      max-file-size: 1GB
      max-history: 30

  level:
    root: INFO
    lavalink: INFO
            

For a more updated code snippet, navigate to: https://github.com/melike2d/lavalink/blob/dev/LavalinkServer/application.yml.example

Now we can go ahead and plugin the credentials. Change the port number to a port that is not being used and set the password to something else if you prefer more security. If you're using ip rotation to circumnavigate YouTube ip bans, make sure to uncomment the ratelimit section. You can read more about bypassing ip bans here.

When you've filled all the credentials, you can run the jar file by typing java -jar Lavalink.jar or you could use docker.

Implementing Filters

To implement filters with erela.js, we can simply extend the Player class and add a setFilter function as shown below.

Structure.extend('Player', Player => {
    class player extends Player {
        constructor(...args) {
            super(...args);
        }

        setFilter() {

        }
    }
    return player;
});
            

We want the setFilter function to take one parameter, the body. The body will contain a string of the filter we're setting.

Structure.extend('Player', Player => {
    class player extends Player {
        constructor(...args) {
            super(...args);
        }

        setFilter(body = {}) {

        }
    }
    return player;
});
            

In the setFilter function, we will use the Node class's send function in order to send our new filters to Lavalink. The Player class already has a variable for the Node class, so we can just call that. In the send function, we must include the op, which in this case would be 'filters, the guildId so that it knows which guild it should update the filters for, and finally the body so that it knows exactly which filters it should update.

Full code:

Structure.extend('Player', Player => {
    class player extends Player {
        constructor(...args) {
            super(...args);
        }

        setFilter(body = {}) {
            this.node.send({
                op: 'filters',
                guildId: this.guild.id || this.guild,
                ...body,
            });
        }
    }
    return player;
});
            

Now that we have our setFilter function, we can simply call it by doing player.setFilter({ ... }) where ... would be the object of your filter.

Here is a list of different filters you can change:

{
    "op": "filters",
    "guildId": "...",
    
    // Float value where 1.0 is 100%. Values >1.0 may cause clipping
    "volume": 1.0,
    
    // There are 15 bands (0-14) that can be changed.
    // "gain" is the multiplier for the given band. The default value is 0. Valid values range from -0.25 to 1.0,
    // where -0.25 means the given band is completely muted, and 0.25 means it is doubled. Modifying the gain could
    // also change the volume of the output.
    "equalizer": [
        {
            "band": 0,
            "gain": 0.2
        }
    ],
    
    // Uses equalization to eliminate part of a band, usually targeting vocals.
    "karaoke": {
        "level": 1.0,
        "monoLevel": 1.0,
        "filterBand": 220.0,
        "filterWidth": 100.0
    },
    
    // Changes the speed, pitch, and rate. All default to 1.
    "timescale": {
        "speed": 1.0,
        "pitch": 1.0,
        "rate": 1.0
    },
    
    // Uses amplification to create a shuddering effect, where the volume quickly oscillates.
    // Example: https://en.wikipedia.org/wiki/File:Fuse_Electronics_Tremolo_MK-III_Quick_Demo.ogv
    "tremolo": {
        "frequency": 2.0, // 0 < x
        "depth": 0.5      // 0 < x ≤ 1
    },
    
    // Similar to tremolo. While tremolo oscillates the volume, vibrato oscillates the pitch.
    "vibrato": {
        "frequency": 2.0, // 0 < x ≤ 14
        "depth": 0.5      // 0 < x ≤ 1
    },
    
    // Rotates the sound around the stereo channels/user headphones aka Audio Panning. It can produce an effect similar to: https://youtu.be/QB9EB8mTKcc (without the reverb)
    "rotation": {
        "rotationHz": 0 // The frequency of the audio rotating around the listener in Hz. 0.2 is similar to the example video above.
    },
    
    // Distortion effect. It can generate some pretty unique audio effects.
    "distortion": {
        "sinOffset": 0,
        "sinScale": 1,
        "cosOffset": 0,
        "cosScale": 1,
        "tanOffset": 0,
        "tanScale": 1,
        "offset": 0,
        "scale": 1
    } 
    
    // Mixes both channels (left and right), with a configurable factor on how much each channel affects the other.
    // With the defaults, both channels are kept independent from each other.
    // Setting all factors to 0.5 means both channels get the same audio.
    "channelMix": {
        "leftToLeft": 1.0,
        "leftToRight": 0.0,
        "rightToLeft": 0.0,
        "rightToRight": 1.0,
    }
    
    // Higher frequencies get suppressed, while lower frequencies pass through this filter, thus the name low pass.
    "lowPass": {
        "smoothing": 20.0
    }
}
            

From https://github.com/freyacodes/Lavalink/blob/dev/IMPLEMENTATION.md#using-filters