jonathan warykowskijwarykowski

keybars

A music equaliser on your keyboard's backlight: whatever the computer plays, bass on the left and treble on the right.

Each column of keys becomes a bar rising from the bottom row, with a white peak dot that holds a moment and falls like a hi-fi meter. keybars sends only the keys that changed, so it keeps up with the music at about 30 frames a second. When the music stops, the keys go back to your layout’s colours.

It works with the ZSA Moonlander and Voyager today, through Keymapp. Any other per-key RGB keyboard needs only a backend and a map.

Install

You need:

  • a supported keyboard, with Keymapp running and its API turned on in its settings
  • cava to listen to the audio: brew install cava, dnf install cava or apt install cava
  • on macOS, a loopback device such as BlackHole: see macOS audio
  • Go 1.25 or later
go install github.com/jwarykowski/keybars@latest
keybars version

Quick start

Check the keyboard first. test sweeps a bar across each column, then fills the board row by row, with no audio involved:

keybars test

Then play some music and run it:

keybars

It finds the connected keyboard on its own and leaves the number row alone, free for other lights. Stop it with Ctrl-C and the keys go back to your layout.

Settings

Every setting is a flag. The same names work in the config file.

FlagDefaultWhat it does
-colours#00ff00Bar colours, bottom to top: #00ff00,#ffff00,#ff0000 for a meter
-gain1.5Amplify the music first, so the bars reach the top more often
-heightall but the top rowBar height in rows from the bottom
-peakstrueFalling peak dots above the bars
-peak-colour#ffffffPeak dot colour
-peak-hold400msHow long a peak dot holds before it falls
-peak-fall6How fast peak dots fall, in rows a second
-smooth35cava smoothing, 0 to 100: lower is jumpier
-fps30Frames a second asked of cava
-sourcecava’s defaultcava’s capture device, such as BlackHole 2ch
-idle1sSilence before the keys go back to the layout
-reservenoneLEDs never to touch, comma-separated
-modeldetectedBuilt-in map: moonlander or voyager
-mapnoneYour own map file, in place of a built-in one
-stdinfalseRead frames from another program instead of cava
-vfalsePrint the frame rate on exit

With -stdin, any program can drive the bars: one frame per line, one height (0 to the bar height) per column.

printf '1;2;3;4;3;2;1;0;1;2;3;4;\n' | keybars -stdin

Config file

keybars and keybars start read ~/.config/keybars/config ($XDG_CONFIG_HOME/keybars/config when that is set): one flag per line, by its own name. Flags on the command line override the file, and -config FILE reads a different one.

# ~/.config/keybars/config
source = BlackHole 2ch
colours = #00ff00,#ffff00,#ff0000
gain = 2
peaks = false

Lines starting with # are comments; a # later in a line is part of the value, as in the colours. An unknown setting stops keybars and names its line.

Running in the background

keybars start          # in the background, with the config file
keybars start -gain 3  # flags work here too
keybars status         # running or not, and the log's path
keybars stop           # hand the keys back and exit

start keeps a pid file and a log in ~/.local/state/keybars/. If keybars fails straight away, say because Keymapp isn’t running, start prints why.

Start at login on macOS

Save this as ~/Library/LaunchAgents/dev.jwarykowski.keybars.plist, with the path to your keybars:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>dev.jwarykowski.keybars</string>
  <key>ProgramArguments</key>
  <array><string>/Users/you/go/bin/keybars</string></array>
  <key>EnvironmentVariables</key>
  <dict><key>PATH</key><string>/opt/homebrew/bin:/usr/bin:/bin</string></dict>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>ThrottleInterval</key><integer>10</integer>
  <key>StandardErrorPath</key><string>/tmp/keybars.log</string>
</dict>
</plist>
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/dev.jwarykowski.keybars.plist

keybars exits when Keymapp or the keyboard goes away, and KeepAlive starts it again every 10 seconds until they’re back, so set Keymapp to open at login too. PATH is there so launchd can find cava.

Start at login on Linux

A systemd user unit, in ~/.config/systemd/user/keybars.service:

[Unit]
Description=keybars

[Service]
ExecStart=%h/go/bin/keybars
Restart=always
RestartSec=10

[Install]
WantedBy=default.target
systemctl --user enable --now keybars

macOS audio

macOS gives programs no way to listen to what it plays, so cava listens to a loopback device instead.

  1. Install BlackHole: brew install blackhole-2ch.
  2. In Audio MIDI Setup, add a Multi-Output Device with BlackHole 2ch and your speakers or headphones. Make the speakers or headphones the primary device, and tick Drift Correction for BlackHole.
  3. Choose the Multi-Output Device as the sound output, in Control Centre > Sound. Connecting Bluetooth headphones switches the output back to them, so pick it again after. The volume keys don’t work on a Multi-Output Device; use your headphones’ or speakers’ own.
  4. Put source = BlackHole 2ch in the config file, or run keybars -source 'BlackHole 2ch'.

Keyboard maps

A map is Keymapp’s index for each key, drawn as the keyboard looks: one line per row of keys, top to bottom. Columns become bars and rows become levels, so any size works. - marks a gap, | is decoration and # starts a comment. The Moonlander’s:

 -  1  2  3  4  5 | 37 38 39 40 41 42
 7  8  9 10 11 12 | 44 45 46 47 48 49
14 15 16 17 18 19 | 51 52 53 54 55 56
21 22 23 24 25 26 | 57 58 59 60 61 62
27 28 29 30 31  - |  - 63 64 65 66 67

keybars maps lists the built-in maps and which one the connected keyboard uses. The Moonlander and Voyager maps are both tested on real boards.

Adding a keyboard

If QMK knows the keyboard, gen writes its map:

keybars gen zsa/voyager > voyager.txt

Otherwise, find the indices by hand:

  1. keybars find darkens the board and lights one key at a time, printing its index. Write each index where its key sits.
  2. keybars test -map my-board.txt sweeps the bars across it: a key out of line has the wrong index.
  3. Use it with -map my-board.txt, or open a pull request adding it to maps/.

Troubleshooting

Nothing lights up at all. Run keybars test. If that fails too, check Keymapp is open with its API on, that it shows the keyboard as connected, and that the keyboard’s firmware was compiled on Oryx recently enough to support the API. Recompiling and flashing fixes the last one.

test works but the music doesn’t. cava is hearing silence, and keybars keeps the layout’s colours when it does. Check that frames get through:

yes '1;2;3;3;2;1;1;2;3;3;2;1;' | keybars -stdin

If those bars show, the problem is the audio. On macOS:

  • macOS counts listening to BlackHole as using the microphone. Allow your terminal (or keybars, under launchd) in System Settings > Privacy & Security > Microphone; without it, cava hears silence and says nothing.
  • Check the sound output is still the Multi-Output Device, not your headphones.
  • In QuickTime, File > New Audio Recording, with BlackHole 2ch as the input, shows whether sound reaches BlackHole at all.

Keys outside the bars go dark while it plays. That’s the ZSA firmware: once Keymapp drives any key, the board shows only what it sets. They come back when the music stops.