use real sounds from kbsim
This commit is contained in:
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
DEST="$ROOT/assets/samples/kbsim"
|
||||
TMP="$(mktemp -d)"
|
||||
REPO="https://github.com/tplai/kbsim.git"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "Cloning kbsim audio assets..."
|
||||
git clone --depth 1 --filter=blob:none --sparse "$REPO" "$TMP/kbsim"
|
||||
cd "$TMP/kbsim"
|
||||
git sparse-checkout set src/assets/audio
|
||||
|
||||
AUDIO_SRC="$TMP/kbsim/src/assets/audio"
|
||||
if [[ ! -d "$AUDIO_SRC" ]]; then
|
||||
echo "Expected audio directory not found in kbsim repo" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$DEST"
|
||||
mkdir -p "$DEST"
|
||||
|
||||
# folder:switch_id pairs (kbsim folder name -> sfxkeeb switch id)
|
||||
PAIRS=(
|
||||
"cream:cream"
|
||||
"holypanda:holypanda"
|
||||
"alpaca:alpaca"
|
||||
"turquoise:turquoise"
|
||||
"blackink:inkblack"
|
||||
"redink:inkred"
|
||||
"mxblack:mxblack"
|
||||
"mxbrown:mxbrown"
|
||||
"mxblue:mxblue"
|
||||
"boxnavy:boxnavy"
|
||||
"buckling:buckling"
|
||||
"bluealps:alpsblue"
|
||||
"topre:topre"
|
||||
)
|
||||
|
||||
for pair in "${PAIRS[@]}"; do
|
||||
folder="${pair%%:*}"
|
||||
switch_id="${pair##*:}"
|
||||
if [[ -d "$AUDIO_SRC/$folder" ]]; then
|
||||
cp -R "$AUDIO_SRC/$folder" "$DEST/$switch_id"
|
||||
echo " copied $folder -> $switch_id"
|
||||
else
|
||||
echo " warning: missing folder $folder" >&2
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Generating manifest.json..."
|
||||
python3 - "$DEST" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
dest = Path(sys.argv[1])
|
||||
manifest: dict[str, dict[str, list[str]]] = {}
|
||||
|
||||
for switch_dir in sorted(dest.iterdir()):
|
||||
if not switch_dir.is_dir() or switch_dir.name == "__pycache__":
|
||||
continue
|
||||
switch_id = switch_dir.name
|
||||
press_dir = switch_dir / "press"
|
||||
release_dir = switch_dir / "release"
|
||||
press = sorted(p.stem for p in press_dir.glob("*.mp3")) if press_dir.is_dir() else []
|
||||
release = sorted(p.stem for p in release_dir.glob("*.mp3")) if release_dir.is_dir() else []
|
||||
manifest[switch_id] = {"press": press, "release": release}
|
||||
|
||||
manifest_path = dest / "manifest.json"
|
||||
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n")
|
||||
total = sum(len(v["press"]) + len(v["release"]) for v in manifest.values())
|
||||
print(f" wrote {manifest_path} ({len(manifest)} switches, {total} samples)")
|
||||
PY
|
||||
|
||||
echo "Done. Samples installed to $DEST"
|
||||
@@ -1,83 +0,0 @@
|
||||
"""Generate synthetic mechanical keyboard switch samples."""
|
||||
|
||||
import math
|
||||
import struct
|
||||
import wave
|
||||
from pathlib import Path
|
||||
|
||||
SAMPLE_RATE = 48000
|
||||
OUT_DIR = Path(__file__).resolve().parent.parent / "assets" / "samples"
|
||||
|
||||
|
||||
def write_wav(path: Path, samples: list[float]) -> None:
|
||||
pcm = b"".join(
|
||||
struct.pack("<h", max(-32768, min(32767, int(s * 32767)))) for s in samples
|
||||
)
|
||||
with wave.open(str(path), "w") as wf:
|
||||
wf.setnchannels(1)
|
||||
wf.setsampwidth(2)
|
||||
wf.setframerate(SAMPLE_RATE)
|
||||
wf.writeframes(pcm)
|
||||
|
||||
|
||||
def envelope(length: int, attack: float, decay: float) -> list[float]:
|
||||
env = []
|
||||
attack_samples = int(attack * SAMPLE_RATE)
|
||||
decay_samples = int(decay * SAMPLE_RATE)
|
||||
for i in range(length):
|
||||
if i < attack_samples:
|
||||
env.append(i / max(attack_samples, 1))
|
||||
elif i < attack_samples + decay_samples:
|
||||
t = (i - attack_samples) / max(decay_samples, 1)
|
||||
env.append(1.0 - t)
|
||||
else:
|
||||
env.append(0.0)
|
||||
return env
|
||||
|
||||
|
||||
def generate_click(
|
||||
freq: float,
|
||||
duration: float,
|
||||
attack: float,
|
||||
decay: float,
|
||||
noise_mix: float = 0.0,
|
||||
click_burst: bool = False,
|
||||
) -> list[float]:
|
||||
length = int(duration * SAMPLE_RATE)
|
||||
env = envelope(length, attack, decay)
|
||||
out = []
|
||||
for i in range(length):
|
||||
t = i / SAMPLE_RATE
|
||||
tone = math.sin(2 * math.pi * freq * t) * (0.7 if not click_burst else 0.4)
|
||||
if click_burst and t < 0.008:
|
||||
tone += math.sin(2 * math.pi * (freq * 2.8) * t) * 0.5
|
||||
noise = 0.0
|
||||
if noise_mix > 0:
|
||||
noise = (((i * 1103515245 + 12345) & 0x7FFFFFFF) / 0x7FFFFFFF - 0.5) * noise_mix
|
||||
out.append((tone + noise) * env[i])
|
||||
peak = max(abs(s) for s in out) or 1.0
|
||||
return [s / peak * 0.85 for s in out]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
profiles = {
|
||||
"cherry-mx-blue.wav": generate_click(
|
||||
2800, 0.09, 0.001, 0.085, noise_mix=0.15, click_burst=True
|
||||
),
|
||||
"cherry-mx-red.wav": generate_click(
|
||||
1200, 0.05, 0.002, 0.045, noise_mix=0.08
|
||||
),
|
||||
"cherry-mx-brown.wav": generate_click(
|
||||
1800, 0.07, 0.002, 0.065, noise_mix=0.12, click_burst=True
|
||||
),
|
||||
}
|
||||
|
||||
for name, samples in profiles.items():
|
||||
write_wav(OUT_DIR / name, samples)
|
||||
print(f"Wrote {OUT_DIR / name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user