Tools for authoring laptop speaker tunings for Omarchy.
Laptop speakers ship voiced by the vendor's Windows DSP layer, which Linux does
not get. Omarchy restores that per model as a PipeWire filter-chain — see
docs/AUDIO-TUNING.md in the Omarchy repo for how a tuning is shipped and
applied. This repo is the other half: measuring a laptop and producing the
filter-chain.conf that goes into it.
Most people never need this. Install it only if you are building a tuning for a laptop Omarchy does not cover yet.
omarchy pkg add python ffmpeg mpv lsp-plugins-lv2lsp-plugins-lv2 supplies the lookahead limiter every tuning ends in, so it is
needed to audition a generated chain, not just to author one.
Generate the probe once, then measure, fit, and generate:
omarchy-audio-tuner probe # once, before measuring
omarchy-audio-tuner capture <raw-speaker-sink> raw.wav
omarchy-audio-tuner analyse raw.wav > raw.txtFrom a git checkout use ./omarchy-audio-tuner instead; it is the same script.
Generated files (the probe and its tone list) go to
~/.cache/omarchy-audio-tuner, so nothing is written into the install tree.
Everything measures the same way: play the probe into a sink and capture the
result. capture.sh always records the physical speaker sink's monitor, no
matter which sink you play into, because that is where both the raw and the
processed signal end up. A sink's monitor carries what it receives, so tapping a
DSP sink's own monitor measures the signal before its processing and every path
comes back identical.
The easy case, and how the Dell XPS 14 tuning was made: something already sounds right — a hand-tuned EasyEffects preset, say — and you want it as a filter-chain. Measure both paths, subtract, fit. No microphone involved.
omarchy-audio-tuner capture <raw-sink> raw.wav && omarchy-audio-tuner analyse raw.wav > raw.txt
omarchy-audio-tuner capture <ref-sink> ref.wav && omarchy-audio-tuner analyse ref.wav > ref.txt
omarchy-audio-tuner delta raw.txt ref.txt > target.txt # what the tuning must do
omarchy-audio-tuner fit target.txt 10 > fit.txt # biquads that do it
omarchy-audio-tuner generate fit.txt > filter-chain.confCopy the result into Omarchy as
default/audio/tunings/<vendor>-<model>/filter-chain.conf, add a tuning.conf
beside it, and omarchy audio tuning on.
A reference containing a compressor has no single response — it measures differently under different signals. The probe here is dense and pink-weighted for that reason, so a compressor in the path behaves roughly as it does on music. Check afterwards whether the dynamics mattered by comparing loudness range. On the XPS they did not: the reference's 8-band compressor changed LRA by 0.1 LU, so a linear chain replaced it completely.
Now you are designing, not copying, and two things change.
First, you need a calibrated measurement microphone — a UMIK-1 or similar, with its calibration file. In the copy case above the mic never appears; if you do use one there, its own response cancels in the subtraction. Deriving a target from a single absolute measurement gives it nowhere to cancel, so an uncalibrated mic bakes its own curve into the tuning as error. A shaped voice mic is disqualifying: its presence lift becomes a presence cut in your correction.
Measure near-field, a few centimetres out, to keep the room out of it:
omarchy-audio-tuner capture --from <mic-source> --seconds 4 <raw-sink> acoustic.wav
omarchy-audio-tuner analyse acoustic.wav > acoustic.txt # then subtract your cal fileSecond, and more importantly: there is no correct target curve. Flat is the wrong answer for laptop speakers — the XPS tuning is deliberately far from flat. Useful practice is to correct the sharp deviations (cabinet and port resonances) without fighting the broad roll-off you cannot win, then voice deliberately from there. That last part is taste, and no measurement settles it.
What is objective is the physical envelope, and it is worth establishing first because it decides where the high-pass goes and how much bass lift is safe:
MIC=<mic-source> omarchy-audio-tuner mic-sweep <sink> tone100.wav 100 rawThat reports harmonic distortion against level at a fixed frequency. Because it compares the same frequencies across levels, the mic's own response cancels, so an uncalibrated mic is fine for it. Sweep a few frequencies in the region you intend to lift, find where distortion takes off, and do not lift past it.
Loudness decides blind comparisons if you let it, so both tools level-match.
omarchy-audio-tuner compare <track> # calibrated A/B on one track
omarchy-audio-tuner switch # move your own playing music between candidatesswitch is the one to use with Spotify: start playing, then switch with the
number keys and the stream moves without restarting. It stops EasyEffects for the
session if it is running, because EasyEffects moves any stream that follows the
default sink to its own sink and would grab audio back from a filter-chain.
A tuning is not reviewable on "sounds better to me". Omarchy asks for these in
tuning.conf:
| Field | What it is | How |
|---|---|---|
magnitude_rms_db |
Deviation from your reference or target | analyse-dense.py |
bass_group_delay_swing_ms |
Max minus min group delay, 30–300 Hz | From the biquad coefficients |
limiter_headroom_db |
Worst-case peak vs the limiter threshold | ffmpeg volumedetect on a hot master |
dynamic_range_delta_lu |
LRA change vs the reference | ffmpeg ebur128 |
Two hard requirements, whatever the numbers say:
- End in a limiter. Peaks must stay under 0 dBFS with headroom.
- Do not boost what the drivers cannot deliver. The XPS 14 tuning deliberately cuts 40 Hz by around 18 dB. Excursion down there buys nothing and costs distortion.
These cost real time building the first tuning.
- Do not measure with a third-octave bandpass.
ffmpeg bandpass=width_type=ohas wide skirts, so wherever the response has a steep gradient the leakage does not cancel between two paths. It read +1.5 dB at 100 Hz on hardware where a sine said -3.1 dB, and produced a match figure wrong by 4.6 dB. That is why the probe is integer-Hz tones analysed over exactly one second at 48 kHz. - Do not optimise magnitude alone. Matching a convolution notch with a high-Q IIR biquad buys magnitude accuracy and pays in group delay. The first XPS fit reached 0.91 dB RMS but swung 31 ms across 63–80 Hz, which smears bass transients. Capping bass Q cost 0.33 dB and halved the swing.
- LSP
limiter_stereodefaultsalrandboostto enabled. Both add programme-dependent gain, so a fixed tuning must set both to 0 or its tone drifts with programme level.gen-filter-chain.pydoes this. - Verify where a stream landed.
mpv --audio-device=pulse/<sink>silently falls back to the default sink when the name does not resolve, andpactl move-sink-inputreports success while leaving a pinned stream where it was. The tools here check the landing sink rather than the exit code.
MIT, same as Omarchy.