Guides

Filters

Apply built-in, plugin and fork filters with a single typed setter.

A filter is active while its key is present in the payload. There is no neutral "off" value: clear removes the key, isEnabled is presence, and a fresh player sends nothing at all.

Presets

The typed setters cover the built-in Lavalink filters and the two filter plugins.

filter-presets.ts
import type {  } from 'hoshimi';

declare const : ;

const  = .;

await .();
await .();
await .({ : 1, : 1, : 220, : 100 });
await .({ : 0, : 0.25 }, { : 1, : 0.15 });

// Plugin facades
await ..({ : 200, : 0.5 });   // lavalink-filter-plugin
await ..({ : 500, : 1.5 }); // lavadspx-plugin

await .(); // drops everything

set, get, clear

set is the generic entry point: it takes any filter name and the payload it expects, writes it to the right envelope and commits. get reads it back, clear removes it.

filter-crud.ts
import {  } from 'hoshimi';
import type {  } from 'hoshimi';

declare const : ;

const  = .;

await .(., { : 1.29, : 1.29, : 0.94 });

const timescale = .(.);
const timescale: TimescaleSettings | undefined
.(.); // true .(); // ["timescale"] await .(.);

Payloads are typed per filter

The payload type is resolved from the filter name, so set(FilterType.Volume, { nope: true }) does not compile. FilterType.Echo and FilterType.DSPXEcho share the echo wire key but take different payloads, and the types keep them apart.

Filters Hoshimi does not know

Any filter can be set without registering it first — a fork's own filter, a plugin you wrote, anything. The options decide where the payload lands in the filters object sent to Lavalink.

custom-filters.ts
import type {  } from 'hoshimi';

declare const : ;

const  = .;

// pluginFilters.myFilter — the extension point the Lavalink v4 spec defines
await .('myFilter', { : 2 });

// pluginFilters["my-plugin"].boost — nested, the spec's plugin shape
await .('boost', { : 2 }, { : 'my-plugin' });

// filters.forkEcho — top level, where a fork exposes its own filters
await .('forkEcho', { : 0.5 }, { : true });

// Clearing needs the same routing that was used to set it
await .('boost', { : 'my-plugin' });

Hoshimi does not check which server it is talking to, so whether a fork-specific filter is safe to send is up to you: point the player at a node that understands it.

OptionsEnvelopeNode check
(none), registered filterwhatever the registry resolvesyes
(none), unknown filterpluginFilters[name]no
plugin: truepluginFilters[name]no
plugin: 'name'pluginFilters['name'][filter]no
top: truefilters[name]no

Pass validate: false to skip the check on a registered filter, for nodes that support a filter without advertising it.

Registering a filter

Registration is optional. It buys routing by name — no options at the call site — plus validation against what the node reports in /v4/info.

register-filter.ts
import { , ,  } from 'hoshimi';
import type {  } from 'hoshimi';

declare const : ;

.({
  : 'boost',
  : .,     // Plugin -> pluginFilters · Core -> top level
  : 'my-plugin',       // omit to write it flat under pluginFilters
  : .,
});

await ..('boost', { : 2 }); // routed and validated

Typing your own filters

Declare the payload of a custom filter through module augmentation. The key is the filter name, the value is what it takes.

typed-custom-filter.ts
import type {  } from 'hoshimi';

declare const : ;

declare module 'hoshimi' {
  interface CustomizableFilters {
    : { : number; : number };
  }
}

await ..('forkEcho', { : 0.5, : 200 }, { : true });

const echo = ..('forkEcho', { : true });
const echo: {
    decay: number;
    delay: number;
} | undefined

A filter nobody declared takes unknown, so ad-hoc payloads keep working without any of this.

Key Concepts

  • Presence is state: data holds only active filters; reading data.timescale returns undefined until you set it.
  • apply() commits the current payload without changing it. The two-argument form is deprecated in favour of set.
  • Node capability: filters the node cannot host are dropped at commit time, once it has reported its info. Filters the registry does not know are never dropped.