BUILD · 2026-09-06 · the FrankenKeys log

Fifty key sounds, no audio files

How FrankenKeys synthesises fifty key clicks live in C++ instead of playing samples, why no two presses are identical, and what that costs and buys.

2026-09-06 · by Corey Redwine · custom key sound keyboard android

Open the FrankenKeys package and look for .wav or .ogg files. There are none. Fifty key sounds, and not one recording. Each is rendered at startup by a small C++ engine from a parameter table, then played through a low-latency audio stream when you press a key. This is why, and what it took.

Why not just record a keyboard

Sampled keyboards give themselves away in one specific way: a fast typist hears the same file repeated, and the ear picks up the repetition within a few words. It sounds like a machine gun. The usual fix is to record several variants and pick randomly, which helps and also quadruples the audio in the package.

Synthesis solves both. The engine renders three near-identical variants of each sound and cycles through them, so a burst on one key never plays the same buffer twice in a row. On top of that, every key carries a fixed pitch offset of up to 25 cents either way, derived from its key code, so q always sounds like q and never quite like p. Each press adds up to 12 cents of jitter on top. And each press carries a velocity from how the key was struck, which becomes a per-voice gain between 0.55 and 1.15. Same sound, never the same press.

What fifty sounds are

Seven originals: a Cherry-style click, a Topre thock, a cream, a tick, a typewriter, a bubble and a laser. Twenty-three more in the extra pack, from Gateron reds and box jades through beam spring and buckling spring to marble, wood block, glass tap, raindrop, heartbeat, arcade, sci-fi, thunder, snare, hi-hat, bongo and robot. Then ten in the dark pack: coffin lid, bone tap, iron latch, crypt drip, music box, chain link, stone slab, floor creak, crypt bell and jar lid. And ten in the lab pack: tesla arc, Jacob's ladder, relay throw, beaker clink, flask bubble, geiger tick, vacuum tube, lever pull, jar of eyes and lab bell.

The rule for the dark pack was set before the first one was written, and the lab pack inherited it: every sound is an object being struck, dripped on or thrown, never a living thing in trouble, so the app stays rated for everyone. That rule lives beside the parameter table in the C++ file.

A fifty-first slot is yours: import any audio file, trim it to a fraction of a second on a waveform, and it becomes the click. That is a Pro feature, and the imported waveform is stored only as a key-click buffer.

What the engine actually does

The audio runs on Oboe, Google's low-latency audio library, at 48 kHz. The design rules, learned the hard way from a race audit:

  • The audio callback is the sole owner of every voice. Nothing else touches them.
  • Key presses arrive as packed commands in a lock-free single-producer, single-consumer ring the callback drains at the top of each burst.
  • Waveforms live in two banks. A profile change renders into the inactive bank and publishes it with one atomic index; a flush flag silences voices still reading the old bank before it can be rewritten.
  • No allocation, no locks, no system calls on the callback path.

Eight voices, so a fast roll of keys overlaps rather than cutting off. Each buffer is at most 500 milliseconds, which is room for a user-imported sound; the synthesised clicks are far shorter.

Since the release-feel work, each profile also renders the key coming back up: a shorter, duller version of the same switch, so a press and release read as one keystroke rather than two events. Profiles with a reset echo, the little second tick a real switch makes, render it as its own 28 millisecond part so it can be re-added with a delay only for taps too quick to have had a real release.

N/I

No audio files in the package. Pull the APK and list its contents: unzip -l frankenkeys.apk | grep -iE "wav|ogg|mp3" returns nothing. The engine is lib/<abi>/libforgeaudio.so and the parameter table is in its source, forgeaudio.cpp. How to pull the APK.

What it costs

Two banks of 27 slots of up to 24,000 float frames each is about 5 megabytes of virtual address space, declared static so the allocator never sees it. Pages are only committed when written and a click touches a handful of its slots, so the resident cost is a fraction of that. It is still not free, and a keyboard's memory budget is the thing that gets it killed by the system, so I watch it.

The other cost is that synthesis has a ceiling. A real Topre board recorded well sounds better than my Topre profile, and I am not going to pretend otherwise. What the synthesis gives up in fidelity it returns in variation, size and the fact that no sample library licence is involved.

Twenty haptics under it

The haptic profiles follow the same idea. Twenty of them, ordered light to heavy: feather, soft, light, ripple, twitch, swell, bump, medium, triple, stagger, pulse, crisp, decay, double, rebound, strike, thud, anvil, heavy, jolt. On Android 11 and later, where the actuator reports support for the click and tick primitives, each profile is a composition of the motor's own tuned transients, scaled by your intensity slider, so intensity is real amplitude and not a shorter buzz. On older hardware the predefined effects are used at full strength and a waveform is scaled underneath. Every profile is resolved once at startup to a cached immutable effect, so the key-down path is one method call.

Twenty things, separated by rhythm, not one thing at twenty volumes. The effects page lists all of it in one place.

The numbers in this post, and where they are

  • 50 catalogue sounds: SoundProfiles.CATALOGUE_SIZE in audio/AudioEngine.kt, the ids listed above it.
  • 3 variants per sound, 8 voices, 24,000 frames per buffer, 48 kHz: constants at the top of forgeaudio.cpp.
  • 25 cents per-key offset, 12 cents jitter, gain 0.55 to 1.15: ClickFeel in audio/AudioEngine.kt.
  • 20 haptic profiles: HapticEngine.PROFILES.

Nothing was recorded, and nothing suffered. The coffin lid is a filtered noise burst with a wooden resonance. I am aware of how that sentence reads.