TL;DR: I've recently open-sourced KeyEcho, an application that listens to your keyboard typing and plays pleasing sounds (like mechanical keyboard sounds). Built with Tauri, it's cross-platform and highly performant. This article covers its performance comparison and some technical insights.
I love the sound of mechanical keyboard keys and have tried many keyboards. Currently, I am using a capacitive keyboard, but it lacks a lot of the fun.
Recently, I discovered Mechvibes and Klack. Both can produce mechanical keyboard sounds when typing. However, both have some shortcomings:
Mechvibes is an open-source, cross-platform application built with Electron. This makes it large in memory and disk usage, and it occasionally lags.
Klack is a paid, closed-source project limited to macOS. It is built natively with Swift, and it runs smoothly on macOS. However, I have multiple devices and want to use it on other platforms as well. Additionally, its sound packs are not as varied or customizable as those in Mechvibes.
So recently, I created my ideal application, KeyEcho. It uses Tauri and benefits from Rust, resulting in minimal memory and CPU usage and extremely fast performance.
Here is a rough comparison video of KeyEcho, Klack, Mechvibes, and rustyvibes on a MacBook:
You can see that it has excellent CPU and memory usage during rapid key presses. This is mainly due to native event listening, Rust's high performance, and some caching techniques.
Technical Overview
As mentioned earlier, it is built using the Tauri framework - unlike Electron, which bundles Chromium and Node.js, consuming a lot of memory and disk space. Instead, Tauri uses each platform's WebView to implement desktop applications, with Rust as the backend. This allows it to achieve high performance with minimal disk space usage.
Let's break down the composition of KeyEcho:
Backend
The backend uses Rust, where the main logic is to listen to user keystrokes and play the corresponding key sounds:
Listening to Keystrokes:
In this directory, you can see the native keyboard event listening logic for the three major platforms (Windows, MacOS, Linux). Thanks to Rust's friendly interaction with the C language, native keyboard listening can be implemented with minimal code and minimal use of unsafe code.
This logic is inspired by rdev, a Rust library for listening to and sending keyboard and mouse events. This is also what rustyvibes uses internally. KeyEcho does not use this library directly because it cannot customize the selection of listening to either keyboard or mouse events, and it contains a lot of logic that KeyEcho doesn't need. Therefore, I chose to implement it myself to achieve pure native keyboard event listening logic.
Playing Sounds Based on Keystrokes:
The logic here is to obtain an audio file and a key's start time and duration within the audio file, so that when a user keystroke is detected, the corresponding audio segment can be found and played.
Audio Files and Configuration Files:
These audio files are sourced from the existing Mechvibes repository, which contains many mechanical keyboard audio configurations. I processed them further to suit KeyEcho. For example, I chose the WAV format for audio files, which is lossless and requires no decompression, minimizing processing delay.
WAV Audio File Decoding and Playback:
After obtaining the entire WAV file, I used Symphonia for decoding and rodio for playback.
In fact, rodio can handle this task alone - decoding, seeking to the specified audio position, and playing for the specified duration. However, after reviewing its source code, I found that it performs additional tasks for greater compatibility, which consume more CPU and memory than I wanted. Therefore, I used its dependency, Symphonia, for lower-level decoding to obtain the decoded resources at a lower cost and then handed them over to rodio for playback.
Decoded Resource Caching:
Since we often press many repeat keys while using the keyboard, re-decoding each time is unnecessary. Using caching techniques allows us to quickly retrieve it from memory. Here, I chose LRU Cache, which is well-suited for this scenario. It automatically removes keys we rarely use when the capacity is full.
Frontend
For the frontend, I chose Vue3 + radix-vue + tailwindcss. This is a compromise to further reduce the size. I know using Svelte or Solid might reduce the size even further, but for future scalability and a better UI library, I chose Vue3 (yes, I gave up on React long ago due to its size).
This is where Tauri's charm comes into play; it allows defining commands in Rust and exposing them to the frontend, enabling simple communication between the frontend and the Rust backend via the @tauri-apps/api. I used tauri-specta to achieve type-safe Rust command exports to TypeScript.
Currently, the frontend only provides features like audio download selection, but there is much more we can do. Your valuable PRs are welcome.
What's Next
I will continue to improve this program by adding more audio resources and supporting custom audio. Your valuable feedback is greatly appreciated to make this project better.
Additionally, I will share a series of articles on building Tauri applications in my newsletter to consolidate my ideas and help more people. Please consider subscribing for updates.
Thank you for reading!
Top comments (0)