---
name: macbook-speaker-eq
description: Fix thin, tinny internal speakers on a 2013 to 2015 MacBook Pro (MacBookPro11,x with the Cirrus CS4208 codec) running Omarchy or any Arch based PipeWire desktop. Installs EasyEffects with a measured correction curve, autostarts it, and clears the rtkit realtime limit bug that makes PipeWire clients die every second.
---

# macbook-speaker-eq

Use this when someone says their MacBook Pro speakers sound like earbuds
across the room under Linux, or when EasyEffects and other PipeWire clients
"launch and never appear" or audio plays for two seconds and stops.

## What is actually wrong

The hardware is fine. Both DACs are driven in normal stereo (tweeters on pin
0x12 from DAC 0x03, woofers on pin 0x13 from DAC 0x04). There is no hidden
GPIO, EAPD, or vendor coefficient that turns the bass on. GPIO0 is the
speaker enable and the `mbp11` kernel fixup already sets it.

What macOS does that Linux does not is EQ. Measured with the internal mic and
a log sine sweep, the raw speaker response relative to 800 Hz to 2 kHz is
roughly:

| Band | Level |
|---|---|
| 250 Hz | -19 dB |
| 315 Hz | -15 dB |
| 400 Hz | -12 dB |
| 500 Hz | -10 dB |
| 3 kHz and up | +7 to +10 dB |

The woofers do move down to 40 Hz at full level, so the fix is correction,
not replacement.

## Do not chase these

- Do not A/B by muting the ALSA `Speaker` or `Bass Speaker` controls with
  amixer. WirePlumber sees the mute and PipeWire soft mutes the whole sink,
  so you hear nothing from either driver and conclude the woofers are dead.
  Mute a DAC at the codec instead:
  `hda-verb /dev/snd/hwC1D0 0x03 0x300 0xb0f7` (mute tweeter DAC),
  `0xb077` to restore. `hda-verb` is in `alsa-tools`.
- Do not switch the card to a 4.0 profile. Stereo already feeds both DACs.
- Do not add `model=` overrides in modprobe. None of the cs420x quirks help.

## Step 1: install

```bash
sudo pacman -S --needed easyeffects rtkit
sudo systemctl enable --now rtkit-daemon
```

## Step 2: the rtkit trap (read this even if audio seems fine)

If `xdg-desktop-portal` started before rtkit existed, it caches
`RTTimeUSecMax=0` and `MaxRealtimePriority=0` from the Realtime portal.
PipeWire's `module-rt` then sets `RLIMIT_RTTIME=0` on every client's
realtime thread and the kernel SIGKILLs it on the first tick. Symptoms:
EasyEffects launches and vanishes, YouTube plays two seconds then stops,
`pw-top` shows clients appearing and disappearing.

Check:

```bash
busctl --user call org.freedesktop.portal.Desktop /org/freedesktop/portal/desktop \
  org.freedesktop.DBus.Properties Get ss org.freedesktop.portal.Realtime RTTimeUSecMax
```

`0` means you are hit. Fix:

```bash
systemctl --user restart xdg-desktop-portal
```

It should now report 200000. With rtkit enabled at boot it does not recur.
To prove the cause rather than guess: ftrace `signal_generate` filtered on
`sig==9` shows the kernel sending the kill to the `data-loop.0` thread with
`si_code=128`, and `strace -f` on the client shows
`prlimit64(RLIMIT_RTTIME, {0, 0})` right before it dies.

## Step 3: install the preset

Save this as `~/.local/share/easyeffects/output/MacBook Speakers.json`.
The equalizer input gain of -8 dB is the headroom for the +11 dB shelf. If
kick drums pop or the level pumps, lower that number first, not the bands.

```json
{
  "output": {
    "blocklist": [],
    "plugins_order": ["equalizer#0", "limiter#0"],
    "equalizer#0": {
      "bypass": false, "mode": "IIR", "input-gain": -8.0, "output-gain": 0.0,
      "num-bands": 7, "split-channels": false, "balance": 0.0,
      "pitch-left": 0.0, "pitch-right": 0.0,
      "left": {
        "band0": {"type":"Hi-pass","mode":"RLC (BT)","slope":"x1","solo":false,"mute":false,"gain":0.0,"frequency":70.0,"q":0.7,"width":4.0},
        "band1": {"type":"Lo-shelf","mode":"RLC (BT)","slope":"x1","solo":false,"mute":false,"gain":11.0,"frequency":250.0,"q":0.7,"width":4.0},
        "band2": {"type":"Bell","mode":"RLC (BT)","slope":"x1","solo":false,"mute":false,"gain":7.0,"frequency":350.0,"q":0.7,"width":4.0},
        "band3": {"type":"Bell","mode":"RLC (BT)","slope":"x1","solo":false,"mute":false,"gain":5.0,"frequency":160.0,"q":1.0,"width":4.0},
        "band4": {"type":"Bell","mode":"RLC (BT)","slope":"x1","solo":false,"mute":false,"gain":-4.0,"frequency":1100.0,"q":0.8,"width":4.0},
        "band5": {"type":"Bell","mode":"RLC (BT)","slope":"x1","solo":false,"mute":false,"gain":-3.0,"frequency":4000.0,"q":1.1,"width":4.0},
        "band6": {"type":"Hi-shelf","mode":"RLC (BT)","slope":"x1","solo":false,"mute":false,"gain":-9.0,"frequency":2500.0,"q":0.7,"width":4.0}
      },
      "right": "SAME AS LEFT (copy the left object)"
    },
    "limiter#0": {
      "bypass": false, "input-gain": 0.0, "output-gain": 0.0,
      "mode": "Herm Wide", "oversampling": "None", "dithering": "None",
      "sidechain-type": "Internal", "lookahead": 5.0, "attack": 5.0,
      "release": 40.0, "threshold": -0.5, "gain-boost": true,
      "sidechain-preamp": 0.0, "stereo-link": 100.0, "alr": false,
      "alr-attack": 5.0, "alr-release": 50.0, "alr-knee": 0.0, "alr-knee-smooth": -5.0
    }
  }
}
```

Replace the `"right"` placeholder with a copy of the `left` object. Then:

```bash
easyeffects -l "MacBook Speakers"
wpctl status | grep -i easyeffects   # easyeffects_sink should be the default output
```

If it is not the default, `wpctl set-default <id of easyeffects_sink>`.

## Step 4: autostart

Omarchy (Hyprland, Lua config), append to `~/.config/hypr/autostart.lua`:

```lua
-- Speaker EQ for the MacBookPro11,2 (preset "MacBook Speakers"); window hidden.
o.launch_on_start("easyeffects -w")
```

Then `hyprctl reload && hyprctl configerrors` and expect no errors. On any
other desktop use `exec-once = easyeffects -w` or the EasyEffects
"start hidden on login" option under its preferences.

## Tuning by ear

Open EasyEffects, Output tab, Equalizer. Bands 1 to 3 (250, 350, 160 Hz)
are the bass and low mids. Band 4 (1.1 kHz) is the presence cut, band 6
(2.5 kHz shelf) is the treble. The Bypass toggle compares against raw. Save
changes under Presets so they survive a restart.

## Measuring your own curve

If the numbers above do not suit your unit, generate a 20 Hz to 20 kHz log
sweep, record it with the internal mic at a moderate volume (the limiter
will flatten the measurement if you go loud), and take the max level per
1/3 octave band over the whole recording. Alignment does not matter for a
max over time method. The internal mic is deaf below about 120 Hz, so do not
read anything into that region.

## Fallback: PipeWire filter-chain instead of EasyEffects

The same curve works as `libpipewire-module-filter-chain` with builtin
`bq_highpass`, `bq_lowshelf`, `bq_peaking`, `bq_highshelf` nodes in that
order, feeding the ALSA sink. EasyEffects is simply easier to adjust. Never
run both at once.
