KeyEcho 1.0: 27x Faster Audio Hot Path with Zero-Copy Rust

Two years ago, the author open-sourced KeyEcho, a desktop app that plays mechanical-keyboard sounds on keypress. It got 800+ stars. After a quiet period, v1.0 shipped in a single PR: 130 files, 11,405 lines added, 9,292 removed. The core was a rebuild of the audio hot path, yielding a 27x speedup on average slices (1184.07 ns/op to 43.50 ns/op) and 38x on the largest slice, with zero sample bytes copied.

The Hot Path

KeyEcho's latency-sensitive path is short: a global keyboard hook fires on every key event, pushes the first key-down into a bounded queue, and an audio thread pulls from the queue, maps the key to a sound slice, and plays it. The old v0.0.5 was already fast—Tauri + Rust, native keyboard hooks, lossless WAV with no decompression, LRU cache for decoded audio. But each keypress still copied 66.84 KiB of samples and hit a global mutex shared with pack switching and volume changes.

The Four Moves

  1. Predecode once – When a pack is selected, all slices are decoded up front. No decoding during a keystroke.
  2. Deduplicate identical slices – Many keys point to the same audio slice. The builder uses a HashMap on (start_ms, duration_ms) to decode each unique slice once:
    let mut slice_sources: HashMap<[u64; 2], AudioSource> = HashMap::new();
    let mut key_sources: HashMap = HashMap::new();
    for (key, slice) in defines {
        let source = slice_sources
            .entry(slice)
            .or_insert_with(|| {
                let [start_ms, duration_ms] = slice;
                let samples = decode(start_ms, duration_ms);
                AudioSource::new(Arc::from(samples), channels, sample_rate)
            })
            .clone(); // Arc clone, not sample copy
        key_sources.insert(key, source);
    }
    
  3. Zero-copy playback – Decoded samples live in Arc<[f32]>. A key lookup returns an Arc clone (reference-count bump), not a sample copy. Per-key cost: zero bytes copied.
  4. Drop the global mutex – The new handle uses ArcSwapOption and AtomicU32 for volume. Lookup is lock-free:
    fn source_for_key(&self, key: Key) -> Option<(AudioSource, f32)> {
        let sound = self.current_sound.load();          // lock-free
        let source = sound.as_ref()?.key_source(key)?;  // map get + Arc clone
        let volume = f32::from_bits(self.volume_bits.load(Ordering::Relaxed));
        Some((source, volume))
    }
    

Predecoding trades memory for speed, bounded by a 10 MiB budget and a bounded queue.

Benchmarks

Release-build microbenchmarks of the lookup path:

Pathv0.0.5v1.0Change
Cached lookup, average slice1184.07 ns/op; 66.84 KiB copied43.50 ns/op; 0 bytes copied27.2x faster
Cached lookup, largest slice1638.57 ns/op; 98.88 KiB copied43.10 ns/op; 0 bytes copied38.0x faster
Press/release gate58.82 ns/tap; 2 messages54.69 ns/tap; 1 messagehalf the messages

These are lookup-path microbenchmarks, not end-to-end speaker latency.

Why Rust Made This Safe

The borrow checker and type system caught most AI-generated mistakes at compile time. Moving to Arc<[f32]> and removing the mutex introduced aliasing and Send/Sync errors that Rust rejected before human review.

The AI Fix I Refused

CI for Linux armv7 failed under QEMU: libgit2 couldn't fetch a git dependency. The AI agent suggested dropping the git pin on the audio crate and using the crates.io release. CI went green. But the pin existed to hold cpal at 0.18.1, which has the DeviceChanged event for sound following when you unplug headphones. Reverting to crates.io silently downgraded to cpal 0.17.3, dropping device-following. Two user issues (#41, #20) were about that behavior. The real fix: vendor dependencies with cargo vendor and build offline.

Lessons

  • Write the "why" next to every pin, hack, and magic number. Agents have no memory of the incident.
  • Never accept a dependency version change you didn't ask for, even when CI is green. Green tests don't prove coverage of what you'll lose.

What I Cut

The armv7 build took ~2 hours under emulation with no clear user. Dropped from 1.0. An agent has no sense of opportunity cost—deciding to stop is a human job.

How the Work Was Structured

Before release, the author ran AI review passes inside git worktrees for parallel scanning. Models are specializing: Claude Fable (creative engineer) and GPT-5.6 (executor).

What 1.0 Is

Still free, AGPL-3.0, Tauri 2 + SolidJS. Signed Windows/macOS builds. Linux packages for x64 and ARM64. Try it at keyecho.app without downloading. Benchmarks and method in the repo.

Why It Matters

This article demonstrates concrete techniques for optimizing hot paths in Rust: predecoding, shared buffers via Arc, lock-free state with ArcSwap and atomics. It also shows the practical pitfalls of AI-generated code—specifically, dependency version changes that break undocumented behavior. For developers using AI agents, the lesson to document assumptions explicitly is critical.

Editor's Take

I've been burned by AI agents suggesting dependency bumps that "fix" CI but silently drop features. The cpal pin story is exactly the kind of regression that unit tests miss. My rule: every pin gets a comment with the reason, and I review all dependency changes manually, even if the agent says it's safe. The speedup numbers are impressive, but the real value is the methodology—predecode, deduplicate, zero-copy, lock-free. That pattern applies to any audio or data-heavy hot path.