A sequential plan so we can ship the menu-bar equalizer step by step.
- Create a new SwiftUI macOS app targeting macOS 15+ on Apple Silicon.
- Configure signing, hardened runtime, and microphone/audio entitlements.
- Add basic README notes on installing BlackHole 2ch for loopback use.
- Implement the menu-bar status item with a SwiftUI popover host view.
- Set up a shared
EqualiserStore(ObservableObject) for global state. - Persist minimal preferences (selected devices, bypass state) via UserDefaults.
- Build
AudioEngineManageraroundAVAudioEnginewith input/output nodes. - Insert two
AUNBandEQunits (bands 1–16 and 17–32) plus optional limiter node. - Add smooth parameter ramping utilities to avoid zipper noise.
- Implement
DeviceManagerto list Core Audio input/output devices (including BlackHole). - Allow users to pick input/output from the menu UI and reconfigure the engine safely.
- Remember the last-used devices and auto-reconnect on launch.
- Create
HALIOManagerowning akAudioUnitSubType_HALOutputAudio Unit. - Enable input/output scopes and expose
setInputDevice(id:)/setOutputDevice(id:)helpers. - Read device stream formats (ASBD) and apply them to the HAL unit for each scope.
- Add lifecycle controls (
initialize,start,stop,uninitialize) with structured logging + error propagation.
- Refactor to use two separate HAL units (one input-only, one output-only) since a single HAL unit can only connect to one physical device.
- Add
HALIOModeenum (.inputOnly,.outputOnly) to configure each unit appropriately. - Implement ring buffer (
AudioRingBuffer.swift) for lock-free audio transfer between input and output callbacks. - Register input callback on input HAL unit to capture audio and write to ring buffer.
- Register output callback on output HAL unit to read from ring buffer and process through EQ.
- Register HAL input/output callbacks that pass audio buffers to/from the EQ pipeline.
- Run the dual
AUNBandEQchain viaAVAudioEnginemanual rendering and handle buffer/latency alignment. - Guard against rate mismatches (resample or reject) and zero-fill if the EQ render returns insufficient data.
- Update
EqualiserStoreto ownRenderPipeline, persist selected device UIDs, and trigger rebuilds on change. - Surface routing status/errors to the menu UI (e.g., "BlackHole 2ch → Built-in Output" or warning on failure).
- Add Start/Stop routing buttons with proper state management.
- Add optional level meter or debug log toggle so we can verify signal presence without leaving the app.
- Add device hot-swap handling (listener for device changes).
- Scenario: macOS output → BlackHole, app input=BlackHole, output=Built-in Output; verified audio through speakers.
- Scenario: hot-swap output (e.g., to headphones) mid-stream and confirm seamless switch.
- Scenario: device removed or mic permission denied; ensure graceful fallback messaging.
- Refactor app to use
MenuBarExtra+Windowinstead ofWindowGroup+AppDelegatepopover. - Hide dock icon permanently (
NSApp.setActivationPolicy(.accessory)). - Add "Open EQ Settings" button in menu bar popover to show main window.
- Create placeholder
EQWindowViewfor the main EQ window. - Move mic permission request from
AppDelegateto app initialization. - Remove
AppDelegate(no longer needed withMenuBarExtra). - Main window should hide (not close) when user clicks close button.
| Window | Purpose |
|---|---|
| Menu Bar Popover | Quick access: device selection, routing, bypass, preset picker, open EQ settings |
| Main EQ Window | Detailed 32-band EQ controls, preset management, advanced settings |
- Design compact 32-band controls in the main EQ window (horizontal scrolling sliders).
- Add gain/frequency readouts for each band.
- Add "Flatten" button to reset all bands to 0 dB.
- Double-tap on any band slider to reset it to 0 dB.
- Display real-time level meters or band activity indicators (optional stretch goal).
- Create preset model (name + band settings + metadata) in
PresetModel.swift. - Add preset dropdown/list to menu bar popover for quick switching (
CompactPresetPicker). - Support save, rename, delete presets in main EQ window (
PresetToolbar,SavePresetSheet). - Presets stored in
.eqpresetJSON files at~/Library/Application Support/Equaliser/Presets/. - Add EasyEffects import/export support with Q-to-bandwidth conversion.
- Add user preference to display bandwidth as octaves or Q factor.
- Include factory presets: Flat, Bass Boost, Treble Boost, Vocal Presence, Loudness, Acoustic.
- Show "modified" indicator when current settings differ from loaded preset.
- Add unit tests for band-mapping logic and preset serialization.
- Add release script to bundle and package application as a .dmg.
- Prepare signed/notarized builds and optionally integrate Sparkle or TestFlight for updates.
- System EQ toggle (master bypass) — complete bypass of EQ and gains when OFF
- Compare mode segmented control ([EQ|Flat]) — A/B comparison with gains still applied in Flat mode
- Auto-revert timer (5 minutes) to switch back to EQ from Flat
- Thread-safe bypass flag access using atomic operations
- Help button (?) with popover explaining Compare Mode
- Replace SwiftUI shape-based meters with
NSView+ Core Animation (CALayer/CAGradientLayer) - Leverage GPU-accelerated rendering without Metal complexity
- Target 30 FPS smooth animations with minimal CPU overhead
- Use observer pattern for direct meter updates bypassing SwiftUI re-rendering
- Bundle custom virtual audio driver with the app (no external dependency like BlackHole)
- In-app driver installation with authentication prompt and status feedback
- Automatic driver selection on startup with transparent device naming
- Volume sync: system volume slider controls both driver and output device
- Bluetooth volume support with VirtualMasterVolume fallback
- Sample rate sync: driver matches output device sample rate
- Driver uninstall option in settings
- Driver visibility: only appears when Equaliser is running
- Allow users to select which applications route through the EQ (e.g., Spotify, Safari)
- Build app picker UI showing all audio-producing processes with icons
- Handle apps that launch/quit dynamically (add/remove from routing list)
- Support excluding specific apps from processing while others pass through
- Support parallel or serial EQ configurations (e.g., one for music, one for voice)
- Allow chaining multiple EQ presets with different band counts
- Design UI for managing EQ chain slots (add/remove/reorder)
- Consider latency implications of serial processing
- Eliminate the orange microphone indicator and microphone permission requirement.
- No orange menu bar dot appears while audio routing is active
- Investigate AirPlay SDK integration using
AVRouteDetectorandAVOutputContextAPIs - Add AirPlay device discovery separate from CoreAudio HAL enumeration
- Implement AirPlay routing alongside standard output device selection
- Handle AirPlay-specific latency (2-5 second buffer) with appropriate UI warnings
- Test with various AirPlay receivers (Apple TV, HomePod, AirPlay speakers)
- Document AirPlay limitations and latency considerations for users
- Create
FilterTypeenum with 7 industry-standard filter types (parametric, low/high pass, low/high shelf, band pass, notch) - Implement
BiquadMathwith RBJ Cookbook coefficient calculation (pure functions) - Create
BiquadCoefficientsvalue type (Equatable, Sendable) - Implement
BiquadFilterusing vDSP biquad with pre-allocated delay elements - Implement
EQChainwith lock-free coefficient updates via ManagedAtomic - Add dirty-tracking to only rebuild changed filters in
applyPendingUpdates() - Add
resetStateparameter to preserve filter memory during slider drags - Remove
AVAudioEngineandAVAudioUnitEQdependencies - Delete
ManualRenderingEngine.swiftandAudioRenderContext.swift - Integrate custom DSP into
RenderCallbackContextwith per-channel chains - Migrate
EQBandConfiguration.filterTypefromAVAudioUnitEQFilterTypetoFilterType - Add backward-compatible preset decoding for legacy presets
- Add unit tests for BiquadMath, BiquadFilter, EQChain, and FilterType
- Add preset migration tests for legacy format compatibility
- Per-channel EQ state in
EQConfiguration(leftState,rightState,channelMode) - Channel selection UI (Linked/Stereo modes with L/R focus toggle)
-
EQChaininstantiated per-channel inRenderCallbackContext -
EQChannelTargetroutes coefficient updates to correct chain(s) - Per-channel band storage in preset model (
rightBandsoptional field) - Backward-compatible preset decoding for legacy presets without channel mode
- Create
REWImporterenum-based parser for Room EQ Wizard filter files - Support all REW filter type codes (PK, LS, HS, LP, HP, BP, NOTCH variants)
- Handle Q and BW/60 bandwidth formats with proper conversion
- Parse ON/OFF filter states (OFF maps to bypassed bands)
- Integrate with Presets menu (Import REW Preset...)
- Apply imported bands to current channel focus in stereo mode
- Add user documentation (docs/user/REW-Import.md)
- Global shortcuts: Cmd+B (toggle bypass), Cmd+S (save preset)
- Band-to-band navigation: Tab/Shift+Tab moves between adjacent bands
- Value adjustment: Arrow keys increment/decrement focused value
- Popover field navigation: Enter moves through gain → frequency → bandwidth