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.
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 everythingset, 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.
import { } from 'hoshimi';
import type { } from 'hoshimi';
declare const : ;
const = .;
await .(., { : 1.29, : 1.29, : 0.94 });
const timescale = .(.);
.(.); // 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.
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.
| Options | Envelope | Node check |
|---|---|---|
| (none), registered filter | whatever the registry resolves | yes |
| (none), unknown filter | pluginFilters[name] | no |
plugin: true | pluginFilters[name] | no |
plugin: 'name' | pluginFilters['name'][filter] | no |
top: true | filters[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.
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 validatedTyping 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.
import type { } from 'hoshimi';
declare const : ;
declare module 'hoshimi' {
interface CustomizableFilters {
: { : number; : number };
}
}
await ..('forkEcho', { : 0.5, : 200 }, { : true });
const echo = ..('forkEcho', { : true });A filter nobody declared takes unknown, so ad-hoc payloads keep working without any of this.
Key Concepts
- Presence is state:
dataholds only active filters; readingdata.timescalereturnsundefineduntil you set it. apply()commits the current payload without changing it. The two-argument form is deprecated in favour ofset.- 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.