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 cavaorapt 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.
| Flag | Default | What it does |
|---|---|---|
-colours | #00ff00 | Bar colours, bottom to top: #00ff00,#ffff00,#ff0000 for a meter |
-gain | 1.5 | Amplify the music first, so the bars reach the top more often |
-height | all but the top row | Bar height in rows from the bottom |
-peaks | true | Falling peak dots above the bars |
-peak-colour | #ffffff | Peak dot colour |
-peak-hold | 400ms | How long a peak dot holds before it falls |
-peak-fall | 6 | How fast peak dots fall, in rows a second |
-smooth | 35 | cava smoothing, 0 to 100: lower is jumpier |
-fps | 30 | Frames a second asked of cava |
-source | cava’s default | cava’s capture device, such as BlackHole 2ch |
-idle | 1s | Silence before the keys go back to the layout |
-reserve | none | LEDs never to touch, comma-separated |
-model | detected | Built-in map: moonlander or voyager |
-map | none | Your own map file, in place of a built-in one |
-stdin | false | Read frames from another program instead of cava |
-v | false | Print 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.
- Install BlackHole:
brew install blackhole-2ch. - 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.
- 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.
- Put
source = BlackHole 2chin the config file, or runkeybars -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:
keybars finddarkens the board and lights one key at a time, printing its index. Write each index where its key sits.keybars test -map my-board.txtsweeps the bars across it: a key out of line has the wrong index.- Use it with
-map my-board.txt, or open a pull request adding it tomaps/.
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.