Build eqMac

KINDAreplaces $3/mosaves $36/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A system-wide audio processor whose difficulty is almost entirely in the platform layer: a virtual output device that survives device changes and sleep, a processing chain that must never add audible latency or artefacts, and a failure mode where a bug means the user's machine has no sound at all.

estimated effort multi-daythe files for this build are in the project pack

CaptureA user-space audio server plugin presenting a virtual output deviceProcessingA biquad filter chain in a real-time safe callbackInterfaceA menu bar application communicating with the driver out of bandRoutingDevice selection and follow-the-default behaviourSafetyA hardware bypass path and an uninstaller that restores the real device

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. have readyfree if you own one

    Why Core Audio taps require 14.2 and the audio server plugin interfaces have changed over recent versions. Older systems need a different approach entirely.

    Get it Check the version before starting and decide the minimum you will support.

    Verify The system version is 14.2 or higher and the version is written down

  2. installfree

    Why An audio server plugin is a native bundle and cannot be built without them.

    Get it Install from the App Store, then the command line tools.

    Verify A sample audio server plugin builds and loads

  3. an annual fee

    Why An unsigned driver does not load on a current system, and the symptom is the device simply never appearing.

    Get it Enrol, create the certificates, and notarise a trivial build before writing the real driver. open ↗

    Verify A trivial signed and notarised bundle loads on a second machine

  4. have readyfree if you have one

    Why A broken audio driver can leave a machine silent, and debugging that on the machine you are working on is miserable.

    Get it A second Mac, a virtual machine, or at minimum a separate user account and a known-good recovery path.

    Verify You can install, break and recover audio without losing your working environment

  5. installfree

    Why Every claim in this build, from bit-identical passthrough to the absence of clicks, is verified by recording rather than by listening.

    Get it Any audio editor that shows a waveform and a spectrum, plus a loopback path to record your own output.

    Verify You can record output and inspect the waveform at a transition

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- presets(id, name, bands_json, preamp_db, created_at) · a preset is a filter chain
- bands(preset_id, ordinal, kind, frequency_hz, gain_db, q) · kind is peaking, shelf or pass
- devices(uid, name, is_default, sample_rate, channels, last_seen_at)
- routes(id, source_device_uid, target_device_uid, active) · what goes where
- state(key, value) · current preset, bypass, volume. Read by the driver, written by the interface
- The driver reads coefficients, never the model. Filter design happens in the interface and only coefficients cross into the audio thread.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
STATE_PATHrequired~/Library/Application Support/eqmac/state.jsonPresets and routing, read by the interface. The driver reads only coefficients.
DEFAULT_PRESETrequiredflatApplied at startup. Flat is the safe default, since a boosted preset at login is unpleasant.
RAMP_MSrequired20How long a gain change takes. Too short clicks, too long feels unresponsive.
FOLLOW_SYSTEM_DEFAULTrequiredtrueWhether to re-target when the system default changes, which is what unplugging headphones does.
LOG_PATHrequired~/Library/Logs/eqmac.logWritten by the interface only. The audio callback never logs.

The build, in order

  1. A virtual device the system will actually use

    Your device appears in sound settings and audio passes through it unchanged.

    1. User space, not a kernel extension. Kernel extensions are deprecated.

      Files driver/Device.cpp

    2. Prove the path before processing anything. Record both and compare.

      Files tests/passthrough.sh

    3. Before the device is ever default. A broken audio device is hard to undo by hand.

      Files scripts/uninstall.sh

    done when · tick each as it passes
    watch out
    • A virtual output device that fails to forward audio leaves the machine with no sound at all, and the user cannot easily work out why or undo it, especially since your interface may be the thing that is broken. Build and test the uninstaller in this phase, before the device is ever the default, and make the failure path fall back to the real device rather than to silence.
  2. Filters that do not click

    Equalisation applies with no audible artefact and no added latency.

    1. The callback consumes coefficients. It never designs filters.

      Files src/biquad.tsdriver/Filter.cpp

    2. Atomic swap of a coefficient block. No locks on the audio thread, ever.

      Files driver/State.cpp

    3. An instant gain change is a discontinuity and a discontinuity is a click.

      Files driver/Filter.cpp

    done when · tick each as it passes
    watch out
    • Anything in the audio callback that allocates, takes a lock or writes a log will eventually miss its deadline and produce a click, and it will do so under load rather than during testing. The callback touches pre-allocated memory and lock-free state only, and coefficient updates are published atomically from outside it.
  3. Devices appearing and disappearing

    Unplugging headphones does not break audio or lose your settings.

    1. Arrival, departure and default changes. All three are routine.

      Files driver/Devices.cpp

    2. Losing the target device should cost a moment of audio, not the session.

      Files driver/Route.cpp

    3. Test it repeatedly. It is where audio software reliably breaks.

      Files tests/sleep-wake.md

    done when · tick each as it passes
    watch out
    • Holding a reference to an output device that is then unplugged gives a driver writing into a device that no longer exists, which on a good day is silence and on a bad day is a kernel-level audio stall requiring a restart. Watch for device changes and re-target, treating disappearance as routine rather than exceptional.
  4. Presets worth switching between

    Saved chains you can move between instantly, including a true bypass.

    1. Crossfade or ramp. A gap between presets is as bad as a click.

      Files driver/Preset.cpp

    2. Compute maximum chain gain. Digital clipping is worse than the problem.

      Files src/headroom.tssrc/headroom.test.ts

    3. Bit-identical to no processing, not a flat curve. Verify by recording.

      Files driver/Bypass.cpp

    done when · tick each as it passes
    watch out
    • Boosting several bands without reducing the preamp clips the signal, and digital clipping sounds far worse than the problem the boost was meant to solve. Compute the chain's maximum gain and either apply automatic preamp reduction or refuse the setting, rather than letting the user build a preset that distorts.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more music & audio.