AudioLogic

Spatial Audio Engine for Linux/PipeWire built around five distinct algorithms.

AudioLogic is a spatial audio engine for Linux/PipeWire built around five distinct algorithms, each solving a different problem: manufacturing width from narrow masters, re-placing the diffuse field already in a recording, synthesizing distance and space, or decoding matrixed surround. It runs as a persistent virtual sink — every application routes through it automatically — and control lives in a small tray panel: pick the algorithm and output device, then work the parameters directly. On the default deep_field, that means dialing distance, space size, field level and air absorption in real time, with a wet/dry blend that A/Bs against the untouched signal so you can hear exactly what the processing costs.

The design is honest about its trade-offs: bit-exact bypass where it claims one, one-way processing that widens without collapsing your center, and documented limits where the field falls short of its own spec. Whether it improves a given recording depends heavily on the source material and your render profile — speakers and headphones get deliberately different cues — because it recovers and re-places what's there, it doesn't invent detail that was never captured.

Install on Fedora

Add my package repository once (Fedora 41+ / dnf5):

sudo dnf config-manager addrepo --from-repofile=https://mattmierzwinski.com/fedora/mattmierzwinski.repo

Then install (and later update) the package like any other:

sudo dnf install audiologic

On older Fedora with dnf4: sudo dnf config-manager --add-repo https://mattmierzwinski.com/fedora/mattmierzwinski.repo

Or download the RPM manually

Other builds

Portable builds for distributions without my DNF repository. These are not managed by the package manager -- update them by downloading again.

Dependency tree

audiologic pulls following dependencies:

  • /usr/bin/bash
  • SDL2
  • gtk3
  • libSDL2-2.0.so.0 (64bit)
  • libatk-1.0.so.0 (64bit)
  • libc.so.6 (64bit)
    • GLIBC_2.14 (64bit)
    • GLIBC_2.2.5 (64bit)
    • GLIBC_2.32 (64bit)
    • GLIBC_2.34 (64bit)
    • GLIBC_2.38 (64bit)
    • GLIBC_2.4 (64bit)
  • libcairo-gobject.so.2 (64bit)
  • libcairo.so.2 (64bit)
  • libgcc_s.so.1 (64bit)
    • GCC_3.0 (64bit)
  • libgdk-3.so.0 (64bit)
  • libgdk_pixbuf-2.0.so.0 (64bit)
  • libgio-2.0.so.0 (64bit)
  • libglib-2.0.so.0 (64bit)
  • libgobject-2.0.so.0 (64bit)
  • libgtk-3.so.0 (64bit)
  • libharfbuzz.so.0 (64bit)
  • libm.so.6 (64bit)
    • GLIBC_2.2.5 (64bit)
    • GLIBC_2.27 (64bit)
    • GLIBC_2.29 (64bit)
  • libpango-1.0.so.0 (64bit)
  • libpangocairo-1.0.so.0 (64bit)
  • libpipewire-0.3.so.0 (64bit)
  • libstdc++.so.6 (64bit)
    • CXXABI_1.3 (64bit)
    • CXXABI_1.3.5 (64bit)
    • CXXABI_1.3.9 (64bit)
    • GLIBCXX_3.4 (64bit)
    • GLIBCXX_3.4.11 (64bit)
    • GLIBCXX_3.4.14 (64bit)
    • GLIBCXX_3.4.18 (64bit)
    • GLIBCXX_3.4.19 (64bit)
    • GLIBCXX_3.4.20 (64bit)
    • GLIBCXX_3.4.21 (64bit)
    • GLIBCXX_3.4.22 (64bit)
    • GLIBCXX_3.4.26 (64bit)
    • GLIBCXX_3.4.29 (64bit)
    • GLIBCXX_3.4.31 (64bit)
    • GLIBCXX_3.4.9 (64bit)
  • libvulkan.so.1 (64bit)
  • libz.so.1 (64bit)
  • pipewire
  • rtld
    • GNU_HASH
  • vulkan-loader
  • wireplumber

Read straight from the package's RPM headers. A library required at several symbol versions is listed once, with those versions nested beneath it. Dependencies this repository provides are expanded; the rest are satisfied by Fedora itself and shown as leaves, because resolving those would mean mirroring Fedora's own repositories.

Changelog

[2.4.3] — 2026-08-19

Fixed

  • The RPM could not be installed on another machine. dnf refused it with nothing provides libm.so.6(GLIBC_2.43). The package was correct — that requirement is generated from the binary — but the requirement itself was too new: glibc 2.43 (Fedora 44) introduced fresh symbol versions for three single-precision maths functions (sqrtf, atan2f, log10f), and building here recorded a dependency no older distribution can satisfy.

    The two DSP binaries now route those calls through the double-precision entry points, which are still versioned at glibc 2.2.5, and compile with -fno-math-errno so sqrt becomes a hardware instruction instead of a libm call at all. The floor drops from glibc 2.43 to 2.38 (Fedora 39 and newer), and the numbers are unchanged.

    Adding Requires: glibc >= 2.43 would not have fixed this — it only renames the failure on a machine whose repositories have no such glibc.

[2.4.2] — 2026-08-19

Fixed

  • The daemon no longer dies between songs. Every two or three track changes the audio stopped, because audiologicd was being killed outright — not crashing — and systemd was restarting it three seconds later. PipeWire runs the audio callback on a real-time thread and, from inside our own process, lowers RLIMIT_RTTIME to 200 ms; the kernel kills a real-time thread that computes for that long without waiting, and because the soft and hard limits are equal there is not even a warning signal first. A track boundary suspends and resumes the graph, the graph then runs cycles back to back to catch up, and at roughly 7 ms of DSP per cycle it takes only a few dozen of them to reach the limit.

    The audio thread now measures itself: after 60 ms of computing with no wait in between, it passes audio straight through until the graph has caught up, fading across the seam rather than stepping. The effect gives way for a block or two; the daemon does not die. Measured on the failure as reported — Bluetooth output, deep_field, skipping tracks every 14 seconds — twenty consecutive track changes with zero restarts, where the same test previously killed the daemon twice in seven. The six bursts that did occur cost one 42 ms block each.

    The daemon reports them (real-time relief engaged (N bursts, M blocks …)), so a machine that has to shed work constantly is visible rather than silent.

Note

  • The unit's LimitRTTIME=infinity never had the effect its comment claimed: the limit is lowered by PipeWire after systemd sets it, from inside the process, and a hard limit cannot be raised back without privileges the daemon does not have. The comment now says so.

[2.4.1] — 2026-08-19

Added

  • Switch output device from the tray icon: right click → Output device, with Automatic and every device the daemon can see. Changing output is the thing this applet is asked for most often and it does not deserve a window. It is also unusually painless here, and the reason is structural rather than clever: every application is connected to one virtual sink, so the change happens downstream of them — nothing an application holds has to move, and none of them notice. The usual "most things moved, but one app is still on the old speakers" cannot happen.

[2.4.0] — 2026-08-19

Added

  • Bluetooth output works, and any other kind of sink. The routing used to select its target by matching node names against alsa_output, so a Bluetooth speaker was invisible to it however highly PipeWire ranked it — on the machine this was found on, the connected speaker outranked the built-in card and was still never chosen.
  • An output-device selector in the tray panel, and output_device on the control surface (audiologic devices, audiologic set output_device …). Automatic — the highest-priority sink — remains the default. A chosen device is honoured whenever it is present, and when it disappears the daemon falls back to the best available rather than to silence, returning to the choice when it returns.

Changed

  • Routing moved into the daemon. route-daemon.sh, unroute-daemon.sh, move-streams.sh and the suspend watcher are gone, along with the audiologic-resume.service unit and the package's python3 and pipewire-utils dependencies. The daemon holds the PipeWire connection that owns the nodes, so it links them itself and reacts to registry events: a device appearing, disappearing or re-enumerating after suspend is now an event rather than a retry loop or a second service watching logind.
  • The router also removes bypass links — a player left feeding the hardware directly, which happens whenever the daemon was briefly not running, and which sounds exactly like the DSP doing nothing.

[2.3.1] — 2026-08-19

Changed

  • The tray panel's masthead. The logotype now spans the top of the panel as a white full-width bar with the logo centred in it, flush to the top edge. The artwork is a horizontal lockup — the mark beside the wordmark, trimmed of the whitespace the stacked version carries — because the stacked logotype is nearly square: at any width that keeps it legible it eats a third of a control panel, and at a height that does not, it is unreadably small.

[2.3.0] — 2026-08-19

Fixed

  • The daemon was killed on every track change while the visualizer was running, taking the visualizer down with it (BindsTo) and interrupting playback for about three seconds. The visualizer's capture stream asked PipeWire for RT_PROCESS, which put its callback inside the same real-time graph cycle as the DSP. PipeWire runs that cycle on an rtkit-elevated SCHED_RR thread with a 200 ms RLIMIT_RTTIME, and the kernel SIGKILLs a real-time thread that overruns it — so a graph re-negotiation, which is exactly what a track change causes, killed audiologicd outright (status=9/KILL). The visualizer now captures on an ordinary thread, as its own design always said it should; a missed buffer costs a slightly older frame and nothing else. A compile-time assertion keeps the flag from coming back, the capture asks for the same 2048-frame quantum the daemon pins, and the daemon's unit sets LimitRTTIME=infinity as a backstop.

Added

  • Branding. The logotype now appears at the top of the tray panel (compiled into the binary, so it cannot go missing), the tray menu has an About AudioLogic entry and the panel a footer link, both opening the project page. The CLI and the visualizer print the project and its page on --help. The visualizer opens with a card carrying the logotype's mark — concentric arcs split by a centre bar, in the brand magenta — drawn as ordinary line geometry so whichever preset is running tears it apart, like any other title. The tray icons are redrawn as that same mark, magenta when processing and navy when in pass-through. Written by Matt Mierzwinski; https://mattmierzwinski.com/page/audiologic.

[2.2.0] — 2026-08-19

Added

  • The visualizer announces what is playing. On a track change the name appears large in the centre of the screen, holds for two seconds while it is legible, and is then handed to the feedback field — where the preset's warp tears it apart and the decay takes it away, the way Milkdrop did it in Winamp. The two stages are two different render passes: crisp on the composited frame first, then geometry inside the field.
    • Read over MPRIS, the freedesktop standard, so it works with any compliant player — Spotify, browsers, VLC, mpv, Rhythmbox — and prefers whichever player is actually playing when several are on the bus.
    • Drawn with a built-in vector stroke font: no font file to ship or find, and the text is ordinary line geometry, which is exactly why the preset can destroy it for free.
    • A missing player or session bus is not an error, and a player re-emitting its metadata (on a pause, a seek, a volume nudge) does not put the title back on screen.

[2.1.0] — 2026-08-19

Changed

  • Space now shows the next preset — it is the key everyone presses without reading anything. Locking moved to L, and Space releases a lock rather than doing nothing.

Added

  • Eighteen more visualizer presets, twenty in total, each built around a different motion signature rather than a variation of one: vortex, kaleidoscope, starfield, ripple, smoke, pulse grid, spiral galaxy, liquid, neon scope, deep space, fracture, breathe, stormfront, inversion, helix — plus three built on the beat: hyperspace (flight, throttled by bass), blackhole (the field falls inward hardest near the horizon and the whole picture flares on each kick) and supernova (still between kicks, then a charge-and-blast outward with a gamma flash).
  • Seven waveform modes for presets to select with nWaveMode — a ring, an X-Y oscilloscope of the stereo field, the centred line, two lines one per channel, an outward spiral, a spectrum arc, and a radial burst with one spoke per band. Previously every preset drew the same line, which made presets with quite different equations look alike.
  • Additive blending for waveforms (bAdditiveWaves), which is what keeps a wave visible against a bright feedback field.
  • A corpus test. Every shipped preset must compile with no unsupported construct, stay finite and on-screen on material from silence to overload, and be measurably different from every other preset — each is reduced to what it does to the field and no two may land in the same place. It caught two near-duplicates while the set was being written.

[2.0.0] — 2026-08-18

Added

  • Two matrix surround decoders, matrix_surround and matrix_steer. These decode rather than invent: they assume the two channels carry a matrixed surround mix, recover left, right, centre and the surrounds, and fold them back into two. The passive one is fixed and exactly mono-safe; the active one adds a dominance servo and crosstalk cancellation, measuring better than 45 dB of separation on encoded material against the passive matrix's 3 dB. Both are the cheapest algorithms in the project — 0.8 % and 2.2 % of a core — and add no latency. Knobs: surround_gain, plus centre_width, dimension and panorama for the steered decoder.
  • audiologic-vis, a Milkdrop-class visualizer. Preset visuals driven by what you are actually hearing: a feedback renderer (Vulkan, RGBA16F ping-pong targets) whose motion comes from .milk preset programs — an expression compiler and register VM run the per-frame and per-vertex equations, with the audio bound to the variables presets are written against. Alt+Enter or F for fullscreen, N/P to change preset, Space to lock, Esc to quit. Opened from the tray application's Milkdrop button, its menu, or systemctl --user start audiologic-vis.
    • It cannot affect playback. A separate process with its own PipeWire capture stream: no code path into the daemon, no socket, no shared memory. The systemd pin is one-way (BindsTo), and it never requests real-time scheduling.
    • Verified on Intel integrated graphics; discrete GPUs are preferred where both are present, and AUDIOLOGIC_VIS_GPU overrides the choice.
    • Two starter presets are installed on first run. The classic third-party corpus is not redistributed.

Changed

  • The RPM and tarball now carry the visualizer, its user unit and the starter presets. New runtime requirements: SDL2 and vulkan-loader (a GPU driver is a runtime matter — mesa on Intel and AMD, the proprietary package on NVIDIA).

[1.4.3] — 2026-08-18

Fixed

  • Settings were never saved. The daemon read its config file at start-up and never wrote to it, so everything dialled in over a session lived only in that process's memory: restarting it — or having it die — silently reverted to the built-in defaults, which is indistinguishable from the controls not working. Every accepted change is now written back to ~/.config/audiologic/config.json as it is made, atomically (a temporary file and a rename, so an interrupted write cannot leave a config that will not load), and the file is created on first use if it does not exist. A change that cannot be saved is still applied, and the reply says so.
  • Whether processing was on is remembered too, so a daemon that restarts comes back processing rather than silently in pass-through. A daemon that has never been told anything still starts in pass-through.
  • The playback profile ("Render for") was unreachable on Deep field, though the algorithm has its own speaker and headphone profiles — it was scoped to the immersive algorithm alone. On headphones the speaker profile is close to transparent, so this was audible as the algorithm doing less than it should.
  • recycle had no control at all despite being a live knob; it now has a slider on the Deep field panel.

Changed

  • The shipped defaults are now the deep_field tuning in use: deep_field on headphones, distance 8, space size 12 s, field level −26.1 dB, air 0.22, envelopment 1.0, crispness +6 dB at 6766 Hz. Existing config files are untouched; this is what a fresh install starts from.

[1.4.2] — 2026-08-18

Added

  • A bass control, in the panel and live (audiologic set bass 0.6). It is a trim rather than a tone control: 1.0 (the default) is the track's own bass, passed through bit-exactly — the filter is skipped, not run flat — and 0.3 is about −10.5 dB below 180 Hz. It applies to every algorithm, and is there for the low-frequency build-up a large field_size or a close distance produces in deep_field. Measured on real material at 0.3: −9.7 dB below 180 Hz, +0.2 dB from 1–6 kHz, and −2.7 dB through the low mids where the shelf's slope reaches.

Fixed

  • audiologic --help listed the live keys from a hand-written list that had fallen six knobs behind, so the deep_field controls were undiscoverable from the command line. It is generated from the knob table now, like the status reply.

[1.4.1] — 2026-08-18

Fixed

  • The Deep field sliders snapped back a second after being moved. The value was sent and applied, but the daemon's status reply — which the tray panel re-reads on a timer — never mentioned the six deep_field knobs, so on the next read the panel saw nothing for them and put its sliders back to the defaults. The status reply is now generated from the same table that defines the knobs, so every knob the daemon accepts is also reported. Requires restarting the daemon (systemctl --user restart audiologicd, or Restart daemon in the panel) — the fix is in the daemon, not the panel.

[1.4.0] — 2026-08-18

Added

  • A third algorithm: deep_field. Distance and a synthesized, boundary-less space — a source can be placed far away and surrounded, which neither other algorithm can do (the echo widener has no distance model, and the immersive algorithm is required by test to leave a dry source exactly where it is). It adds no discrete echo: a velvet-noise diffuser makes the field dense from its first millisecond, and an eight-line feedback network with in-loop diffusion carries the decay. Knobs: distance, field_size, field_level, air, envelopment, recycle, all live and all in the tray panel. Cost: about 23% of one core; no added latency.
  • The field ducks where a recording already has one. deep_field measures how diffuse each frequency band already is and sends only that much into the space, so a dry instrument gets the full treatment while an already-reverberant mix is left alone rather than given a second room. recycle sets how much of the already-diffuse part still goes through; 0, the default, sends none of it.
  • Speakers and headphones get their own field tuning, and a mild behind-the-head colouring is applied to the surrounding field only, never to the sound itself.
  • The tray application replaces a running instance. Starting it a second time terminates the first rather than adding a second icon and a second poller to the same daemon. It checks that the recorded process is still alive AND is the same program before signalling anything, so a recycled process id is never disturbed. --allow-multiple opts out. It also shuts down cleanly on SIGTERM now, so the icon disappears and the record is released.

[1.3.0] — 2026-08-18

Fixed

  • The immersive algorithm pulled centred voices toward the left ear. A vocal could end up sounding as though the singer were standing at your left eye rather than in front of you. Each of the algorithm's per-frequency adjustments nudges one narrow slice of the sound to one side, and it is only their alternation across the spectrum that turns that into width instead of a pan — but the alternation had become slower than the bandwidth of a voice, so a whole voice was pushed the same way. Measured at 7.2 dB into the left ear; now within 0.15 dB. The alternation is faster, and the left/right balance of every frequency band — both its level and its timing — is now measured and restored, so the effect can no longer move the image sideways at all. A source that the mix itself panned stays exactly where the mix put it.

Changed

  • "Ambience depth" is now "Distance", and it does what the name says. It trades the direct sound against the space around it — the ear's primary distance cue — instead of merely turning the ambience up: above 1.0 sources move back into the room, below 1.0 they come forward, 1.0 is neutral. Its range is now 0.25–4.0. If you had it set to 2.0, that now means "a little further away" rather than "6 dB more ambience"; try higher values for real distance.
  • Slightly less width than 1.2.1 on ordinary material (inter-channel coherence 0.50 → 0.40 at the defaults, rather than 0.37). Holding the left/right balance costs some of it, because the widening works by imbalancing individual frequencies. Centred and correct beats wide and pulled to one side.

[1.2.1] — 2026-08-18

Fixed

  • The immersive algorithm could make already-wide material NARROWER, which is the opposite of its purpose. On a production that has already spread a vocal out in space — a Schiller record, say — the vocal was pulled back into the centre of the head. Two causes, both fixed:
    • Its mid/side rotation equalises mid and side rather than widening, and where a bin's mid and side components are correlated — which is exactly what studio widening produces — one direction of that rotation cancels the side signal outright. The rotation is now one-way: it can only ever move energy out of the centre, never into it, whatever the material.
    • The "rear-ness" colouring was net-attenuating and was applied in proportion to how diffuse a bin was, so it quietened the widest content in the mix in the presence band, letting the dry centre dominate. It is now normalised to leave the level alone and only shape the field.
  • Measured, at the strongest settings available: a reverberant spread-out vocal now comes out 43% wider instead of collapsing, already-decorrelated material is no longer narrowed, and studio-widened material is left exactly as it is rather than being "corrected".

Changed

  • The effect is a little gentler on ordinary material than 1.2.0 (inter-channel coherence 0.50 → 0.37 on headphones at the defaults, rather than 0.21) — 1.2.0 bought some of its strength by widening in ways that damaged material which was already wide. Reach for ambience_depth if you want more.

[1.2.0] — 2026-08-18

Fixed

  • The tray application did not work on a comma-decimal locale (Polish, German, French, most of continental Europe). GTK adopts the system locale on startup, so the panel wrote settings as 0,42 — which the daemon rejected as "not a number" — and could not read the daemon's own 0.70 either, stopping after the first character. The result was a panel whose sliders showed defaults, refused every change with an error, and made the immersive algorithm look like it did nothing. Settings are now always written with a dot and accepted with either a dot or a comma, so a value typed by hand in a comma locale works too.
  • Values that are on/off are always shown as a toggle in the panel, never as a slider that could be dragged to a rejected value. (The one such setting, Anti-flutter, already was; the check is now structural.)

Added

  • The tray application starts the daemon for you. If audiologicd is not running when the panel opens, it is started (systemctl --user start — a user unit, so no password and no privileges). Skip it with --no-start.
  • A "Restart daemon" button, in the panel header and in the tray menu. It stays available while the daemon is down, which is when you need it. Because the daemon always comes back in pass-through, processing is switched back on afterwards if it was on before — a restart does not silently change what you were hearing.

Changed

  • The immersive algorithm is considerably stronger, especially on headphones. Its inter-channel cues had been tuned for speakers and applied to headphones as well, where there is no acoustic crosstalk to work against; the headphone profile now gets much wider cues of its own. Measured on ambience-bearing material, inter-channel coherence at the defaults: 0.50 → 0.21 on headphones and → 0.31 on speakers (previously 0.25 and 0.40), with the loudness unchanged.
  • ambience_spread now defaults to 1.0 (was 0.7). It changes placement only, not level, so there is nothing to trade off by having it at full.
  • Slightly more of a master's ambience is now recognised as ambience, so there is more for the algorithm to work with on ordinary mixes. Mono and centred content still pass through bit-exactly.

Note

Set "Render for" to match what you are listening on. It defaults to speakers, and on headphones that setting is deliberately gentle enough to sound nearly transparent — this is the single biggest reason for "I can't hear it".

[1.1.0] — 2026-08-18

Added

  • A second spatial algorithm, selectable live. spatial_algorithm chooses between echo_widener (the default — unchanged) and experimental. Switch it without restarting: audiologic set spatial_algorithm experimental.
  • Immersive reconstruction (experimental): deep, enveloping width with nothing echoed. Per frequency bin it separates the coherent source from the diffuse field already recorded around it, leaves the source exactly where the mix put it, and re-places the diffuse part around you using coherence, level and frequency-dependent phase — no delays, no reverb. Mono and centred content pass through bit-exactly, so a centred voice never moves.
  • New knobs for it, all live-settable and shown in audiologic status: render_target (speakers / headphones), ambience_spread (how far the diffuse field is re-placed; 0 bypasses the stage), ambience_depth (its level; 1.0 is energy-exact), direct_spread (0 keeps instruments in place, above 0 spreads their partials around you too).
  • audiologic-tray, a desktop application for XFCE. A permanent system-tray icon: left click opens a control panel with a switch, sliders and mode selectors for every live knob, middle click toggles processing, right click opens a menu. Closing or minimising the window returns it to the tray. Desktop notifications when the daemon appears or disappears, when processing is switched on or off, when the algorithm changes, or when a setting is refused. Knobs belonging to the algorithm that is not running are hidden. Requires gtk3. Start it at login with: cp /usr/share/audiologic/audiologic-tray-autostart.desktop ~/.config/autostart/

Changed

  • audiologic status now also reports spatial_algorithm, render_target, ambience_spread, ambience_depth and direct_spread.
  • audiologic set accepts the five new keys above. Every existing key behaves exactly as before.

Security

  • The configuration parser rejects unknown or wrong-typed values for the two new string settings (spatial_algorithm, render_target) instead of silently falling back to a default, and range-checks the three new numeric ones. As before, a rejected configuration leaves the running one untouched.

Upgrading

Nothing to do. An existing ~/.config/audiologic/config.json keeps working and keeps sounding identical: the new settings default to the previous behaviour, and the default algorithm is byte-for-byte the one you had. To try the new one, set spatial_algorithm to experimental (or pick "Immersive" in the tray app) — and spatial_algorithm back to echo_widener to return.

[1.0.0] — 2026-07-10

Added

  • First release: a real-time stereo widener for PipeWire, installed as a routable virtual output device that every application can play into, with per-band echoes, band-logic steering, STFT direction re-staging, a broadband Haas widener, a treble shelf, DC blocking and a soft-knee limiter.
  • audiologicd (the DSP daemon, pass-through until enabled), the audiologic CLI (status/on/off/reload/set), offline WAV processing, a systemd user unit and suspend/resume self-healing.

Built with technologies I love

This blog runs on open, proven tools — chosen for reliability, not popularity.