Skip to main content

Function: useEqualizer()

function useEqualizer(player: Player | undefined, options?: UseEqualizerOptions): Equalizer;

Defined in: packages/player/src/hooks/useEqualizer.ts:416

A ten-band equaliser as one hook: the curve, the presets, the persistence, and the one af write that puts it on the signal.

Before this, an EQ screen meant composing four exports and owning the state between them — EQUALIZER_PRESET_LIST for the chips, equalizerPresetChain to compile, defineEqualizerPreset for a user curve, setAudioFilters to apply, and a useState per slider. That is the library making the app do its arithmetic. This is the same machinery with the bookkeeping done:

const eq = useEqualizer(player)

return (
<>
{eq.presets.map((p) => (
<Chip key={p.id} label={p.name} active={p.id === eq.preset?.id}
onPress={() => eq.applyPreset(p)} />
))}
{eq.bands.map((band, index) => (
<Slider key={band.frequency} value={band.gainDb}
minimumValue={eq.gainRangeDb.min}
maximumValue={eq.gainRangeDb.max}
onValueChange={(db) => eq.setBandGain(index, db)} />
))}
</>
)

Parameters

ParameterTypeDescription
playerPlayer | undefinedThe player, or undefined before it has been created. The hook holds its state either way, so a screen can render (and be edited) before the core exists; the chain is written as soon as one appears.
optionsUseEqualizerOptionsSee UseEqualizerOptions.

Returns

Equalizer

See Equalizer.

Remarks

There is no onBandChange/onPresetChange event, on purpose. The returned object is the notification: every mutator re-renders the component that holds the hook, and the value is a fresh immutable snapshot. An event carrying the same fact would be a second source of truth for consumers to fall out of sync with, and a subscription for the common case (one EQ screen) to pay for. Several components can call the hook independently — they will each have their own curve, which is the honest consequence of not keeping app state in the library; hoist it if you want one.

What is written, and when — drag it, it is built for that. Nothing is written unless the compiled chain would actually differ, and how it is written depends on what changed:

  • Only gains changed (the slider case): each moved band is pushed into the running filter with Player.setAudioFilterParam — mpv's af-command, which updates the biquad's coefficients and leaves its state alone. No chain rebuild, no blocked JS thread (the command is async), no click. The curve is compiled editable precisely so this path is available: ten labelled bands whose shape never depends on their values.
  • The graph changed (EQ toggled, chain options changed, or the curve left/returned to flat): one Player.setEqualizerFilters, which rebuilds the entries that differ. That is the expensive path, and it is now reached once per gesture at most.
  • COMMIT_DELAY_MS after the last in-place change: one setEqualizerFilters that makes mpv's af property agree with the running chain again, so the curve survives the next track, device switch or normalization toggle. Until it lands, player.getAudioFilters() still shows the pre-drag string — that read-back is mpv's property, and the whole point of the fast path is not to write it.

An af-command that mpv refuses (nothing playing, an older engine, a filter that will not take the parameter at runtime) switches this hook to commit-only for the rest of the player's life: the curve then lands once the gesture settles rather than following the finger. That is the honest degradation — the alternative, rewriting the chain per frame, is what makes playback stutter, and it is never the right answer.

One timer, and only for the commit above. Nothing else here ticks; every update is caused by a call you made or by the persisted record arriving. The pending commit is flushed on unmount.

Unmounting does not clear the chain. af is a global mpv option that survives track changes, and an EQ screen closing is not a reason to stop equalising. Call setEnabled(false) — or player.clearAudioFilters() — to take it off.

Ownership is scoped to the entries it wrote. This hook owns Player.setEqualizerFilters — the labelled @rnmedia_eq_… half of the chain — and nothing else. player.setAudioFilters([...]) keeps working while an EQ screen is mounted, and keeps working after a slider drag: the two halves are composed (equaliser → your chain → loudness normalization), not overwritten. UseEqualizerOptions.extraFilters was the workaround for the old wholesale behaviour and is deprecated.