Sound packs
The .wmsoundpack format — many recordings of one keyboard, one played per key press and one when you lift, with optional per-key-role sounds.
A sound pack is a key-press sound with more than one recording in it.
A plain key sound addon (type: "sound") is one audio file, and for a
synthesized blip that’s exactly right. It can’t express what a recording of a
real keyboard sounds like, though. A real board never makes the same noise
twice, a Space bar with stabilizers under it doesn’t sound like
a letter key, and a mechanical switch makes a second, quieter noise when your
finger comes off it. A pack carries all three.
If you just want to install one, see Sound & vibration. This page is for publishing them.
The file
Section titled “The file”A pack is a ZIP archive named *.wmsoundpack:
cherrymx-blue-abs.wmsoundpack├── pack.json└── sounds/ ├── 1.wav ├── 2.wav └── …Two rules, both enforced on install:
pack.jsonsits at the archive root.- Every audio file lives under
sounds/.
A path that’s absolute, contains .., or uses a backslash is rejected rather
than sanitized. More precisely: the importer never treats an archive entry name
as a path at all. It reads each entry into memory under that name as a map
key, then writes the bytes to a file it names itself (s000.snd, s001.snd,
…), rewriting pack.json to match. So neither a ../ entry name nor a ../
inside the manifest has anywhere to go.
pack.json
Section titled “pack.json”{ "format": "wmkeyboard-sound-pack", "version": 1,
"id": "cherrymx-blue-abs", "name": "CherryMX Blue ABS", "author": "Monkeytype contributors", "packVersion": "1.0.0", "description": "Cherry MX Blue clickies under ABS keycaps.",
"gain": 1.0,
"press": ["sounds/1.wav", "sounds/2.wav", "sounds/3.wav"], "release": ["sounds/1-up.wav", "sounds/2-up.wav", "sounds/3-up.wav"],
"roles": { "space": { "press": ["sounds/space-1.wav"] }, "enter": { "press": ["sounds/enter-1.wav"] }, "delete": { "press": ["sounds/delete-1.wav"] }, "modifier": { "press": ["sounds/mod-1.wav"], "gain": 0.8 } }}| Field | Required | What it does |
|---|---|---|
format | ✔ | Must be exactly wmkeyboard-sound-pack. Anything else is refused. |
version | ✔ | Format version, currently 1. A higher number is refused rather than guessed at. |
id | ✔ | [A-Za-z0-9._-]+, at most 64 characters. Frozen once published — it’s how installs are keyed. |
name | ✔ | What the user sees in the picker. |
press | ✔ | At least one path. The default key-down recordings. |
author | Shown on the row. | |
packVersion | Semver, informational. The addon entry’s version is what drives updates. | |
description | ||
gain | 0.0–1.0, default 1.0. See Gain is a cut. | |
release | Key-up recordings, played when the finger lifts. See Key-down and key-up. | |
roles | Per-key-role overrides, below. |
Unknown top-level fields are ignored, so a later version can add one without breaking today’s app.
Key-down and key-up
Section titled “Key-down and key-up”A keystroke has two halves, and a mechanical keyboard makes a noise at both: the
switch actuating under the finger, and the stem returning when it comes off.
press is the first, release is the second, and the keyboard plays each at the
moment it happens. Hold a key down and the second sound waits for you.
An empty release means silence on key-up, not a fallback to press. Most
sounds worth typing on are one event — a beep, a pop, an interface click — and
inventing a second one for them would double every keystroke. So a pack with no
release behaves exactly as packs did before the field was played.
Roles and the split are independent. A role can fill press, release, both or
neither, and each field falls back to the pack’s top-level list on its own,
so a space role that names only press still plays the pack’s default key-up.
The two lists are drawn from independently: press[3] and release[3] are not
a pair and are never treated as one, so the lists can be different lengths.
The user can turn key-up sounds off in WM KeyboardKey pressKey press soundKey release sound, which appears only once a pack that has them is selected. A switch that does nothing visible is worse than no switch — it reads as the feature being broken rather than as the pack not having it.
Recording them
Section titled “Recording them”Record the two halves as separate files. If you are cutting them out of recordings that hold both — which is what the monkeytype packs needed — cut at the quietest point between the two events rather than at a fixed delay: the gap varies by tens of milliseconds between recordings of the same switch, and a fixed cut clips the key-up’s attack on the fast ones.
Don’t normalize the halves separately. A key-up is genuinely quieter than its key-down — a tenth to a half the level on real switches — and levelling them to match makes typing sound like two keyboards.
A role is which kind of key was pressed. There are five:
| Role | Which keys |
|---|---|
default | Everything not listed below: letters, digits, punctuation, emoji. Not written under roles; it’s the top-level press and release. |
space | The Space bar. |
enter | Enter and the action key. |
delete | ⌫, including its held repeat and the delete-swipe, plus forward delete. |
modifier | Shift, ?123, ABC, the globe, emoji and the rest of the non-typing furniture. |
A role that’s absent falls back to the default, per field. A pack that
defines only roles.space.press still plays its top-level press everywhere
else, and its top-level gain for the spacebar. A pack with no roles at all
behaves exactly like a one-sound addon that happens to have variants.
Unknown role names are ignored rather than refused, so a pack written against a future version that adds one still installs and still works.
The grouping is by what a key sounds like under a finger, not by what it does.
Emoji, language switch and the layer keys are all modifier because they sit in
the same bottom-row furniture as shift, and anyone who recorded that row
recorded them together.
Variants
Section titled “Variants”Every press and release is a list, and one entry is picked per keystroke.
The pick is uniform random with one rule on top: the same variant never plays twice in a row while the list has more than one entry. Each role tracks its own last-played index, per half, so alternating between the spacebar and letters never makes either repeat, and a run of key-ups doesn’t depend on how many letters were typed between them.
That rule is a deliberate difference from monkeytype, whose own player picks uniformly across the whole list. On a three-variant set that repeats the previous sample about a third of the time, and a repeat is the exact thing variants exist to prevent.
Limits
Section titled “Limits”| Limit | Value | Why |
|---|---|---|
| Audio files per pack | 64 | Every file is decoded into the SoundPool when the pack is selected. Key-up recordings count toward it. |
| Variants per list | 32 | |
| Bytes per audio file | 4 MB | Same ceiling a single-sound addon gets. |
| Unzipped bytes per pack | 16 MB | Counted while extracting, not read from the ZIP header, so a zip bomb stops mid-stream. |
| Installed packs | 20 |
Going over any of these refuses the install with a message naming the limit. It never truncates the pack to fit: a pack quietly missing half its recordings is worse than one that didn’t install.
One thing that isn’t fatal is a press entry naming a file that isn’t in the
archive. That variant is dropped and the rest of the pack is kept, because
losing one of ten switch recordings isn’t worth refusing the other nine. A pack
with nothing left after that is refused.
Audio files
Section titled “Audio files”WAV, OGG and MP3, checked by header rather than by extension.
Ship WAV unless the pack is big enough for that to hurt. These are 20–150 ms recordings, so compression saves a few hundred kilobytes and costs the thing that matters: MP3 carries encoder delay, and the decoder emits a block of silence before the first real sample. On a 30 ms click that’s a tenth of the sound arriving late on every keystroke. WAV decodes to PCM with nothing in front of it.
Mono is enough. SoundPool plays both channels of a stereo file at the same
volume anyway, so stereo doubles the size for a difference nobody hears through
a phone speaker.
Gain is a cut
Section titled “Gain is a cut”SoundPool.play takes a volume of 0.0–1.0 and cannot amplify. gain is
therefore an attenuation, never a boost, and is clamped into that range on load.
A pack that ships quiet samples and asks for "gain": 2.0 gets 1.0 and stays
quiet.
Normalize loud at build time and use gain to pull a role back down: a spacebar
that recorded hotter than the letters, say. The user’s own key-sound volume
multiplies with it, so it’s for balance within a pack, not for setting the
pack’s overall level.
Publishing one
Section titled “Publishing one”In a repository manifest, a pack is type: "sound_pack":
{ "id": "cherrymx-blue-abs", "type": "sound_pack", "name": "CherryMX Blue ABS", "version": "1.0.0", "author": "Monkeytype contributors", "description": "Cherry MX Blue clickies under ABS keycaps. Ten recordings.", "tags": ["switch", "cherry", "clicky"], "path": "packs/cherrymx-blue-abs.wmsoundpack", "sha256": "…", "sizeBytes": 262144, "previews": ["previews/cherrymx-blue-abs.png"], "minAppVersion": 4, "license": "GPL-3.0-or-later", "licenseFile": "LICENSE"}Notes:
- Set
minAppVersion.sound_packdidn’t always exist, and an older app reads an unknown type asunknownand offers no way to install it. The floor turns that into a message that says why. - The manifest
idand thepack.jsoniddon’t have to match, and the app keys the install on the manifest’s. Making them the same is still sensible. - The addon entry’s
versiondrives update detection.
Installing a pack selects it, the same as a theme or a single sound: a pack that installs without the keyboard making its noise reads as an install that did nothing.
The detail page offers a Preview before installing, which extracts up to eight variants of each half and gives every one its own Play row. One button that played a random variant would turn “how much does this vary?” into a guessing game, and that question is the whole reason to pick a pack over a single sound.
A worked example
Section titled “A worked example”wasi-master/wmkeyboard-monkeytype-sounds is a complete repository of 26 packs built from monkeytype’s sound files, with the importer, validator and preview generator that produced them. It’s a reasonable thing to fork.
It’s also a worked example of the key-up cut: monkeytype plays one file per key press, so its switch recordings hold the return in the same file 100–200 ms behind the press, and the importer finds that boundary and cuts there. Which sets get cut is written down by hand, because a detector can’t tell a key coming back up from the second half of a punch — Fist Fight trips it on all eight recordings and is deliberately left whole.
Known gaps
Section titled “Known gaps”- No per-key mapping. Mechvibes-style packs address individual key codes. Roles cover the case that’s actually audible on a phone, the spacebar, without asking anyone to record 104 keys for a keyboard that has about 30.
- Key-up is a sound-pack feature only. The single-file
soundaddon type and the built-in styles have one sound each, and there’s no second file to play.
