marketplace/audio

Keyboard Foley

Add Mechanical, Typewriter, or Soft Pop sounds to ordinary typing without Rebind Link. F8 mutes; F9 changes the theme.

@rebindv0.1.0free

More install options
$rebind install @rebind/keyboard-foley

Gallery

Keyboard Foley preview

Readme

Add sound to ordinary typing with three original synthesized themes. Letters, numbers, punctuation, and Tab use three alternating samples per theme. Space, Enter, and Backspace each have a distinct sound.

ThemeSound design
MechanicalShort switch clicks, a low case resonance, and a light return tick.
TypewriterA sharper impact, metallic resonances, and a two-part mechanism sound.
Soft PopRounded, falling-pitch pops with a muted noise attack.

Start and control

Requires Rebind Engine 3.5.0 or newer on Windows or macOS. Rebind Link is not required. See platform support for capture requirements.

Open the package's main.luau in Rebind and start it from the editor toolbar. For development, run rebind run library/keyboard-foley from the repository root. Open Options to change the theme, volume, or shortcut-sound setting. Settings save automatically and apply while running.

  • F8 mutes or unmutes this package.
  • F9 cycles Mechanical, Typewriter, and Soft Pop.
  • Volume starts at 35%. Set it to 0% for silence.
  • Sound with Ctrl, Alt, Cmd, or Win is off by default. Shifted typing still makes sound.

F8 and F9 are reserved while the script runs, including while muted. Other keys keep their normal behavior. Modifier keys by themselves, navigation keys, function keys, and mouse buttons are silent. Holding a key does not produce a sound for each operating-system repeat.

Stop the script from the editor or press Ctrl+C in its development terminal. To stop every script and release held input, press Left Ctrl + Left Alt + K. Keyboard Foley itself never presses or holds a key.

Samples and privacy

The 18 samples are original procedural synthesis, not recordings or borrowed sound packs. tools/synth.py generates 48 kHz, mono, 16-bit PCM WAV files using only Python's standard library. To regenerate them from the repository root:

python3 library/keyboard-foley/tools/synth.py

The runtime opens only its bundled sounds. It does not save typed text, keep keystroke history, use the clipboard, execute commands, or access the network. Sound plays for captured typing in any application, including password fields; the script cannot identify sensitive fields. Mute it when sound is unwanted.

Limits and verification

At most 12 sounds overlap. If another begins at that limit, the oldest stops. Finished sound handles are released, and muting or stopping ends active sounds. An audio error mutes the package and records the reason in Logs. Correct the problem and press F8 to retry.

Playback uses the host's default audio output. Output-device changes and audio latency need live verification on each platform. Sound quality cannot be established by source checks alone. Listing artwork is an illustration, not a screenshot or evidence of execution.

Reviews

No reviews yet. Write a review in the app.

Source

Version 0.1.0 (current)

tools/synth.py

#!/usr/bin/env python3
"""Regenerate Keyboard Foley's original sound samples with the standard library."""

import array
import math
from pathlib import Path
import random
import sys
import wave

RATE = 48000
ROOT = Path(__file__).resolve().parents[1] / "samples"
THEMES = ("mechanical", "typewriter", "soft-pop")
KINDS = ("key-1", "key-2", "key-3", "space", "enter", "backspace")


def synth(theme, kind):
    rng = random.Random(f"keyboard-foley-v1/{theme}/{kind}")
    pitch = {"key-1": 1, "key-2": 1.06, "key-3": 0.94,
             "space": 0.68, "enter": 0.82, "backspace": 1.18}[kind]
    duration = {"mechanical": 0.095, "typewriter": 0.14, "soft-pop": 0.10}[theme]
    if kind in ("space", "enter"):
        duration += 0.025
    out = []
    low, previous, phase = 0.0, 0.0, 0.0
    for i in range(round(duration * RATE)):
        t = i / RATE
        noise = rng.uniform(-1, 1)
        low += 0.16 * (noise - low)
        high = noise - previous
        previous = noise
        if theme == "mechanical":
            # A noisy contact followed by a damped plastic case and return tick.
            value = 0.65 * high * math.exp(-t / 0.0018)
            value += 0.48 * low * math.exp(-t / 0.008)
            value += 0.38 * math.sin(2 * math.pi * 240 * pitch * t) * math.exp(-t / 0.013)
            value += 0.14 * math.sin(2 * math.pi * 1450 * pitch * t) * math.exp(-t / 0.004)
            if t > 0.021:
                value += 0.17 * high * math.exp(-(t - 0.021) / 0.0015)
        elif theme == "typewriter":
            # Inharmonic metal modes and a delayed mechanism impact.
            value = 0.75 * high * math.exp(-t / 0.003)
            for freq, amp, decay in ((740, 0.3, 0.018), (1730, 0.21, 0.012),
                                     (3270, 0.12, 0.007)):
                value += amp * math.sin(2 * math.pi * freq * pitch * t) * math.exp(-t / decay)
            value += 0.4 * low * math.exp(-t / 0.014)
            if t > 0.032:
                tail = t - 0.032
                value += (0.25 * high + 0.18 * math.sin(2 * math.pi * 380 * pitch * tail)) * math.exp(-tail / 0.007)
        else:
            # Integrate a falling frequency for a continuous, rounded pop.
            freq = (155 + 620 * math.exp(-t / 0.009)) * pitch
            phase += 2 * math.pi * freq / RATE
            value = math.sin(phase) * math.exp(-t / 0.014)
            value += 0.10 * low * math.exp(-t / 0.005)
        out.append(value)

    # Taper both boundaries and remove DC before quantizing.
    mean = sum(out) / len(out)
    attack = round(RATE * (0.0015 if theme == "soft-pop" else 0.0005))
    release = round(RATE * 0.008)
    for i, value in enumerate(out):
        fade_in = min(1, i / attack)
        fade_out = min(1, (len(out) - 1 - i) / release)
        out[i] = (value - mean) * fade_in * fade_out
    peak = max(abs(value) for value in out)
    # Leave headroom for overlapping keystrokes; user volume is separate.
    scale = 0.28 * 32767 / peak
    pcm = array.array("h", (round(value * scale) for value in out))
    if sys.byteorder != "little":
        pcm.byteswap()
    return pcm


def main():
    if sys.argv[1:] not in ([], ["--check"]):
        raise SystemExit("usage: synth.py [--check]")
    check = sys.argv[1:] == ["--check"]
    signatures = set()
    for theme in THEMES:
        folder = ROOT / theme
        if not check:
            folder.mkdir(parents=True, exist_ok=True)
        for kind in KINDS:
            pcm = synth(theme, kind)
            path = folder / f"{kind}.wav"
            if check:
                with wave.open(str(path), "rb") as source:
                    assert (source.getnchannels(), source.getsampwidth(), source.getframerate()) == (1, 2, RATE), path
                    raw = source.readframes(source.getnframes())
                    assert raw == pcm.tobytes(), f"sample differs from synthesis: {path}"
                    assert raw[:2] == raw[-2:] == b"\x00\x00", f"nonzero boundary: {path}"
                    assert raw not in signatures, f"duplicate sample: {path}"
                    signatures.add(raw)
                    assert 0.005 < math.sqrt(sum(x * x for x in pcm) / len(pcm)) / 32767 < 0.2, path
            else:
                with wave.open(str(path), "wb") as output:
                    output.setnchannels(1)
                    output.setsampwidth(2)
                    output.setframerate(RATE)
                    output.writeframes(pcm.tobytes())
            print(f"{theme}/{kind}.wav  {len(pcm) / RATE:.3f} s")
    if check:
        print("PASS: 18 distinct, reproducible PCM samples with tapered boundaries")


if __name__ == "__main__":
    main()

19 files without text previews